diff --git a/.changeset/analytics-millenial-whoop.md b/.changeset/analytics-millenial-whoop.md deleted file mode 100644 index 265e0072ce..0000000000 --- a/.changeset/analytics-millenial-whoop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-app-api': patch ---- - -Fixed a bug that could cause `navigate` analytics events to be misattributed to the plugin mounted on the root route (e.g. the `home` plugin at `/`) when the route that was navigated to wasn't associated with a routable extension. diff --git a/.changeset/beige-files-reflect.md b/.changeset/beige-files-reflect.md deleted file mode 100644 index e888d5e0f8..0000000000 --- a/.changeset/beige-files-reflect.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-search-backend-module-techdocs': patch -'@backstage/plugin-search-backend-module-catalog': patch -'@backstage/plugin-search-backend-module-explore': patch ---- - -Ensure that all services are dependency injected into the module instead of taken from options diff --git a/.changeset/big-bananas-sleep.md b/.changeset/big-bananas-sleep.md new file mode 100644 index 0000000000..71a0772720 --- /dev/null +++ b/.changeset/big-bananas-sleep.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-playlist': patch +--- + +Updated latest playlist screen in readme document diff --git a/.changeset/big-masks-divide.md b/.changeset/big-masks-divide.md new file mode 100644 index 0000000000..f1d346ba7d --- /dev/null +++ b/.changeset/big-masks-divide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +Fixed a bug where customizable home page cards would render missing their normal borders. diff --git a/.changeset/blue-rats-drop.md b/.changeset/blue-rats-drop.md new file mode 100644 index 0000000000..19b732be1a --- /dev/null +++ b/.changeset/blue-rats-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Introduced a new utility for testing service factories, `ServiceFactoryTester`. diff --git a/.changeset/brave-books-pretend.md b/.changeset/brave-books-pretend.md new file mode 100644 index 0000000000..ee80aedb18 --- /dev/null +++ b/.changeset/brave-books-pretend.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-playlist': patch +--- + +change color of delete icon to secondary in playlist table diff --git a/.changeset/brave-carrots-occur.md b/.changeset/brave-carrots-occur.md new file mode 100644 index 0000000000..017a9bdd8c --- /dev/null +++ b/.changeset/brave-carrots-occur.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-pg': patch +--- + +Added `indexerBatchSize` option to be able to control the size of the batches being indexed. Also added a debug log entry to list out all the entities in the batch diff --git a/.changeset/brown-taxis-develop.md b/.changeset/brown-taxis-develop.md deleted file mode 100644 index 19ebfdef1a..0000000000 --- a/.changeset/brown-taxis-develop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-app-api': patch ---- - -Do not throw in backend.stop, if start failed diff --git a/.changeset/calm-cups-smile.md b/.changeset/calm-cups-smile.md new file mode 100644 index 0000000000..68fcfb2d1a --- /dev/null +++ b/.changeset/calm-cups-smile.md @@ -0,0 +1,60 @@ +--- +'@backstage/plugin-api-docs-module-protoc-gen-doc': patch +'@backstage/plugin-catalog-unprocessed-entities': patch +'@backstage/plugin-github-pull-requests-board': patch +'@backstage/integration-react': patch +'@backstage/plugin-github-deployments': patch +'@backstage/plugin-microsoft-calendar': patch +'@backstage/plugin-newrelic-dashboard': patch +'@backstage/plugin-entity-validation': patch +'@backstage/plugin-apollo-explorer': patch +'@backstage/plugin-gitops-profiles': patch +'@backstage/plugin-graphql-voyager': patch +'@backstage/plugin-apache-airflow': patch +'@backstage/plugin-github-actions': patch +'@backstage/plugin-octopus-deploy': patch +'@backstage/plugin-splunk-on-call': patch +'@backstage/plugin-config-schema': patch +'@backstage/plugin-github-issues': patch +'@backstage/plugin-azure-devops': patch +'@backstage/plugin-code-climate': patch +'@backstage/plugin-gcp-projects': patch +'@backstage/plugin-azure-sites': patch +'@backstage/plugin-firehydrant': patch +'@backstage/plugin-cloudbuild': patch +'@backstage/plugin-home-react': patch +'@backstage/plugin-lighthouse': patch +'@backstage/plugin-tech-radar': patch +'@backstage/plugin-codescene': patch +'@backstage/plugin-dynatrace': patch +'@backstage/plugin-gcalendar': patch +'@backstage/plugin-org-react': patch +'@backstage/plugin-shortcuts': patch +'@backstage/plugin-sonarqube': patch +'@backstage/plugin-xcmetrics': patch +'@backstage/plugin-airbrake': patch +'@backstage/plugin-circleci': patch +'@backstage/plugin-graphiql': patch +'@backstage/plugin-linguist': patch +'@backstage/plugin-newrelic': patch +'@backstage/plugin-opencost': patch +'@backstage/plugin-periskop': patch +'@backstage/plugin-playlist': patch +'@backstage/plugin-puppetdb': patch +'@backstage/plugin-bitrise': patch +'@backstage/plugin-jenkins': patch +'@backstage/plugin-rollbar': patch +'@backstage/plugin-allure': patch +'@backstage/plugin-badges': patch +'@backstage/plugin-bazaar': patch +'@backstage/plugin-sentry': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-ilert': patch +'@backstage/plugin-kafka': patch +'@backstage/plugin-nomad': patch +'@backstage/plugin-vault': patch +'@backstage/plugin-todo': patch +'@backstage/plugin-org': patch +--- + +Moved `@types/react` to be a regular dependency diff --git a/.changeset/catalog-everybody-to-the-limit.md b/.changeset/catalog-everybody-to-the-limit.md new file mode 100644 index 0000000000..4c0177820e --- /dev/null +++ b/.changeset/catalog-everybody-to-the-limit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fix to the `limit` parameter on entity queries. diff --git a/.changeset/chatty-foxes-buy.md b/.changeset/chatty-foxes-buy.md deleted file mode 100644 index 609ddc5269..0000000000 --- a/.changeset/chatty-foxes-buy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-graph': patch ---- - -Propagate entity spec to EntityNode so that spec info such as type can be used for graph node customization diff --git a/.changeset/chatty-goats-mate.md b/.changeset/chatty-goats-mate.md new file mode 100644 index 0000000000..2d4911c603 --- /dev/null +++ b/.changeset/chatty-goats-mate.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-kubernetes-common': patch +--- + +Add AWS Annotations to Kubernetes Cluster Resource diff --git a/.changeset/chatty-seahorses-juggle.md b/.changeset/chatty-seahorses-juggle.md deleted file mode 100644 index 8d699699a7..0000000000 --- a/.changeset/chatty-seahorses-juggle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes-backend': patch ---- - -Fixed a bug where the proxy was not rewriting WebSocket request paths properly. diff --git a/.changeset/chatty-spiders-serve.md b/.changeset/chatty-spiders-serve.md new file mode 100644 index 0000000000..e2b5e5630e --- /dev/null +++ b/.changeset/chatty-spiders-serve.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Add support for discovering additional service factories during startup. diff --git a/.changeset/chilly-balloons-enjoy.md b/.changeset/chilly-balloons-enjoy.md new file mode 100644 index 0000000000..e541e4d4cc --- /dev/null +++ b/.changeset/chilly-balloons-enjoy.md @@ -0,0 +1,6 @@ +--- +'@backstage/frontend-plugin-api': minor +'@backstage/frontend-app-api': minor +--- + +Initial release diff --git a/.changeset/chilly-keys-count.md b/.changeset/chilly-keys-count.md deleted file mode 100644 index 3bbc4e40dc..0000000000 --- a/.changeset/chilly-keys-count.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-playlist': patch ---- - -Displaying an alert popup each time the Playlist is created or deleted diff --git a/.changeset/chilly-parrots-yell.md b/.changeset/chilly-parrots-yell.md new file mode 100644 index 0000000000..fe4c760923 --- /dev/null +++ b/.changeset/chilly-parrots-yell.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Added title to props for `HasCards`. diff --git a/.changeset/clean-houses-invent.md b/.changeset/clean-houses-invent.md new file mode 100644 index 0000000000..fe4a252d48 --- /dev/null +++ b/.changeset/clean-houses-invent.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-graphiql': patch +--- + +Added experimental exports for the new frontend system under `/alpha`. diff --git a/.changeset/cold-apricots-poke.md b/.changeset/cold-apricots-poke.md new file mode 100644 index 0000000000..b050c8082b --- /dev/null +++ b/.changeset/cold-apricots-poke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Changed the processing loop task pipeline implementation from recursive to iterative diff --git a/.changeset/cold-numbers-sleep.md b/.changeset/cold-numbers-sleep.md deleted file mode 100644 index ac53307b07..0000000000 --- a/.changeset/cold-numbers-sleep.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Fix the get entities query in the `MyGroupsPicker` to query the `kind=Group` entities. diff --git a/.changeset/create-app-1690284535.md b/.changeset/create-app-1692704146.md similarity index 100% rename from .changeset/create-app-1690284535.md rename to .changeset/create-app-1692704146.md diff --git a/.changeset/create-app-1690892926.md b/.changeset/create-app-1693311380.md similarity index 100% rename from .changeset/create-app-1690892926.md rename to .changeset/create-app-1693311380.md diff --git a/.changeset/cuddly-avocados-warn.md b/.changeset/cuddly-avocados-warn.md new file mode 100644 index 0000000000..aa70da287b --- /dev/null +++ b/.changeset/cuddly-avocados-warn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Expand techdocs.publisher.type with `googleGcs`,`awsS3`,`azureBlobStorage` and `openStackSwift` diff --git a/.changeset/curvy-roses-march.md b/.changeset/curvy-roses-march.md new file mode 100644 index 0000000000..cd38808165 --- /dev/null +++ b/.changeset/curvy-roses-march.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Migrated the GitLab auth provider to be implemented using the new `@backstage/plugin-auth-backend-module-gitlab-provider` module. diff --git a/.changeset/cyan-roses-relate.md b/.changeset/cyan-roses-relate.md new file mode 100644 index 0000000000..e6dbeb6ed9 --- /dev/null +++ b/.changeset/cyan-roses-relate.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added the ability to create a plain backend module with the `new` command. diff --git a/.changeset/dirty-carrots-notice.md b/.changeset/dirty-carrots-notice.md new file mode 100644 index 0000000000..ccc31c25ca --- /dev/null +++ b/.changeset/dirty-carrots-notice.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added `authPlugin` export for the new backend system. The plugin does not include any built-in auth providers, they must instead be added by installing additional modules, for example `authModuleGoogleProvider` from `@backstage/plugin-auth-backend-module-google-provider`. diff --git a/.changeset/dirty-cars-poke.md b/.changeset/dirty-cars-poke.md new file mode 100644 index 0000000000..5412229f34 --- /dev/null +++ b/.changeset/dirty-cars-poke.md @@ -0,0 +1,34 @@ +--- +'@backstage/plugin-adr-backend': minor +'@backstage/plugin-airbrake-backend': minor +'@backstage/plugin-auth-backend': minor +'@backstage/plugin-azure-devops-backend': minor +'@backstage/plugin-badges-backend': minor +'@backstage/plugin-bazaar-backend': minor +'@backstage/plugin-catalog-backend-module-unprocessed': minor +'@backstage/plugin-devtools-backend': minor +'@backstage/plugin-entity-feedback-backend': minor +'@backstage/plugin-kafka-backend': minor +'@backstage/plugin-lighthouse-backend': minor +'@backstage/plugin-linguist-backend': minor +'@backstage/plugin-periskop-backend': minor +'@backstage/plugin-proxy-backend': minor +'@backstage/plugin-todo-backend': minor +'@backstage/plugin-user-settings-backend': minor +--- + +**BREAKING**: The export for the new backend system has been moved to be the `default` export. + +For example, if you are currently importing the plugin using the following pattern: + +```ts +import { examplePlugin } from '@backstage/plugin-example-backend'; + +backend.add(examplePlugin); +``` + +It should be migrated to this: + +```ts +backend.add(import('@backstage/plugin-example-backend')); +``` diff --git a/.changeset/angry-beers-relate.md b/.changeset/dirty-cars-smoke.md similarity index 66% rename from .changeset/angry-beers-relate.md rename to .changeset/dirty-cars-smoke.md index 4e51de99e4..5ba8380d68 100644 --- a/.changeset/angry-beers-relate.md +++ b/.changeset/dirty-cars-smoke.md @@ -1,43 +1,48 @@ --- -'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch -'@backstage/plugin-catalog-backend-module-bitbucket-server': patch +'@backstage/plugin-app-backend': 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-catalog-backend': patch +'@backstage/plugin-catalog-backend-module-aws': patch +'@backstage/plugin-catalog-backend-module-azure': patch '@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch -'@backstage/plugin-search-backend-module-elasticsearch': patch -'@backstage/plugin-catalog-backend-module-puppetdb': patch -'@backstage/plugin-catalog-backend-module-msgraph': patch -'@backstage/plugin-search-backend-module-techdocs': patch +'@backstage/plugin-catalog-backend-module-bitbucket-server': patch +'@backstage/plugin-catalog-backend-module-gcp': patch '@backstage/plugin-catalog-backend-module-gerrit': patch '@backstage/plugin-catalog-backend-module-github': patch '@backstage/plugin-catalog-backend-module-gitlab': patch -'@backstage/plugin-events-backend-module-aws-sqs': patch -'@backstage/plugin-search-backend-module-catalog': patch -'@backstage/plugin-search-backend-module-explore': patch -'@backstage/plugin-catalog-backend-module-azure': patch -'@backstage/plugin-events-backend-module-github': patch -'@backstage/plugin-events-backend-module-gitlab': patch -'@backstage/plugin-catalog-backend-module-aws': patch -'@backstage/plugin-catalog-backend-module-gcp': patch -'@backstage/plugin-search-backend-module-pg': patch -'@backstage/plugin-azure-devops-backend': patch -'@backstage/plugin-kubernetes-backend': patch -'@backstage/plugin-lighthouse-backend': patch -'@backstage/plugin-permission-backend': patch -'@backstage/plugin-scaffolder-backend': patch -'@backstage/backend-defaults': patch -'@backstage/plugin-airbrake-backend': patch -'@backstage/plugin-devtools-backend': patch -'@backstage/plugin-periskop-backend': patch -'@backstage/plugin-techdocs-backend': patch -'@backstage/backend-common': patch -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-badges-backend': patch -'@backstage/plugin-bazaar-backend': patch +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-catalog-backend-module-puppetdb': patch '@backstage/plugin-events-backend': patch +'@backstage/plugin-events-backend-module-aws-sqs': patch +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-permission-backend': patch +'@backstage/plugin-permission-backend-module-allow-all-policy': patch +'@backstage/plugin-scaffolder-backend': patch '@backstage/plugin-search-backend': patch -'@backstage/plugin-kafka-backend': patch -'@backstage/plugin-proxy-backend': patch -'@backstage/plugin-todo-backend': patch -'@backstage/plugin-app-backend': patch +'@backstage/plugin-search-backend-module-catalog': patch +'@backstage/plugin-search-backend-module-elasticsearch': patch +'@backstage/plugin-search-backend-module-explore': patch +'@backstage/plugin-search-backend-module-pg': patch +'@backstage/plugin-search-backend-module-techdocs': patch +'@backstage/plugin-techdocs-backend': patch --- -Use `coreServices.rootConfig` instead of `coreService.config` +The export for the new backend system has been moved to be the `default` export. + +For example, if you are currently importing the plugin using the following pattern: + +```ts +import { examplePlugin } from '@backstage/plugin-example-backend'; + +backend.add(examplePlugin); +``` + +It should be migrated to this: + +```ts +backend.add(import('@backstage/plugin-example-backend')); +``` diff --git a/.changeset/dirty-chefs-listen.md b/.changeset/dirty-chefs-listen.md deleted file mode 100644 index 84b684b977..0000000000 --- a/.changeset/dirty-chefs-listen.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/config-loader': minor ---- - -Loading invalid TypeScript configuration schemas will now throw an error rather than silently being ignored. - -In particular this includes defining any additional types other than `Config` in the schema file, or use of unsupported types such as `Record` or `Partial`. diff --git a/.changeset/dirty-garlics-kiss.md b/.changeset/dirty-garlics-kiss.md new file mode 100644 index 0000000000..98a9b569cc --- /dev/null +++ b/.changeset/dirty-garlics-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-radar': patch +--- + +Add names to blip entry links in the radar to improve accessibility diff --git a/.changeset/dirty-years-travel.md b/.changeset/dirty-years-travel.md new file mode 100644 index 0000000000..e6e5b8a608 --- /dev/null +++ b/.changeset/dirty-years-travel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-code-coverage-backend': patch +--- + +Added support for LCOV coverage reports diff --git a/.changeset/dry-wasps-occur.md b/.changeset/dry-wasps-occur.md new file mode 100644 index 0000000000..0c7d231708 --- /dev/null +++ b/.changeset/dry-wasps-occur.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-gcp-iap-provider': minor +--- + +New module for `@backstage/plugin-auth-backend` that adds a GCP IAP auth provider. diff --git a/.changeset/dull-tables-joke.md b/.changeset/dull-tables-joke.md new file mode 100644 index 0000000000..e987377023 --- /dev/null +++ b/.changeset/dull-tables-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Fixed an issue where `EntitySwitch` was preventing the display of entity errors. diff --git a/.changeset/early-rules-taste.md b/.changeset/early-rules-taste.md new file mode 100644 index 0000000000..1a332dfe36 --- /dev/null +++ b/.changeset/early-rules-taste.md @@ -0,0 +1,14 @@ +--- +'@backstage/create-app': patch +--- + +Added a `fix` scripts that calls the new `backstage-cli repo fix` command. + +To apply this change to an existing app, make the following change to your root `package.json`: + +```diff + "test": "backstage-cli repo test", + "test:all": "backstage-cli repo test --coverage", ++ "fix": "backstage-cli repo fix", + "lint": "backstage-cli repo lint --since origin/master", +``` diff --git a/.changeset/early-trains.draw.md b/.changeset/early-trains.draw.md new file mode 100644 index 0000000000..30aad1a08d --- /dev/null +++ b/.changeset/early-trains.draw.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-radar': patch +--- + +Fixed the width of the tech radar dialog so that the content fits diff --git a/.changeset/eight-radios-listen.md b/.changeset/eight-radios-listen.md new file mode 100644 index 0000000000..0421a561c0 --- /dev/null +++ b/.changeset/eight-radios-listen.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Fixed bug in styles that caused next and previous links in footer to overlap page content. diff --git a/.changeset/eight-timers-sin.md b/.changeset/eight-timers-sin.md new file mode 100644 index 0000000000..28d6259cb4 --- /dev/null +++ b/.changeset/eight-timers-sin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Export the `EntityAutocompletePicker` component. diff --git a/.changeset/eighty-emus-float.md b/.changeset/eighty-emus-float.md new file mode 100644 index 0000000000..c149affa09 --- /dev/null +++ b/.changeset/eighty-emus-float.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add ESM loader for the experimental backend start command. diff --git a/.changeset/eighty-fans-provide.md b/.changeset/eighty-fans-provide.md new file mode 100644 index 0000000000..b98bc39924 --- /dev/null +++ b/.changeset/eighty-fans-provide.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Fixed an issue causing `StructuredMetadataTable` to crash in case metadata contained `null` values. diff --git a/.changeset/empty-rings-type.md b/.changeset/empty-rings-type.md new file mode 100644 index 0000000000..2aa41054f9 --- /dev/null +++ b/.changeset/empty-rings-type.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Add examples for `publish:github` and `publish:gitlab` scaffolder actions. diff --git a/.changeset/fair-paws-flash.md b/.changeset/fair-paws-flash.md new file mode 100644 index 0000000000..d9a7b1827e --- /dev/null +++ b/.changeset/fair-paws-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-azure': patch +--- + +Improve consistency of results from the `AzureDevOpsEntityProvider`. diff --git a/.changeset/few-nails-smile.md b/.changeset/few-nails-smile.md new file mode 100644 index 0000000000..2fad9a0b40 --- /dev/null +++ b/.changeset/few-nails-smile.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-entity-feedback': patch +--- + +Improve README to note that Backstage identity is required to be configured diff --git a/.changeset/fifty-adults-shake.md b/.changeset/fifty-adults-shake.md new file mode 100644 index 0000000000..a21c5805ab --- /dev/null +++ b/.changeset/fifty-adults-shake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Allow configuring the processing interval in your app-config, under the `catalog.processingInterval` key. diff --git a/.changeset/fluffy-pens-prove.md b/.changeset/fluffy-pens-prove.md deleted file mode 100644 index 0bd7d9baf3..0000000000 --- a/.changeset/fluffy-pens-prove.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Updated `frameHandler` to return `undefined` when using the redirect flow instead of returning `postMessageReponse` which was causing errors diff --git a/.changeset/forty-rocks-jog.md b/.changeset/forty-rocks-jog.md new file mode 100644 index 0000000000..aff3d34a4a --- /dev/null +++ b/.changeset/forty-rocks-jog.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes': patch +'@backstage/test-utils': patch +--- + +Add pod exec terminal to Container Card diff --git a/.changeset/four-masks-fry.md b/.changeset/four-masks-fry.md new file mode 100644 index 0000000000..94bd929016 --- /dev/null +++ b/.changeset/four-masks-fry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Update OpenAPI schema to relax the encoding validation of all request parameters. diff --git a/.changeset/fresh-candles-warn.md b/.changeset/fresh-candles-warn.md new file mode 100644 index 0000000000..49ca2e5402 --- /dev/null +++ b/.changeset/fresh-candles-warn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-node': patch +--- + +The `BackstageIdentityResponse` interface now has an optional `expiresInSeconds` field that can be used to signal session expiration. The `prepareBackstageIdentityResponse` utility will now also read the expiration from the provided token, and include it in the response. diff --git a/.changeset/friendly-pugs-lick.md b/.changeset/friendly-pugs-lick.md new file mode 100644 index 0000000000..3a82497da8 --- /dev/null +++ b/.changeset/friendly-pugs-lick.md @@ -0,0 +1,5 @@ +--- +'@backstage/config': minor +--- + +Added a `readDurationFromConfig` function diff --git a/.changeset/funny-ligers-matter.md b/.changeset/funny-ligers-matter.md new file mode 100644 index 0000000000..0cec3eb333 --- /dev/null +++ b/.changeset/funny-ligers-matter.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-app-api': patch +--- + +Adds include and exclude configuration to feature discovery of backend packages +Adds alpha modules to feature discovery diff --git a/.changeset/funny-timers-retire.md b/.changeset/funny-timers-retire.md new file mode 100644 index 0000000000..87cd4d8f0d --- /dev/null +++ b/.changeset/funny-timers-retire.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added a new `repo fix` command that fixes auto-fixable problems in all packages. Initially the command fixes package export declarations, as well as marks all non-bundled frontend packages as side-effect free. Marking packages as free of side-effects can drastically reduce the Webpack bundle size. diff --git a/.changeset/giant-coats-brush.md b/.changeset/giant-coats-brush.md new file mode 100644 index 0000000000..e4fe391b9e --- /dev/null +++ b/.changeset/giant-coats-brush.md @@ -0,0 +1,129 @@ +--- +'@backstage/app-defaults': patch +'@backstage/catalog-client': patch +'@backstage/catalog-model': patch +'@backstage/config': patch +'@backstage/core-app-api': patch +'@backstage/core-components': patch +'@backstage/core-plugin-api': patch +'@backstage/dev-utils': patch +'@backstage/errors': patch +'@backstage/integration': patch +'@backstage/integration-react': patch +'@backstage/plugin-adr': patch +'@backstage/plugin-adr-common': patch +'@backstage/plugin-airbrake': patch +'@backstage/plugin-allure': patch +'@backstage/plugin-analytics-module-ga': patch +'@backstage/plugin-analytics-module-ga4': patch +'@backstage/plugin-analytics-module-newrelic-browser': patch +'@backstage/plugin-apache-airflow': patch +'@backstage/plugin-api-docs': patch +'@backstage/plugin-api-docs-module-protoc-gen-doc': patch +'@backstage/plugin-apollo-explorer': patch +'@backstage/plugin-azure-devops': patch +'@backstage/plugin-azure-devops-common': patch +'@backstage/plugin-azure-sites': patch +'@backstage/plugin-azure-sites-common': patch +'@backstage/plugin-badges': patch +'@backstage/plugin-bazaar': patch +'@backstage/plugin-bitbucket-cloud-common': patch +'@backstage/plugin-bitrise': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-common': patch +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-catalog-unprocessed-entities': patch +'@backstage/plugin-cicd-statistics': patch +'@backstage/plugin-cicd-statistics-module-gitlab': patch +'@backstage/plugin-circleci': patch +'@backstage/plugin-cloudbuild': patch +'@backstage/plugin-code-climate': patch +'@backstage/plugin-code-coverage': patch +'@backstage/plugin-codescene': patch +'@backstage/plugin-config-schema': patch +'@backstage/plugin-cost-insights': patch +'@backstage/plugin-cost-insights-common': patch +'@backstage/plugin-devtools': patch +'@backstage/plugin-devtools-common': patch +'@backstage/plugin-dynatrace': patch +'@backstage/plugin-entity-feedback': patch +'@backstage/plugin-entity-feedback-common': patch +'@backstage/plugin-entity-validation': patch +'@backstage/plugin-explore': patch +'@backstage/plugin-explore-common': patch +'@backstage/plugin-explore-react': patch +'@backstage/plugin-firehydrant': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-gcalendar': patch +'@backstage/plugin-gcp-projects': patch +'@backstage/plugin-git-release-manager': patch +'@backstage/plugin-github-actions': patch +'@backstage/plugin-github-deployments': patch +'@backstage/plugin-github-issues': patch +'@backstage/plugin-github-pull-requests-board': patch +'@backstage/plugin-gitops-profiles': patch +'@backstage/plugin-gocd': patch +'@backstage/plugin-graphiql': patch +'@backstage/plugin-graphql-voyager': patch +'@backstage/plugin-home': patch +'@backstage/plugin-home-react': patch +'@backstage/plugin-ilert': patch +'@backstage/plugin-jenkins': patch +'@backstage/plugin-jenkins-common': patch +'@backstage/plugin-kafka': patch +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-kubernetes-common': patch +'@backstage/plugin-lighthouse': patch +'@backstage/plugin-lighthouse-common': patch +'@backstage/plugin-linguist': patch +'@backstage/plugin-linguist-common': patch +'@backstage/plugin-microsoft-calendar': patch +'@backstage/plugin-newrelic': patch +'@backstage/plugin-newrelic-dashboard': patch +'@backstage/plugin-nomad': patch +'@backstage/plugin-octopus-deploy': patch +'@backstage/plugin-opencost': patch +'@backstage/plugin-org': patch +'@backstage/plugin-org-react': patch +'@backstage/plugin-pagerduty': patch +'@backstage/plugin-periskop': patch +'@backstage/plugin-permission-common': patch +'@backstage/plugin-permission-react': patch +'@backstage/plugin-playlist': patch +'@backstage/plugin-playlist-common': patch +'@backstage/plugin-puppetdb': patch +'@backstage/plugin-rollbar': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-common': patch +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-search': patch +'@backstage/plugin-search-common': patch +'@backstage/plugin-search-react': patch +'@backstage/plugin-sentry': patch +'@backstage/plugin-shortcuts': patch +'@backstage/plugin-sonarqube': patch +'@backstage/plugin-sonarqube-react': patch +'@backstage/plugin-splunk-on-call': patch +'@backstage/plugin-stack-overflow': patch +'@backstage/plugin-stackstorm': patch +'@backstage/plugin-tech-insights': patch +'@backstage/plugin-tech-insights-common': patch +'@backstage/plugin-tech-radar': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-techdocs-addons-test-utils': patch +'@backstage/plugin-techdocs-module-addons-contrib': patch +'@backstage/plugin-techdocs-react': patch +'@backstage/plugin-todo': patch +'@backstage/plugin-user-settings': patch +'@backstage/plugin-vault': patch +'@backstage/plugin-xcmetrics': patch +'@backstage/release-manifests': patch +'@backstage/test-utils': patch +'@backstage/theme': patch +'@backstage/types': patch +'@backstage/version-bridge': patch +--- + +Mark package as being free of side effects, allowing more optimized Webpack builds. diff --git a/.changeset/gorgeous-coins-attack.md b/.changeset/gorgeous-coins-attack.md new file mode 100644 index 0000000000..d500459225 --- /dev/null +++ b/.changeset/gorgeous-coins-attack.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Added delete alert popup when user delete the entity diff --git a/.changeset/gorgeous-months-doubt.md b/.changeset/gorgeous-months-doubt.md deleted file mode 100644 index d440e28b72..0000000000 --- a/.changeset/gorgeous-months-doubt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-openapi-utils': patch ---- - -Add a new `createRouter` method for generating an `express` router that validates against your spec. Also fixes a bug with the query parameters type resolution. diff --git a/.changeset/great-chairs-swim.md b/.changeset/great-chairs-swim.md new file mode 100644 index 0000000000..e6b8ab0882 --- /dev/null +++ b/.changeset/great-chairs-swim.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': minor +--- + +Fixed two bugs in how the `OAuth2Session` type represents the underlying data. The `expiresAt` and `backstageIdentity` are now both optional, since that's what they are in practice. This is not considered a breaking change since it was effectively a bug in the modelling of the state that this type represents, and the type was not used in any other external contract. diff --git a/.changeset/grumpy-papayas-tie.md b/.changeset/grumpy-papayas-tie.md new file mode 100644 index 0000000000..48580673d8 --- /dev/null +++ b/.changeset/grumpy-papayas-tie.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Additional fix for Gitiles auth links diff --git a/.changeset/grumpy-ties-know.md b/.changeset/grumpy-ties-know.md new file mode 100644 index 0000000000..86e3b0ec7d --- /dev/null +++ b/.changeset/grumpy-ties-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Add a notification when `yarn install` is taking a long time. diff --git a/.changeset/healthy-spiders-arrive.md b/.changeset/healthy-spiders-arrive.md new file mode 100644 index 0000000000..e8d110ccaa --- /dev/null +++ b/.changeset/healthy-spiders-arrive.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Ensure direct relations are shown for User entities while keeping support for aggregating closest parent group ownership diff --git a/.changeset/healthy-tools-count.md b/.changeset/healthy-tools-count.md new file mode 100644 index 0000000000..5d3f04159f --- /dev/null +++ b/.changeset/healthy-tools-count.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Deprecated several exports that are now available from `@backstage/plugin-auth-node` instead. diff --git a/.changeset/hip-seahorses-exist.md b/.changeset/hip-seahorses-exist.md new file mode 100644 index 0000000000..e8e247cc0a --- /dev/null +++ b/.changeset/hip-seahorses-exist.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +Align zod dependency to align with other packages. diff --git a/.changeset/honest-ads-rush.md b/.changeset/honest-ads-rush.md new file mode 100644 index 0000000000..63510f7c1a --- /dev/null +++ b/.changeset/honest-ads-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Add examples for `publish:bitbucket` scaffolder actions. diff --git a/.changeset/hot-cats-rush.md b/.changeset/hot-cats-rush.md deleted file mode 100644 index 2963804ff2..0000000000 --- a/.changeset/hot-cats-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-explore-backend': patch ---- - -Moved the config example from the "Tools as Code" section to the "Tools as Config" section of the README diff --git a/.changeset/hungry-cherries-speak.md b/.changeset/hungry-cherries-speak.md new file mode 100644 index 0000000000..bd67f61446 --- /dev/null +++ b/.changeset/hungry-cherries-speak.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Add support for installing backend features via module imports, for example `startTestBackend({ features: [import('my-plugin')] })`. diff --git a/.changeset/hungry-dots-design.md b/.changeset/hungry-dots-design.md new file mode 100644 index 0000000000..0dd8d415e9 --- /dev/null +++ b/.changeset/hungry-dots-design.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-backend-module-azure': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/backend-common': patch +--- + +Use `DefaultAzureDevOpsCredentialsProvider` to retrieve credentials for Azure DevOps. diff --git a/.changeset/hungry-shrimps-care.md b/.changeset/hungry-shrimps-care.md deleted file mode 100644 index 665a7bfc82..0000000000 --- a/.changeset/hungry-shrimps-care.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch -'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch -'@backstage/plugin-events-backend-module-bitbucket-cloud': patch -'@backstage/plugin-search-backend-module-elasticsearch': patch -'@backstage/plugin-catalog-backend-module-unprocessed': patch -'@backstage/plugin-scaffolder-backend-module-sentry': patch -'@backstage/plugin-scaffolder-backend-module-yeoman': patch -'@backstage/plugin-scaffolder-backend-module-rails': patch -'@backstage/plugin-search-backend-module-techdocs': patch -'@backstage/plugin-cicd-statistics-module-gitlab': patch -'@backstage/plugin-events-backend-module-aws-sqs': patch -'@backstage/plugin-search-backend-module-catalog': patch -'@backstage/plugin-search-backend-module-explore': patch -'@backstage/plugin-events-backend-module-gerrit': patch -'@backstage/plugin-events-backend-module-github': patch -'@backstage/plugin-events-backend-module-gitlab': patch -'@backstage/plugin-events-backend-module-azure': patch -'@backstage/plugin-events-backend-test-utils': patch -'@backstage/plugin-search-backend-module-pg': patch -'@backstage/plugin-entity-feedback-backend': patch -'@backstage/backend-openapi-utils': patch -'@backstage/plugin-bitbucket-cloud-common': patch -'@backstage/plugin-entity-feedback-common': patch -'@backstage/plugin-code-coverage-backend': patch -'@backstage/plugin-user-settings-backend': patch -'@backstage/plugin-analytics-module-ga4': patch -'@backstage/plugin-azure-devops-backend': patch -'@backstage/plugin-analytics-module-ga': patch -'@backstage/plugin-git-release-manager': patch -'@backstage/plugin-search-backend-node': patch -'@backstage/integration-react': patch -'@backstage/plugin-github-deployments': patch -'@backstage/plugin-microsoft-calendar': patch -'@backstage/plugin-newrelic-dashboard': patch -'@backstage/plugin-permission-backend': patch -'@backstage/plugin-entity-validation': patch -'@backstage/plugin-sonarqube-backend': patch -'@backstage/plugin-airbrake-backend': patch -'@backstage/plugin-devtools-backend': patch -'@backstage/plugin-linguist-backend': patch -'@backstage/plugin-periskop-backend': patch -'@backstage/plugin-playlist-backend': patch -'@backstage/plugin-apollo-explorer': patch -'@backstage/plugin-devtools-common': patch -'@backstage/plugin-entity-feedback': patch -'@backstage/plugin-explore-backend': patch -'@backstage/plugin-graphql-voyager': patch -'@backstage/plugin-jenkins-backend': patch -'@backstage/plugin-linguist-common': patch -'@backstage/plugin-playlist-common': patch -'@backstage/plugin-scaffolder-node': patch -'@backstage/plugin-apache-airflow': patch -'@backstage/plugin-bazaar-backend': patch -'@backstage/plugin-events-backend': patch -'@backstage/plugin-jenkins-common': patch -'@backstage/plugin-octopus-deploy': patch -'@backstage/plugin-search-backend': patch -'@backstage/plugin-stack-overflow': patch -'@backstage/plugin-catalog-graph': patch -'@backstage/plugin-code-coverage': patch -'@backstage/plugin-config-schema': patch -'@backstage/plugin-github-issues': patch -'@backstage/plugin-nomad-backend': patch -'@backstage/plugin-tech-insights': patch -'@backstage/plugin-catalog-node': patch -'@backstage/plugin-code-climate': patch -'@backstage/plugin-events-node': patch -'@backstage/plugin-firehydrant': patch -'@backstage/cli-node': patch -'@backstage/plugin-auth-node': patch -'@backstage/plugin-codescene': patch -'@backstage/plugin-dynatrace': patch -'@backstage/plugin-gcalendar': patch -'@backstage/plugin-org-react': patch -'@backstage/plugin-shortcuts': patch -'@backstage/plugin-xcmetrics': patch -'@backstage/plugin-airbrake': patch -'@backstage/plugin-periskop': patch -'@backstage/plugin-playlist': patch -'@backstage/plugin-bitrise': patch -'@backstage/plugin-allure': patch -'@backstage/plugin-bazaar': patch -'@backstage/plugin-ilert': patch -'@backstage/plugin-kafka': patch -'@backstage/plugin-nomad': patch -'@backstage/plugin-adr': patch -'@backstage/plugin-org': patch ---- - -Add package repository and homepage metadata diff --git a/.changeset/khaki-flies-draw.md b/.changeset/khaki-flies-draw.md deleted file mode 100644 index ada45dea06..0000000000 --- a/.changeset/khaki-flies-draw.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-gitlab': patch ---- - -Added option to skip forked repos in GitlabDiscoveryEntityProvider diff --git a/.changeset/khaki-pigs-accept.md b/.changeset/khaki-pigs-accept.md new file mode 100644 index 0000000000..e00db1746f --- /dev/null +++ b/.changeset/khaki-pigs-accept.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-devtools-backend': patch +'@backstage/plugin-devtools-common': patch +'@backstage/plugin-devtools': patch +--- + +Show resource utilization in `DevTools` plugin diff --git a/.changeset/khaki-spiders-sniff.md b/.changeset/khaki-spiders-sniff.md new file mode 100644 index 0000000000..53798a6cf0 --- /dev/null +++ b/.changeset/khaki-spiders-sniff.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': patch +--- + +conditionally rendering the user email in user profile card diff --git a/.changeset/kind-cougars-allow.md b/.changeset/kind-cougars-allow.md deleted file mode 100644 index d37c4c5cf2..0000000000 --- a/.changeset/kind-cougars-allow.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/plugin-devtools-backend': patch ---- - -Add DevTools configuration to enable dependency listing to be filtered with custom prefixes. For instance, in your `app-config.yaml`: - -```yaml -devTools: - info: - packagePrefixes: - - @backstage/ - - @roadiehq/backstage- - - @spotify/backstage- -``` diff --git a/.changeset/large-badgers-switch.md b/.changeset/large-badgers-switch.md deleted file mode 100644 index 47c06a4afe..0000000000 --- a/.changeset/large-badgers-switch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-linguist-common': patch ---- - -Exported new LanguageType type alias diff --git a/.changeset/large-experts-poke.md b/.changeset/large-experts-poke.md deleted file mode 100644 index 9f6ca640b6..0000000000 --- a/.changeset/large-experts-poke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-linguist-backend': patch ---- - -Fixed bug in LinguistBackendClient.ts file where if the linguistJsOptions is specified and sent over to the linguist-js package it would get changed (another attribute would be added) causing future entities of the batch to fail with an error diff --git a/.changeset/large-vans-cross.md b/.changeset/large-vans-cross.md deleted file mode 100644 index f50b432742..0000000000 --- a/.changeset/large-vans-cross.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-sonarqube': patch ---- - -Remove sonarQube card disable class diff --git a/.changeset/late-cows-turn.md b/.changeset/late-cows-turn.md new file mode 100644 index 0000000000..7fa775ce22 --- /dev/null +++ b/.changeset/late-cows-turn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed a link to the frontend Backstage plugin that had pointed to itself. diff --git a/.changeset/late-hounds-yell.md b/.changeset/late-hounds-yell.md new file mode 100644 index 0000000000..903b161c35 --- /dev/null +++ b/.changeset/late-hounds-yell.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Updated Dockerfile to include `apt-get update` when installing the SQLite dependency diff --git a/.changeset/late-roses-dream.md b/.changeset/late-roses-dream.md new file mode 100644 index 0000000000..b0c2316c95 --- /dev/null +++ b/.changeset/late-roses-dream.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-code-coverage-backend': patch +--- + +Add text body parser for LCOV reports diff --git a/.changeset/lazy-pugs-wash.md b/.changeset/lazy-pugs-wash.md deleted file mode 100644 index 923ef5b116..0000000000 --- a/.changeset/lazy-pugs-wash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-app-node': minor ---- - -Added the `app` plugin node library, initially providing an extension point that can be used to configure a static fallback handler. diff --git a/.changeset/lazy-pumas-hope.md b/.changeset/lazy-pumas-hope.md new file mode 100644 index 0000000000..5e06014a3c --- /dev/null +++ b/.changeset/lazy-pumas-hope.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': patch +--- + +changed auto theme tooltip title to camel case diff --git a/.changeset/lazy-pumas-shop.md b/.changeset/lazy-pumas-shop.md new file mode 100644 index 0000000000..13524aa741 --- /dev/null +++ b/.changeset/lazy-pumas-shop.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-permission-backend': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-permission-node': patch +--- + +Minor `package.json` update. diff --git a/.changeset/lazy-rice-rule.md b/.changeset/lazy-rice-rule.md new file mode 100644 index 0000000000..1c3a3c3250 --- /dev/null +++ b/.changeset/lazy-rice-rule.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fixed the plugin and module ID of the alpha `catalogModuleTemplateKind` export. diff --git a/.changeset/lemon-donuts-clap.md b/.changeset/lemon-donuts-clap.md new file mode 100644 index 0000000000..75408d2584 --- /dev/null +++ b/.changeset/lemon-donuts-clap.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Ensure that root scoped services cannot accept (at a type level) plugin scoped deps diff --git a/.changeset/light-drinks-rush.md b/.changeset/light-drinks-rush.md new file mode 100644 index 0000000000..08383ab089 --- /dev/null +++ b/.changeset/light-drinks-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add experimental support for frontend package discovery. diff --git a/.changeset/little-penguins-build.md b/.changeset/little-penguins-build.md deleted file mode 100644 index 9e54a566e2..0000000000 --- a/.changeset/little-penguins-build.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': minor ---- - -Support configuring applicable kinds for `AnnotateScmSlugEntityProcessor` diff --git a/.changeset/long-flowers-sort.md b/.changeset/long-flowers-sort.md new file mode 100644 index 0000000000..2f25260030 --- /dev/null +++ b/.changeset/long-flowers-sort.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Entity relations toggle should by default be aggregated for User entities diff --git a/.changeset/loud-apricots-destroy.md b/.changeset/loud-apricots-destroy.md new file mode 100644 index 0000000000..e60be6dccf --- /dev/null +++ b/.changeset/loud-apricots-destroy.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-radar': patch +--- + +Add description property for Rings which will be shown in the footer. diff --git a/.changeset/loud-garlics-press.md b/.changeset/loud-garlics-press.md deleted file mode 100644 index 28aee20619..0000000000 --- a/.changeset/loud-garlics-press.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Reload the frontend when app config changes diff --git a/.changeset/lovely-years-sniff.md b/.changeset/lovely-years-sniff.md new file mode 100644 index 0000000000..09b86f7112 --- /dev/null +++ b/.changeset/lovely-years-sniff.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': minor +--- + +The `OAuth` class which is used by all OAuth providers will now consider both the session expiration of both the Backstage identity as well as the upstream identity provider, and refresh the session with either of them is about to expire. diff --git a/.changeset/lucky-chicken-hear.md b/.changeset/lucky-chicken-hear.md new file mode 100644 index 0000000000..b7f0d6ec48 --- /dev/null +++ b/.changeset/lucky-chicken-hear.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-code-coverage-backend': patch +--- + +Replace `express-xml-bodyparser` with `body-parser-xml`. + +`express-xml-bodyparser` was last updated 8 years ago +and currently depends on a version of `xml2js` which +contains a vulnerability. + +This change will swap it out in favor of `body-parser-xml` +which is more maintained and depends on a more recent `xml2js` +version without the vulnerability. diff --git a/.changeset/lucky-chicken-lie.md b/.changeset/lucky-chicken-lie.md new file mode 100644 index 0000000000..6edb3a36f6 --- /dev/null +++ b/.changeset/lucky-chicken-lie.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-gitlab-provider': minor +--- + +New module for `@backstage/plugin-auth-backend` that adds a GitLab auth provider. diff --git a/.changeset/many-tips-attend.md b/.changeset/many-tips-attend.md new file mode 100644 index 0000000000..34b5898803 --- /dev/null +++ b/.changeset/many-tips-attend.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-react': patch +--- + +Optionally initializes the search context with default settings for search queries only when the config is defined, rather than always overriding it. diff --git a/.changeset/mean-carpets-provide.md b/.changeset/mean-carpets-provide.md new file mode 100644 index 0000000000..b5e9f551d4 --- /dev/null +++ b/.changeset/mean-carpets-provide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-azure': patch +--- + +Remove duplications from Azure search before committing the new locations to the catalog. diff --git a/.changeset/mean-squids-relax.md b/.changeset/mean-squids-relax.md deleted file mode 100644 index 558b42fcfc..0000000000 --- a/.changeset/mean-squids-relax.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-home-react': patch ---- - -Make `title` optional when defining the `createCardExtension` diff --git a/.changeset/metal-buttons-search.md b/.changeset/metal-buttons-search.md deleted file mode 100644 index 9578d8d2d4..0000000000 --- a/.changeset/metal-buttons-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Adds examples to a few scaffolder actions. diff --git a/.changeset/mighty-crabs-cheat.md b/.changeset/mighty-crabs-cheat.md new file mode 100644 index 0000000000..ef2abbada4 --- /dev/null +++ b/.changeset/mighty-crabs-cheat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added the ability to disable the built-in auth providers by passing `disableDefaultProviderFactories` to `createRouter`. diff --git a/.changeset/mighty-lions-search-2.md b/.changeset/mighty-lions-search-2.md deleted file mode 100644 index a7fb1a4a7d..0000000000 --- a/.changeset/mighty-lions-search-2.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-test-utils': minor ---- - -**BREAKING**: Renamed `mockServices.config` to `mockServices.rootConfig`. diff --git a/.changeset/mighty-lions-search-3.md b/.changeset/mighty-lions-search-3.md deleted file mode 100644 index 6b381ce2b2..0000000000 --- a/.changeset/mighty-lions-search-3.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-app-api': minor ---- - -**BREAKING**: Renamed `configServiceFactory` to `rootConfigServiceFactory`. diff --git a/.changeset/mighty-lions-search.md b/.changeset/mighty-lions-search.md deleted file mode 100644 index 6d2ffd02c0..0000000000 --- a/.changeset/mighty-lions-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-plugin-api': minor ---- - -**BREAKING**: Renamed `coreServices.config` to `coreServices.rootConfig`. diff --git a/.changeset/modern-trees-add.md b/.changeset/modern-trees-add.md new file mode 100644 index 0000000000..bb5ea78a5b --- /dev/null +++ b/.changeset/modern-trees-add.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs': minor +'@backstage/plugin-catalog': minor +--- + +This change allows a new annotation of `backstage.io/techdocs-entity` this ref allows you to reference another entity for its TechDocs. This allows you have a single TechDoc for all items in a system, for example you might have a frontend and a backend in the same repo. This would allow you to have TechDocs build under a `System` entity while referencing the system e.g.: `backstage.io/techdocs-entity: system:default/example` that will show the systems docs in both the TechDocs button and the TechDocs tab without needing to do duplicate builds and filling the TechDocs page with garbage. diff --git a/.changeset/nasty-cherries-exercise.md b/.changeset/nasty-cherries-exercise.md new file mode 100644 index 0000000000..bb4386e978 --- /dev/null +++ b/.changeset/nasty-cherries-exercise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +fix "undefined" kind for custom resources diff --git a/.changeset/nasty-forks-beg.md b/.changeset/nasty-forks-beg.md new file mode 100644 index 0000000000..0662431453 --- /dev/null +++ b/.changeset/nasty-forks-beg.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-search-react': minor +'@backstage/plugin-search': minor +--- + +The SearchPage component can now be configured via app-config.yaml with default query parameters to define how it behaves when it is first loaded or reset. Check out the following example: + +```yaml +search: + query: + pageLimit: 50 +``` + +Acceptable values for `pageLimit` are `10`, `25`, `50` or `100`. diff --git a/.changeset/nasty-snails-complain.md b/.changeset/nasty-snails-complain.md new file mode 100644 index 0000000000..d7d2cac319 --- /dev/null +++ b/.changeset/nasty-snails-complain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes': patch +--- + +fix logs dialog min height diff --git a/.changeset/nasty-worms-share.md b/.changeset/nasty-worms-share.md new file mode 100644 index 0000000000..4be8315cce --- /dev/null +++ b/.changeset/nasty-worms-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-playlist': patch +--- + +Fix a bug that led to errors being thrown in guest mode diff --git a/.changeset/neat-coins-raise.md b/.changeset/neat-coins-raise.md deleted file mode 100644 index c3045634bf..0000000000 --- a/.changeset/neat-coins-raise.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-bitbucket-server': patch -'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch -'@backstage/plugin-catalog-backend-module-puppetdb': patch -'@backstage/plugin-catalog-backend-module-msgraph': patch -'@backstage/plugin-catalog-backend-module-gerrit': patch -'@backstage/plugin-catalog-backend-module-github': patch -'@backstage/plugin-catalog-backend-module-gitlab': patch -'@backstage/plugin-events-backend-module-aws-sqs': patch -'@backstage/plugin-catalog-backend-module-azure': patch -'@backstage/plugin-catalog-backend-module-aws': patch -'@backstage/backend-common': patch -'@backstage/plugin-jenkins-backend': patch -'@backstage/plugin-proxy-backend': patch ---- - -Fixed invalid configuration schema. The configuration schema may be more strict as a result. diff --git a/.changeset/neat-lamps-press.md b/.changeset/neat-lamps-press.md deleted file mode 100644 index 23c0b15fb7..0000000000 --- a/.changeset/neat-lamps-press.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/core-components': patch -'@backstage/plugin-splunk-on-call': patch -'@backstage/plugin-xcmetrics': patch -'@backstage/plugin-catalog': patch ---- - -Prefer simple `theme.spacing` without string interpolation diff --git a/.changeset/new-sheep-cover.md b/.changeset/new-sheep-cover.md new file mode 100644 index 0000000000..390de1a9b3 --- /dev/null +++ b/.changeset/new-sheep-cover.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-linguist-backend': patch +--- + +When creating the router using `createRouterFromConfig` or using the plugin for the new backend system the `linguist.useSourceLocation` configuration is now optional. diff --git a/.changeset/nice-forks-remain.md b/.changeset/nice-forks-remain.md new file mode 100644 index 0000000000..6d833ff283 --- /dev/null +++ b/.changeset/nice-forks-remain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-code-coverage': patch +--- + +Use fetchApi to ensure authorization is used when fetching code-coverage data diff --git a/.changeset/ninety-horses-remember.md b/.changeset/ninety-horses-remember.md new file mode 100644 index 0000000000..22ef53b340 --- /dev/null +++ b/.changeset/ninety-horses-remember.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +The experimental backend feature discovery now only considers default exports from packages. It no longer filters packages to include based on the package role, except that `'cli'` packages are ignored. However, the `"backstage"` field is still required in `package.json`. diff --git a/.changeset/ninety-news-visit.md b/.changeset/ninety-news-visit.md new file mode 100644 index 0000000000..1b990f9ac7 --- /dev/null +++ b/.changeset/ninety-news-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +The algorithm used when generating Backstage tokens can be configured via `auth.identityTokenAlgorithm`. diff --git a/.changeset/odd-avocados-buy.md b/.changeset/odd-avocados-buy.md deleted file mode 100644 index 9e71c19fc0..0000000000 --- a/.changeset/odd-avocados-buy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch ---- - -Use `fetchContents` directly instead of a `fetchPlainAction` diff --git a/.changeset/old-geese-drive.md b/.changeset/old-geese-drive.md new file mode 100644 index 0000000000..ddaa7ad30a --- /dev/null +++ b/.changeset/old-geese-drive.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Extended `mockService` to also include mocked variants, for example `mockServices.lifecycle.mock()`. The returned mocked implementation will have a `factory` property which is a service factory for itself. You can also pass a partial implementation of the service to the mock function to use a mock implementation of specific methods. diff --git a/.changeset/orange-houses-fly.md b/.changeset/orange-houses-fly.md new file mode 100644 index 0000000000..546484304c --- /dev/null +++ b/.changeset/orange-houses-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Gitiles: Fixed auth prefix issue diff --git a/.changeset/pink-cups-rescue.md b/.changeset/pink-cups-rescue.md new file mode 100644 index 0000000000..abb69e88b7 --- /dev/null +++ b/.changeset/pink-cups-rescue.md @@ -0,0 +1,28 @@ +--- +'@backstage/plugin-auth-node': minor +--- + +Introduced a new system for building auth providers for `@backstage/plugin-auth-backend`, which both increases the amount of code re-use across providers, and also works better with the new backend system. + +Many existing types have been moved from `@backstage/plugin-auth-backend` in order to avoid a direct dependency on the plugin from modules. + +Auth provider integrations are now primarily implemented through a pattern of creating "authenticators", which are in turn specific to each kind of integrations. Initially there are two types: `createOAuthAuthenticator` and `createProxyAuthenticator`. These come paired with functions that let you create the corresponding route handlers, `createOAuthRouteHandlers` and `createProxyAuthRouteHandlers`, as well as provider factories, `createOAuthProviderFactory` and `createProxyAuthProviderFactory`. This new authenticator pattern allows the sign-in logic to be separated from the auth integration logic, allowing it to be completely re-used across all providers of the same kind. + +The new provider factories also implement a new declarative way to configure sign-in resolvers, rather than configuration through code. Sign-in resolvers can now be configured through the `resolvers` configuration key, where the first resolver that provides an identity will be used, for example: + +```yaml +auth: + providers: + google: + development: + clientId: ... + clientSecret: ... + signIn: + resolvers: + - resolver: emailMatchingUserEntityAnnotation + - resolver: emailLocalPartMatchingUserEntityName +``` + +These configurable resolvers are created with a new `createSignInResolverFactory` function, which creates a sign-in resolver factory, optionally with an options schema that will be used both when configuring the sign-in resolver through configuration and code. + +The internal helpers from `@backstage/plugin-auth-backend` that were used to implement auth providers using passport strategies have now also been made available as public API, through `PassportHelpers` and `PassportOAuthAuthenticatorHelper`. diff --git a/.changeset/pink-months-hammer.md b/.changeset/pink-months-hammer.md new file mode 100644 index 0000000000..20a484a389 --- /dev/null +++ b/.changeset/pink-months-hammer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-backend': patch +--- + +Moved `permissionModuleAllowAllPolicy` to `@backstage/plugin-permission-backend-module-allow-all-policy` diff --git a/.changeset/pink-squids-nail.md b/.changeset/pink-squids-nail.md deleted file mode 100644 index 989b5034d5..0000000000 --- a/.changeset/pink-squids-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-unprocessed': minor ---- - -**BREAKING**: Fixing typo in exported module. You will have to rename the import to the correct spelling. `UnprocessedEntites` -> `UnprocessedEntities` diff --git a/.changeset/polite-forks-grin.md b/.changeset/polite-forks-grin.md new file mode 100644 index 0000000000..8f6570ccbc --- /dev/null +++ b/.changeset/polite-forks-grin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed validation of the `fullTextFilterFields` query parameter. diff --git a/.changeset/polite-plums-share.md b/.changeset/polite-plums-share.md new file mode 100644 index 0000000000..09c980b921 --- /dev/null +++ b/.changeset/polite-plums-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Added some examples to the catalog OpenAPI definition diff --git a/.changeset/poor-olives-own.md b/.changeset/poor-olives-own.md new file mode 100644 index 0000000000..6eb8b5ac2b --- /dev/null +++ b/.changeset/poor-olives-own.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixes an issue where `order` was not a recognized parameter for the `/entities` endpoint. diff --git a/.changeset/poor-years-appear.md b/.changeset/poor-years-appear.md new file mode 100644 index 0000000000..d5b53a7051 --- /dev/null +++ b/.changeset/poor-years-appear.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-entity-feedback-backend': patch +--- + +Improve backend logging if method calls fail diff --git a/.changeset/popular-clocks-whisper.md b/.changeset/popular-clocks-whisper.md new file mode 100644 index 0000000000..7c1d86ab03 --- /dev/null +++ b/.changeset/popular-clocks-whisper.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fixed a bug where package exports entry points could not be `.tsx` files. diff --git a/.changeset/popular-coins-tap.md b/.changeset/popular-coins-tap.md new file mode 100644 index 0000000000..3ece9e20b1 --- /dev/null +++ b/.changeset/popular-coins-tap.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-tasks': patch +--- + +Use `readDurationFromConfig` from the config package diff --git a/.changeset/popular-fans-camp.md b/.changeset/popular-fans-camp.md deleted file mode 100644 index 47fc095ef8..0000000000 --- a/.changeset/popular-fans-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Updated config schema to match what was being used in code diff --git a/.changeset/popular-readers-allow.md b/.changeset/popular-readers-allow.md new file mode 100644 index 0000000000..b89e0c264e --- /dev/null +++ b/.changeset/popular-readers-allow.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': minor +--- + +Added the optional `expiresAt` field that may now be part of a `BackstageIdentityResponse`. diff --git a/.changeset/pre.json b/.changeset/pre.json index 605f1ea771..2345e3af67 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -2,280 +2,353 @@ "mode": "pre", "tag": "next", "initialVersions": { - "example-app": "0.2.85", - "@backstage/app-defaults": "1.4.1", - "example-backend": "0.2.85", - "@backstage/backend-app-api": "0.4.5", - "@backstage/backend-common": "0.19.1", - "@backstage/backend-defaults": "0.1.12", + "example-app": "0.2.86", + "@backstage/app-defaults": "1.4.2", + "example-backend": "0.2.86", + "@backstage/backend-app-api": "0.5.0", + "@backstage/backend-common": "0.19.2", + "@backstage/backend-defaults": "0.2.0", "@backstage/backend-dev-utils": "0.1.1", - "example-backend-next": "0.0.13", - "@backstage/backend-openapi-utils": "0.0.2", - "@backstage/backend-plugin-api": "0.5.4", - "@backstage/backend-tasks": "0.5.4", - "@backstage/backend-test-utils": "0.1.39", + "example-backend-next": "0.0.14", + "@backstage/backend-openapi-utils": "0.0.3", + "@backstage/backend-plugin-api": "0.6.0", + "@backstage/backend-tasks": "0.5.5", + "@backstage/backend-test-utils": "0.2.0", "@backstage/catalog-client": "1.4.3", "@backstage/catalog-model": "1.4.1", - "@backstage/cli": "0.22.9", + "@backstage/cli": "0.22.10", "@backstage/cli-common": "0.1.12", - "@backstage/cli-node": "0.1.2", + "@backstage/cli-node": "0.1.3", "@backstage/codemods": "0.1.45", "@backstage/config": "1.0.8", - "@backstage/config-loader": "1.3.2", - "@backstage/core-app-api": "1.9.0", - "@backstage/core-components": "0.13.3", + "@backstage/config-loader": "1.4.0", + "@backstage/core-app-api": "1.9.1", + "@backstage/core-components": "0.13.4", "@backstage/core-plugin-api": "1.5.3", - "@backstage/create-app": "0.5.3", - "@backstage/dev-utils": "1.0.17", - "e2e-test": "0.2.5", + "@backstage/create-app": "0.5.4", + "@backstage/dev-utils": "1.0.18", + "e2e-test": "0.2.6", "@backstage/errors": "1.2.1", "@backstage/eslint-plugin": "0.1.3", - "@backstage/integration": "1.5.1", + "@backstage/integration": "1.6.0", "@backstage/integration-aws-node": "0.1.5", - "@backstage/integration-react": "1.1.15", + "@backstage/integration-react": "1.1.16", "@backstage/release-manifests": "0.0.9", - "@backstage/repo-tools": "0.3.2", - "@techdocs/cli": "1.4.4", - "techdocs-cli-embedded-app": "0.2.84", - "@backstage/test-utils": "1.4.1", + "@backstage/repo-tools": "0.3.3", + "@techdocs/cli": "1.4.5", + "techdocs-cli-embedded-app": "0.2.85", + "@backstage/test-utils": "1.4.2", "@backstage/theme": "0.4.1", "@backstage/types": "1.1.0", "@backstage/version-bridge": "1.0.4", - "@backstage/plugin-adr": "0.6.3", - "@backstage/plugin-adr-backend": "0.3.5", - "@backstage/plugin-adr-common": "0.2.11", - "@backstage/plugin-airbrake": "0.3.20", - "@backstage/plugin-airbrake-backend": "0.2.20", - "@backstage/plugin-allure": "0.1.36", - "@backstage/plugin-analytics-module-ga": "0.1.31", - "@backstage/plugin-analytics-module-ga4": "0.1.2", - "@backstage/plugin-apache-airflow": "0.2.13", - "@backstage/plugin-api-docs": "0.9.6", + "@backstage/plugin-adr": "0.6.4", + "@backstage/plugin-adr-backend": "0.3.6", + "@backstage/plugin-adr-common": "0.2.12", + "@backstage/plugin-airbrake": "0.3.21", + "@backstage/plugin-airbrake-backend": "0.2.21", + "@backstage/plugin-allure": "0.1.37", + "@backstage/plugin-analytics-module-ga": "0.1.32", + "@backstage/plugin-analytics-module-ga4": "0.1.3", + "@backstage/plugin-analytics-module-newrelic-browser": "0.0.1", + "@backstage/plugin-apache-airflow": "0.2.14", + "@backstage/plugin-api-docs": "0.9.7", "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.2", - "@backstage/plugin-apollo-explorer": "0.1.13", - "@backstage/plugin-app-backend": "0.3.47", - "@backstage/plugin-auth-backend": "0.18.5", - "@backstage/plugin-auth-node": "0.2.16", - "@backstage/plugin-azure-devops": "0.3.2", - "@backstage/plugin-azure-devops-backend": "0.3.26", + "@backstage/plugin-apollo-explorer": "0.1.14", + "@backstage/plugin-app-backend": "0.3.48", + "@backstage/plugin-app-node": "0.1.0", + "@backstage/plugin-auth-backend": "0.18.6", + "@backstage/plugin-auth-node": "0.2.17", + "@backstage/plugin-azure-devops": "0.3.3", + "@backstage/plugin-azure-devops-backend": "0.3.27", "@backstage/plugin-azure-devops-common": "0.3.0", - "@backstage/plugin-azure-sites": "0.1.9", - "@backstage/plugin-azure-sites-backend": "0.1.9", + "@backstage/plugin-azure-sites": "0.1.10", + "@backstage/plugin-azure-sites-backend": "0.1.10", "@backstage/plugin-azure-sites-common": "0.1.0", - "@backstage/plugin-badges": "0.2.44", - "@backstage/plugin-badges-backend": "0.2.2", - "@backstage/plugin-bazaar": "0.2.11", - "@backstage/plugin-bazaar-backend": "0.2.10", - "@backstage/plugin-bitbucket-cloud-common": "0.2.8", - "@backstage/plugin-bitrise": "0.1.47", - "@backstage/plugin-catalog": "1.12.0", - "@backstage/plugin-catalog-backend": "1.11.0", - "@backstage/plugin-catalog-backend-module-aws": "0.2.2", - "@backstage/plugin-catalog-backend-module-azure": "0.1.18", - "@backstage/plugin-catalog-backend-module-bitbucket": "0.2.14", - "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.1.14", - "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.1.12", - "@backstage/plugin-catalog-backend-module-gerrit": "0.1.15", - "@backstage/plugin-catalog-backend-module-github": "0.3.2", - "@backstage/plugin-catalog-backend-module-gitlab": "0.2.3", - "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.4.0", - "@backstage/plugin-catalog-backend-module-ldap": "0.5.14", - "@backstage/plugin-catalog-backend-module-msgraph": "0.5.6", - "@backstage/plugin-catalog-backend-module-openapi": "0.1.13", - "@backstage/plugin-catalog-backend-module-puppetdb": "0.1.4", - "@backstage/plugin-catalog-backend-module-unprocessed": "0.1.1", + "@backstage/plugin-badges": "0.2.45", + "@backstage/plugin-badges-backend": "0.2.3", + "@backstage/plugin-bazaar": "0.2.12", + "@backstage/plugin-bazaar-backend": "0.2.11", + "@backstage/plugin-bitbucket-cloud-common": "0.2.9", + "@backstage/plugin-bitrise": "0.1.48", + "@backstage/plugin-catalog": "1.12.1", + "@backstage/plugin-catalog-backend": "1.12.0", + "@backstage/plugin-catalog-backend-module-aws": "0.2.3", + "@backstage/plugin-catalog-backend-module-azure": "0.1.19", + "@backstage/plugin-catalog-backend-module-bitbucket": "0.2.15", + "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.1.15", + "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.1.13", + "@backstage/plugin-catalog-backend-module-gcp": "0.1.0", + "@backstage/plugin-catalog-backend-module-gerrit": "0.1.16", + "@backstage/plugin-catalog-backend-module-github": "0.3.3", + "@backstage/plugin-catalog-backend-module-gitlab": "0.2.4", + "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.4.1", + "@backstage/plugin-catalog-backend-module-ldap": "0.5.15", + "@backstage/plugin-catalog-backend-module-msgraph": "0.5.7", + "@backstage/plugin-catalog-backend-module-openapi": "0.1.14", + "@backstage/plugin-catalog-backend-module-puppetdb": "0.1.5", + "@backstage/plugin-catalog-backend-module-unprocessed": "0.2.0", "@backstage/plugin-catalog-common": "1.0.15", - "@internal/plugin-catalog-customized": "0.0.12", - "@backstage/plugin-catalog-graph": "0.2.32", + "@internal/plugin-catalog-customized": "0.0.13", + "@backstage/plugin-catalog-graph": "0.2.33", "@backstage/plugin-catalog-graphql": "0.3.22", - "@backstage/plugin-catalog-import": "0.9.10", - "@backstage/plugin-catalog-node": "1.4.0", - "@backstage/plugin-catalog-react": "1.8.0", - "@backstage/plugin-catalog-unprocessed-entities": "0.1.1", - "@backstage/plugin-cicd-statistics": "0.1.22", - "@backstage/plugin-cicd-statistics-module-gitlab": "0.1.16", - "@backstage/plugin-circleci": "0.3.20", - "@backstage/plugin-cloudbuild": "0.3.20", - "@backstage/plugin-code-climate": "0.1.20", - "@backstage/plugin-code-coverage": "0.2.13", - "@backstage/plugin-code-coverage-backend": "0.2.13", - "@backstage/plugin-codescene": "0.1.15", - "@backstage/plugin-config-schema": "0.1.43", - "@backstage/plugin-cost-insights": "0.12.9", + "@backstage/plugin-catalog-import": "0.9.11", + "@backstage/plugin-catalog-node": "1.4.1", + "@backstage/plugin-catalog-react": "1.8.1", + "@backstage/plugin-catalog-unprocessed-entities": "0.1.2", + "@backstage/plugin-cicd-statistics": "0.1.23", + "@backstage/plugin-cicd-statistics-module-gitlab": "0.1.17", + "@backstage/plugin-circleci": "0.3.21", + "@backstage/plugin-cloudbuild": "0.3.21", + "@backstage/plugin-code-climate": "0.1.21", + "@backstage/plugin-code-coverage": "0.2.14", + "@backstage/plugin-code-coverage-backend": "0.2.14", + "@backstage/plugin-codescene": "0.1.16", + "@backstage/plugin-config-schema": "0.1.44", + "@backstage/plugin-cost-insights": "0.12.10", "@backstage/plugin-cost-insights-common": "0.1.1", - "@backstage/plugin-devtools": "0.1.2", - "@backstage/plugin-devtools-backend": "0.1.2", - "@backstage/plugin-devtools-common": "0.1.2", - "@backstage/plugin-dynatrace": "7.0.0", - "@backstage/plugin-entity-feedback": "0.2.3", - "@backstage/plugin-entity-feedback-backend": "0.1.5", - "@backstage/plugin-entity-feedback-common": "0.1.1", - "@backstage/plugin-entity-validation": "0.1.5", - "@backstage/plugin-events-backend": "0.2.8", - "@backstage/plugin-events-backend-module-aws-sqs": "0.2.2", - "@backstage/plugin-events-backend-module-azure": "0.1.9", - "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.1.9", - "@backstage/plugin-events-backend-module-gerrit": "0.1.9", - "@backstage/plugin-events-backend-module-github": "0.1.9", - "@backstage/plugin-events-backend-module-gitlab": "0.1.9", - "@backstage/plugin-events-backend-test-utils": "0.1.9", - "@backstage/plugin-events-node": "0.2.8", - "@internal/plugin-todo-list": "1.0.15", - "@internal/plugin-todo-list-backend": "1.0.15", + "@backstage/plugin-devtools": "0.1.3", + "@backstage/plugin-devtools-backend": "0.1.3", + "@backstage/plugin-devtools-common": "0.1.3", + "@backstage/plugin-dynatrace": "7.0.1", + "@backstage/plugin-entity-feedback": "0.2.4", + "@backstage/plugin-entity-feedback-backend": "0.1.6", + "@backstage/plugin-entity-feedback-common": "0.1.2", + "@backstage/plugin-entity-validation": "0.1.6", + "@backstage/plugin-events-backend": "0.2.9", + "@backstage/plugin-events-backend-module-aws-sqs": "0.2.3", + "@backstage/plugin-events-backend-module-azure": "0.1.10", + "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.1.10", + "@backstage/plugin-events-backend-module-gerrit": "0.1.10", + "@backstage/plugin-events-backend-module-github": "0.1.10", + "@backstage/plugin-events-backend-module-gitlab": "0.1.10", + "@backstage/plugin-events-backend-test-utils": "0.1.10", + "@backstage/plugin-events-node": "0.2.9", + "@internal/plugin-todo-list": "1.0.16", + "@internal/plugin-todo-list-backend": "1.0.16", "@internal/plugin-todo-list-common": "1.0.12", - "@backstage/plugin-explore": "0.4.6", - "@backstage/plugin-explore-backend": "0.0.9", + "@backstage/plugin-explore": "0.4.7", + "@backstage/plugin-explore-backend": "0.0.10", "@backstage/plugin-explore-common": "0.0.1", "@backstage/plugin-explore-react": "0.0.30", - "@backstage/plugin-firehydrant": "0.2.4", - "@backstage/plugin-fossa": "0.2.52", - "@backstage/plugin-gcalendar": "0.3.16", - "@backstage/plugin-gcp-projects": "0.3.39", - "@backstage/plugin-git-release-manager": "0.3.33", - "@backstage/plugin-github-actions": "0.6.1", - "@backstage/plugin-github-deployments": "0.1.51", - "@backstage/plugin-github-issues": "0.2.9", - "@backstage/plugin-github-pull-requests-board": "0.1.14", - "@backstage/plugin-gitops-profiles": "0.3.38", - "@backstage/plugin-gocd": "0.1.26", - "@backstage/plugin-graphiql": "0.2.52", - "@backstage/plugin-graphql-backend": "0.1.37", - "@backstage/plugin-graphql-voyager": "0.1.5", - "@backstage/plugin-home": "0.5.4", - "@backstage/plugin-home-react": "0.1.1", - "@backstage/plugin-ilert": "0.2.9", - "@backstage/plugin-jenkins": "0.8.2", - "@backstage/plugin-jenkins-backend": "0.2.2", - "@backstage/plugin-jenkins-common": "0.1.17", - "@backstage/plugin-kafka": "0.3.20", - "@backstage/plugin-kafka-backend": "0.2.40", - "@backstage/plugin-kubernetes": "0.9.3", - "@backstage/plugin-kubernetes-backend": "0.11.2", + "@backstage/plugin-firehydrant": "0.2.5", + "@backstage/plugin-fossa": "0.2.53", + "@backstage/plugin-gcalendar": "0.3.17", + "@backstage/plugin-gcp-projects": "0.3.40", + "@backstage/plugin-git-release-manager": "0.3.34", + "@backstage/plugin-github-actions": "0.6.2", + "@backstage/plugin-github-deployments": "0.1.52", + "@backstage/plugin-github-issues": "0.2.10", + "@backstage/plugin-github-pull-requests-board": "0.1.15", + "@backstage/plugin-gitops-profiles": "0.3.39", + "@backstage/plugin-gocd": "0.1.27", + "@backstage/plugin-graphiql": "0.2.53", + "@backstage/plugin-graphql-backend": "0.1.38", + "@backstage/plugin-graphql-voyager": "0.1.6", + "@backstage/plugin-home": "0.5.5", + "@backstage/plugin-home-react": "0.1.2", + "@backstage/plugin-ilert": "0.2.10", + "@backstage/plugin-jenkins": "0.8.3", + "@backstage/plugin-jenkins-backend": "0.2.3", + "@backstage/plugin-jenkins-common": "0.1.18", + "@backstage/plugin-kafka": "0.3.21", + "@backstage/plugin-kafka-backend": "0.2.41", + "@backstage/plugin-kubernetes": "0.10.0", + "@backstage/plugin-kubernetes-backend": "0.11.3", "@backstage/plugin-kubernetes-common": "0.6.5", - "@backstage/plugin-lighthouse": "0.4.5", - "@backstage/plugin-lighthouse-backend": "0.2.3", + "@backstage/plugin-lighthouse": "0.4.6", + "@backstage/plugin-lighthouse-backend": "0.2.4", "@backstage/plugin-lighthouse-common": "0.1.2", - "@backstage/plugin-linguist": "0.1.5", - "@backstage/plugin-linguist-backend": "0.3.1", - "@backstage/plugin-linguist-common": "0.1.0", - "@backstage/plugin-microsoft-calendar": "0.1.5", - "@backstage/plugin-newrelic": "0.3.38", - "@backstage/plugin-newrelic-dashboard": "0.2.13", - "@backstage/plugin-nomad": "0.1.1", - "@backstage/plugin-nomad-backend": "0.1.1", - "@backstage/plugin-octopus-deploy": "0.2.2", - "@backstage/plugin-org": "0.6.10", - "@backstage/plugin-org-react": "0.1.9", - "@backstage/plugin-pagerduty": "0.6.1", - "@backstage/plugin-periskop": "0.1.18", - "@backstage/plugin-periskop-backend": "0.1.18", - "@backstage/plugin-permission-backend": "0.5.22", + "@backstage/plugin-linguist": "0.1.6", + "@backstage/plugin-linguist-backend": "0.4.0", + "@backstage/plugin-linguist-common": "0.1.1", + "@backstage/plugin-microsoft-calendar": "0.1.6", + "@backstage/plugin-newrelic": "0.3.39", + "@backstage/plugin-newrelic-dashboard": "0.2.14", + "@backstage/plugin-nomad": "0.1.2", + "@backstage/plugin-nomad-backend": "0.1.2", + "@backstage/plugin-octopus-deploy": "0.2.3", + "@backstage/plugin-org": "0.6.11", + "@backstage/plugin-org-react": "0.1.10", + "@backstage/plugin-pagerduty": "0.6.2", + "@backstage/plugin-periskop": "0.1.19", + "@backstage/plugin-periskop-backend": "0.1.19", + "@backstage/plugin-permission-backend": "0.5.23", "@backstage/plugin-permission-common": "0.7.7", - "@backstage/plugin-permission-node": "0.7.10", + "@backstage/plugin-permission-node": "0.7.11", "@backstage/plugin-permission-react": "0.4.14", - "@backstage/plugin-playlist": "0.1.12", - "@backstage/plugin-playlist-backend": "0.3.3", - "@backstage/plugin-playlist-common": "0.1.8", - "@backstage/plugin-proxy-backend": "0.2.41", - "@backstage/plugin-puppetdb": "0.1.3", - "@backstage/plugin-rollbar": "0.4.20", - "@backstage/plugin-rollbar-backend": "0.1.44", - "@backstage/plugin-scaffolder": "1.14.1", - "@backstage/plugin-scaffolder-backend": "1.15.1", - "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.2.0", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.23", - "@backstage/plugin-scaffolder-backend-module-gitlab": "0.2.2", - "@backstage/plugin-scaffolder-backend-module-rails": "0.4.16", - "@backstage/plugin-scaffolder-backend-module-sentry": "0.1.7", - "@backstage/plugin-scaffolder-backend-module-yeoman": "0.2.20", - "@backstage/plugin-scaffolder-common": "1.3.2", - "@backstage/plugin-scaffolder-node": "0.1.5", - "@backstage/plugin-scaffolder-react": "1.5.1", - "@backstage/plugin-search": "1.3.3", - "@backstage/plugin-search-backend": "1.3.3", - "@backstage/plugin-search-backend-module-catalog": "0.1.3", - "@backstage/plugin-search-backend-module-elasticsearch": "1.3.2", - "@backstage/plugin-search-backend-module-explore": "0.1.3", - "@backstage/plugin-search-backend-module-pg": "0.5.8", - "@backstage/plugin-search-backend-module-techdocs": "0.1.3", - "@backstage/plugin-search-backend-node": "1.2.3", + "@backstage/plugin-playlist": "0.1.13", + "@backstage/plugin-playlist-backend": "0.3.4", + "@backstage/plugin-playlist-common": "0.1.9", + "@backstage/plugin-proxy-backend": "0.3.0", + "@backstage/plugin-puppetdb": "0.1.4", + "@backstage/plugin-rollbar": "0.4.21", + "@backstage/plugin-rollbar-backend": "0.1.45", + "@backstage/plugin-scaffolder": "1.14.2", + "@backstage/plugin-scaffolder-backend": "1.16.0", + "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.2.1", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.24", + "@backstage/plugin-scaffolder-backend-module-gitlab": "0.2.3", + "@backstage/plugin-scaffolder-backend-module-rails": "0.4.17", + "@backstage/plugin-scaffolder-backend-module-sentry": "0.1.8", + "@backstage/plugin-scaffolder-backend-module-yeoman": "0.2.21", + "@backstage/plugin-scaffolder-common": "1.4.0", + "@backstage/plugin-scaffolder-node": "0.2.0", + "@backstage/plugin-scaffolder-react": "1.5.2", + "@backstage/plugin-search": "1.3.4", + "@backstage/plugin-search-backend": "1.4.0", + "@backstage/plugin-search-backend-module-catalog": "0.1.4", + "@backstage/plugin-search-backend-module-elasticsearch": "1.3.3", + "@backstage/plugin-search-backend-module-explore": "0.1.4", + "@backstage/plugin-search-backend-module-pg": "0.5.9", + "@backstage/plugin-search-backend-module-techdocs": "0.1.4", + "@backstage/plugin-search-backend-node": "1.2.4", "@backstage/plugin-search-common": "1.2.5", - "@backstage/plugin-search-react": "1.6.3", - "@backstage/plugin-sentry": "0.5.5", - "@backstage/plugin-shortcuts": "0.3.12", - "@backstage/plugin-sonarqube": "0.7.1", - "@backstage/plugin-sonarqube-backend": "0.2.1", + "@backstage/plugin-search-react": "1.6.4", + "@backstage/plugin-sentry": "0.5.6", + "@backstage/plugin-shortcuts": "0.3.13", + "@backstage/plugin-sonarqube": "0.7.2", + "@backstage/plugin-sonarqube-backend": "0.2.2", "@backstage/plugin-sonarqube-react": "0.1.7", - "@backstage/plugin-splunk-on-call": "0.4.9", - "@backstage/plugin-stack-overflow": "0.1.18", - "@backstage/plugin-stack-overflow-backend": "0.2.3", - "@backstage/plugin-stackstorm": "0.1.4", - "@backstage/plugin-tech-insights": "0.3.12", - "@backstage/plugin-tech-insights-backend": "0.5.13", - "@backstage/plugin-tech-insights-backend-module-jsonfc": "0.1.31", + "@backstage/plugin-splunk-on-call": "0.4.10", + "@backstage/plugin-stack-overflow": "0.1.19", + "@backstage/plugin-stack-overflow-backend": "0.2.4", + "@backstage/plugin-stackstorm": "0.1.5", + "@backstage/plugin-tech-insights": "0.3.13", + "@backstage/plugin-tech-insights-backend": "0.5.14", + "@backstage/plugin-tech-insights-backend-module-jsonfc": "0.1.32", "@backstage/plugin-tech-insights-common": "0.2.11", - "@backstage/plugin-tech-insights-node": "0.4.5", - "@backstage/plugin-tech-radar": "0.6.6", - "@backstage/plugin-techdocs": "1.6.5", - "@backstage/plugin-techdocs-addons-test-utils": "1.0.16", - "@backstage/plugin-techdocs-backend": "1.6.4", - "@backstage/plugin-techdocs-module-addons-contrib": "1.0.15", - "@backstage/plugin-techdocs-node": "1.7.3", - "@backstage/plugin-techdocs-react": "1.1.8", - "@backstage/plugin-todo": "0.2.22", - "@backstage/plugin-todo-backend": "0.1.44", - "@backstage/plugin-user-settings": "0.7.5", - "@backstage/plugin-user-settings-backend": "0.1.11", - "@backstage/plugin-vault": "0.1.14", - "@backstage/plugin-vault-backend": "0.3.3", - "@backstage/plugin-xcmetrics": "0.2.40", - "@backstage/plugin-analytics-module-newrelic-browser": "0.0.0", - "@backstage/plugin-catalog-backend-module-gcp": "0.0.0" + "@backstage/plugin-tech-insights-node": "0.4.6", + "@backstage/plugin-tech-radar": "0.6.7", + "@backstage/plugin-techdocs": "1.6.6", + "@backstage/plugin-techdocs-addons-test-utils": "1.0.17", + "@backstage/plugin-techdocs-backend": "1.6.5", + "@backstage/plugin-techdocs-module-addons-contrib": "1.0.16", + "@backstage/plugin-techdocs-node": "1.7.4", + "@backstage/plugin-techdocs-react": "1.1.9", + "@backstage/plugin-todo": "0.2.23", + "@backstage/plugin-todo-backend": "0.2.0", + "@backstage/plugin-user-settings": "0.7.6", + "@backstage/plugin-user-settings-backend": "0.1.12", + "@backstage/plugin-vault": "0.1.15", + "@backstage/plugin-vault-backend": "0.3.4", + "@backstage/plugin-xcmetrics": "0.2.41", + "@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.0.0", + "@backstage/plugin-auth-backend-module-github-provider": "0.0.0", + "@backstage/plugin-auth-backend-module-google-provider": "0.0.0", + "@backstage/plugin-opencost": "0.1.0", + "example-app-next": "0.0.0", + "@backstage/frontend-app-api": "0.0.0", + "@backstage/frontend-plugin-api": "0.0.0", + "@backstage/plugin-auth-backend-module-gitlab-provider": "0.0.0", + "@backstage/backend-plugin-manager": "0.0.0", + "@backstage/plugin-permission-backend-module-allow-all-policy": "0.0.0" }, "changesets": [ - "analytics-millenial-whoop", - "angry-beers-relate", - "chatty-foxes-buy", - "chatty-seahorses-juggle", - "chilly-keys-count", - "cold-numbers-sleep", - "create-app-1690284535", - "create-app-1690892926", - "dirty-chefs-listen", - "gorgeous-months-doubt", - "hungry-shrimps-care", - "khaki-flies-draw", - "kind-cougars-allow", - "large-experts-poke", - "large-vans-cross", - "loud-garlics-press", - "mean-squids-relax", - "mighty-lions-search-2", - "mighty-lions-search-3", - "mighty-lions-search", - "neat-coins-raise", - "neat-lamps-press", - "odd-avocados-buy", - "pink-squids-nail", - "popular-fans-camp", - "quiet-starfishes-kick", - "rare-pens-exist", - "rich-zoos-occur", - "rude-feet-sparkle", - "search-donuts-wash", - "selfish-coats-shout", - "serious-bats-repair", - "slimy-kids-jam", - "smart-pandas-applaud", - "stale-wombats-talk", - "strong-bobcats-unite", - "stupid-berries-run", - "ten-otters-appear", - "tender-fireants-cheer", - "thin-yaks-hang", - "tidy-cobras-scream", - "tough-dolphins-care", - "tough-lies-float", - "wild-timers-sparkle" + "big-bananas-sleep", + "blue-rats-drop", + "brave-books-pretend", + "brave-carrots-occur", + "calm-cups-smile", + "catalog-everybody-to-the-limit", + "chatty-goats-mate", + "chilly-parrots-yell", + "cold-apricots-poke", + "create-app-1692704146", + "create-app-1693311380", + "curvy-roses-march", + "cyan-roses-relate", + "dirty-carrots-notice", + "dirty-garlics-kiss", + "dirty-years-travel", + "dry-wasps-occur", + "dull-tables-joke", + "early-trains.draw", + "eight-radios-listen", + "eight-timers-sin", + "eighty-fans-provide", + "empty-rings-type", + "fair-paws-flash", + "few-nails-smile", + "fifty-adults-shake", + "four-masks-fry", + "fresh-candles-warn", + "friendly-pugs-lick", + "funny-ligers-matter", + "gorgeous-coins-attack", + "great-chairs-swim", + "grumpy-papayas-tie", + "healthy-spiders-arrive", + "healthy-tools-count", + "hip-seahorses-exist", + "honest-ads-rush", + "hungry-dots-design", + "khaki-pigs-accept", + "khaki-spiders-sniff", + "late-cows-turn", + "late-hounds-yell", + "late-roses-dream", + "lazy-pumas-hope", + "lazy-rice-rule", + "long-flowers-sort", + "loud-apricots-destroy", + "lovely-years-sniff", + "lucky-chicken-hear", + "lucky-chicken-lie", + "many-tips-attend", + "mean-carpets-provide", + "mighty-crabs-cheat", + "modern-trees-add", + "nasty-cherries-exercise", + "nasty-forks-beg", + "nasty-snails-complain", + "new-sheep-cover", + "nice-forks-remain", + "ninety-news-visit", + "old-geese-drive", + "orange-houses-fly", + "pink-cups-rescue", + "pink-months-hammer", + "polite-forks-grin", + "polite-plums-share", + "poor-olives-own", + "poor-years-appear", + "popular-coins-tap", + "popular-readers-allow", + "pretty-otters-whisper", + "proud-lamps-cross", + "red-pens-knock", + "renovate-2ebf420", + "renovate-76185f9", + "renovate-b129f45", + "renovate-bbda8e8", + "repo-p-lisa", + "repo-tools-boo-descriptions-two-electric-boogaloo", + "repo-tools-boo-descriptions", + "repo-tools-ci-toldya-so", + "rude-dolphins-reply", + "silent-icons-deny", + "silly-emus-remain", + "six-radios-smile", + "six-wolves-admire", + "slow-ducks-hunt", + "slow-queens-visit", + "smart-islands-jog", + "strange-frogs-count", + "stupid-ants-change", + "sweet-hairs-complain", + "sweet-pens-hunt", + "swift-frogs-drop", + "tall-items-clap", + "tall-peas-visit", + "tricky-melons-fold", + "twelve-elephants-pay", + "warm-llamas-bake", + "warm-panthers-lick", + "weak-tomatoes-dance", + "witty-ants-cover", + "yellow-trains-protect", + "yellow-yaks-behave", + "young-ants-nail", + "young-panthers-mix" ] } diff --git a/.changeset/pretty-otters-whisper.md b/.changeset/pretty-otters-whisper.md new file mode 100644 index 0000000000..7019596b38 --- /dev/null +++ b/.changeset/pretty-otters-whisper.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs-node': minor +'@backstage/plugin-techdocs-backend': minor +--- + +Add a `techdocs.publisher.azureBlobStorage.connectionString` app-config setting, which can be leveraged for improved Azurite support. diff --git a/.changeset/proud-lamps-cross.md b/.changeset/proud-lamps-cross.md new file mode 100644 index 0000000000..cd2528a94c --- /dev/null +++ b/.changeset/proud-lamps-cross.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-code-coverage-backend': patch +--- + +Include auth token when fetching entity diff --git a/.changeset/quiet-starfishes-kick.md b/.changeset/quiet-starfishes-kick.md deleted file mode 100644 index 2fc8ed632e..0000000000 --- a/.changeset/quiet-starfishes-kick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-home': patch ---- - -Now, user can provide a language (optional) as input to receive a greeting in that specific language. Example: `` diff --git a/.changeset/rare-pens-exist.md b/.changeset/rare-pens-exist.md deleted file mode 100644 index 7363f5033a..0000000000 --- a/.changeset/rare-pens-exist.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Enable to print the config schema not merged with the `--no-merge` flag diff --git a/.changeset/red-pens-knock.md b/.changeset/red-pens-knock.md new file mode 100644 index 0000000000..98ee51b894 --- /dev/null +++ b/.changeset/red-pens-knock.md @@ -0,0 +1,93 @@ +--- +'@backstage/plugin-analytics-module-newrelic-browser': patch +'@backstage/plugin-techdocs-module-addons-contrib': patch +'@backstage/plugin-catalog-unprocessed-entities': patch +'@backstage/plugin-github-pull-requests-board': patch +'@backstage/plugin-techdocs-addons-test-utils': patch +'@backstage/plugin-analytics-module-ga4': patch +'@backstage/plugin-analytics-module-ga': patch +'@backstage/plugin-git-release-manager': patch +'@backstage/integration-react': patch +'@backstage/release-manifests': patch +'@backstage/plugin-github-deployments': patch +'@backstage/plugin-microsoft-calendar': patch +'@backstage/plugin-entity-validation': patch +'@backstage/core-components': patch +'@backstage/core-plugin-api': patch +'@backstage/plugin-apollo-explorer': patch +'@backstage/plugin-entity-feedback': patch +'@backstage/plugin-gitops-profiles': patch +'@backstage/plugin-graphql-voyager': patch +'@backstage/config-loader': patch +'@backstage/plugin-apache-airflow': patch +'@backstage/plugin-badges-backend': patch +'@backstage/plugin-github-actions': patch +'@backstage/plugin-octopus-deploy': patch +'@backstage/plugin-splunk-on-call': patch +'@backstage/plugin-stack-overflow': patch +'@backstage/app-defaults': patch +'@backstage/core-app-api': patch +'@backstage/plugin-code-coverage': patch +'@backstage/plugin-config-schema': patch +'@backstage/plugin-cost-insights': patch +'@backstage/plugin-explore-react': patch +'@backstage/plugin-github-issues': patch +'@backstage/plugin-tech-insights': patch +'@backstage/plugin-user-settings': patch +'@backstage/plugin-azure-devops': patch +'@backstage/plugin-code-climate': patch +'@backstage/plugin-gcp-projects': patch +'@backstage/create-app': patch +'@backstage/test-utils': patch +'@backstage/plugin-azure-sites': patch +'@backstage/plugin-firehydrant': patch +'@backstage/dev-utils': patch +'@backstage/plugin-cloudbuild': patch +'@backstage/plugin-home-react': patch +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-lighthouse': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-stackstorm': patch +'@backstage/plugin-tech-radar': patch +'@backstage/plugin-codescene': patch +'@backstage/plugin-dynatrace': patch +'@backstage/plugin-gcalendar': patch +'@backstage/plugin-org-react': patch +'@backstage/plugin-pagerduty': patch +'@backstage/plugin-shortcuts': patch +'@backstage/plugin-sonarqube': patch +'@backstage/plugin-xcmetrics': patch +'@backstage/plugin-airbrake': patch +'@backstage/plugin-api-docs': patch +'@backstage/plugin-circleci': patch +'@backstage/plugin-devtools': patch +'@backstage/plugin-graphiql': patch +'@backstage/plugin-linguist': patch +'@backstage/plugin-newrelic': patch +'@backstage/plugin-periskop': patch +'@backstage/plugin-playlist': patch +'@backstage/plugin-puppetdb': patch +'@backstage/plugin-techdocs': patch +'@backstage/config': patch +'@backstage/plugin-bitrise': patch +'@backstage/plugin-explore': patch +'@backstage/plugin-jenkins': patch +'@backstage/plugin-rollbar': patch +'@backstage/plugin-allure': patch +'@backstage/plugin-badges': patch +'@backstage/plugin-search': patch +'@backstage/plugin-sentry': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-ilert': patch +'@backstage/plugin-kafka': patch +'@backstage/plugin-nomad': patch +'@backstage/plugin-vault': patch +'@backstage/cli': patch +'@backstage/plugin-gocd': patch +'@backstage/plugin-home': patch +'@backstage/plugin-todo': patch +'@backstage/plugin-adr': patch +'@backstage/plugin-org': patch +--- + +Removed `@types/node` dependency diff --git a/.changeset/renovate-2ebf420.md b/.changeset/renovate-2ebf420.md new file mode 100644 index 0000000000..cbd1e76538 --- /dev/null +++ b/.changeset/renovate-2ebf420.md @@ -0,0 +1,7 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-codescene': patch +'@backstage/plugin-sonarqube': patch +--- + +Updated dependency `rc-progress` to `3.5.1`. diff --git a/.changeset/renovate-76185f9.md b/.changeset/renovate-76185f9.md new file mode 100644 index 0000000000..5e23f3cfda --- /dev/null +++ b/.changeset/renovate-76185f9.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +Updated dependency `@types/pluralize` to `^0.0.30`. diff --git a/.changeset/renovate-b129f45.md b/.changeset/renovate-b129f45.md new file mode 100644 index 0000000000..0f747fba5b --- /dev/null +++ b/.changeset/renovate-b129f45.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated dependency `run-script-webpack-plugin` to `^0.2.0`. diff --git a/.changeset/renovate-bbda8e8.md b/.changeset/renovate-bbda8e8.md new file mode 100644 index 0000000000..a43e1c55a9 --- /dev/null +++ b/.changeset/renovate-bbda8e8.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Updated dependency `use-immer` to `^0.9.0`. diff --git a/.changeset/repo-p-lisa.md b/.changeset/repo-p-lisa.md new file mode 100644 index 0000000000..d6a0fa56f4 --- /dev/null +++ b/.changeset/repo-p-lisa.md @@ -0,0 +1,7 @@ +--- +'@backstage/repo-tools': patch +--- + +The `generate-catalog-info` command now uses the first listed `CODEOWNER` as Component owner when initially +creating the `catalog-info.yaml` file. It continues to allow any one listed `CODEOWNER` when updating +entity metadata. diff --git a/.changeset/repo-tools-boo-descriptions-two-electric-boogaloo.md b/.changeset/repo-tools-boo-descriptions-two-electric-boogaloo.md new file mode 100644 index 0000000000..8cb6d49153 --- /dev/null +++ b/.changeset/repo-tools-boo-descriptions-two-electric-boogaloo.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Fixed a bug with the `generate-catalog-info` command that could cause `metadata.description` values to be overwritten by `package.json` description values only because unrelated attributes were being changed. diff --git a/.changeset/repo-tools-boo-descriptions.md b/.changeset/repo-tools-boo-descriptions.md new file mode 100644 index 0000000000..5973dd7127 --- /dev/null +++ b/.changeset/repo-tools-boo-descriptions.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Fixed a bug with the `generate-catalog-info` command that could cause the `--dry-run` flag to indicate changes to files when no changes would actually be made if the command were run without the flag. diff --git a/.changeset/repo-tools-ci-toldya-so.md b/.changeset/repo-tools-ci-toldya-so.md new file mode 100644 index 0000000000..20425fc631 --- /dev/null +++ b/.changeset/repo-tools-ci-toldya-so.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Added a `--ci` flag to the `generate-catalog-info` command. This flag behaves similarly to the same flag on `api-reports`: if `catalog-info.yaml` files would have been added or modified, then the process exits with status code `1`, and instructions are printed. diff --git a/.changeset/rich-zoos-occur.md b/.changeset/rich-zoos-occur.md deleted file mode 100644 index 979acb93c6..0000000000 --- a/.changeset/rich-zoos-occur.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/backend-plugin-api': minor -'@backstage/backend-defaults': minor ---- - -**BREAKING**: Removing shared environments concept from the new experimental backend system. diff --git a/.changeset/rotten-bees-wave.md b/.changeset/rotten-bees-wave.md new file mode 100644 index 0000000000..a5ce8cae85 --- /dev/null +++ b/.changeset/rotten-bees-wave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-github': minor +--- + +Added a `catalogModuleGithubOrgEntityProvider` for the new backend system diff --git a/.changeset/rotten-rabbits-move.md b/.changeset/rotten-rabbits-move.md deleted file mode 100644 index f2e1ffda08..0000000000 --- a/.changeset/rotten-rabbits-move.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-proxy-backend': minor ---- - -Defining proxy endpoints directly under the root `proxy` configuration key is deprecated. Endpoints should now be declared under `proxy.endpoints` instead. The `skipInvalidProxies` and `reviveConsumedRequestBodies` can now also be configured through static configuration. diff --git a/.changeset/rude-dolphins-reply.md b/.changeset/rude-dolphins-reply.md new file mode 100644 index 0000000000..89545cde1f --- /dev/null +++ b/.changeset/rude-dolphins-reply.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-module-addons-contrib': minor +--- + +Enable zoom icon for techdocs images inside lightbox diff --git a/.changeset/rude-feet-sparkle.md b/.changeset/rude-feet-sparkle.md deleted file mode 100644 index fabc83bb0b..0000000000 --- a/.changeset/rude-feet-sparkle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-analytics-module-newrelic-browser': patch ---- - -Introduced the New Relic Browser analytics module. Check out the plugins [README.md](https://github.com/backstage/backstage/tree/master/plugins/analytics-module-newrelic-browser) for more details! diff --git a/.changeset/search-donuts-wash.md b/.changeset/search-donuts-wash.md deleted file mode 100644 index 2fa3325761..0000000000 --- a/.changeset/search-donuts-wash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search-backend-node': patch ---- - -Fix Lunr search engine highlight by ignoring invalid metadata positions. diff --git a/.changeset/selfish-coats-shout.md b/.changeset/selfish-coats-shout.md deleted file mode 100644 index 071c47e371..0000000000 --- a/.changeset/selfish-coats-shout.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -'@backstage/config-loader': minor ---- - -Adds a new `deepVisibility` schema keyword that sets child visibility recursively to the defined value, respecting preexisting values or child `deepVisibility`. - -Example usage: - -```ts -export interface Config { - /** - * Enforces a default of `secret` instead of `backend` for this object. - * @deepVisibility secret - */ - mySecretProperty: { - type: 'object'; - properties: { - secretValue: { - type: 'string'; - }; - - verySecretProperty: { - type: 'string'; - }; - }; - }; -} -``` - -Example of a schema that would not be allowed: - -```ts -export interface Config { - /** - * Set the top level property to secret, enforcing a default of `secret` instead of `backend` for this object. - * @deepVisibility secret - */ - mySecretProperty: { - type: 'object'; - properties: { - frontendUrl: { - /** - * We can NOT override the visibility to reveal a property to the front end. - * @visibility frontend - */ - type: 'string'; - }; - - verySecretProperty: { - type: 'string'; - }; - }; - }; -} -``` diff --git a/.changeset/serious-bats-repair.md b/.changeset/serious-bats-repair.md deleted file mode 100644 index d93b49bda0..0000000000 --- a/.changeset/serious-bats-repair.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/repo-tools': patch ---- - -Update `schema openapi generate` command to now create a default router that can be imported and used directly. diff --git a/.changeset/seven-cougars-smash.md b/.changeset/seven-cougars-smash.md deleted file mode 100644 index 5493a724d7..0000000000 --- a/.changeset/seven-cougars-smash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-tasks': patch ---- - -Make `readTaskScheduleDefinitionFromConfig` properly handle bad inputs diff --git a/.changeset/sharp-months-think.md b/.changeset/sharp-months-think.md deleted file mode 100644 index 04ffbb8d1b..0000000000 --- a/.changeset/sharp-months-think.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-app-backend': patch ---- - -Migrated the alpha `appBackend` export to use static configuration and extension points rather than accepting options. diff --git a/.changeset/silent-icons-deny.md b/.changeset/silent-icons-deny.md new file mode 100644 index 0000000000..3dd7d75c90 --- /dev/null +++ b/.changeset/silent-icons-deny.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Extension points are now tracked via their ID rather than reference, in order to support package duplication. diff --git a/.changeset/silly-emus-remain.md b/.changeset/silly-emus-remain.md new file mode 100644 index 0000000000..4fe605080a --- /dev/null +++ b/.changeset/silly-emus-remain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Add examples for `github:actions:dispatch` scaffolder actions. diff --git a/.changeset/six-radios-smile.md b/.changeset/six-radios-smile.md new file mode 100644 index 0000000000..e4f4eed450 --- /dev/null +++ b/.changeset/six-radios-smile.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-code-coverage-backend': patch +--- + +Correct the line hits calculation for Cobertura reports diff --git a/.changeset/six-wolves-admire.md b/.changeset/six-wolves-admire.md new file mode 100644 index 0000000000..efc80826c2 --- /dev/null +++ b/.changeset/six-wolves-admire.md @@ -0,0 +1,23 @@ +--- +'@backstage/integration': minor +--- + +Added `AzureDevOpsCredentialsProvider` to support multiple Azure DevOps organizations and **deprecated** `AzureIntegrationConfig.credential` and `AzureIntegrationConfig.token` in favour of `AzureIntegrationConfig.credentials`. You can now use specific credentials for different Azure DevOps (Server) organizations by specifying the `organizations` field on a credential: + +```yaml +integrations: + azure: + - host: dev.azure.com + credentials: + - organizations: + - my-org + - my-other-org + clientId: ${AZURE_CLIENT_ID} + clientSecret: ${AZURE_CLIENT_SECRET} + tenantId: ${AZURE_TENANT_ID} + - organizations: + - yet-another-org + personalAccessToken: ${PERSONAL_ACCESS_TOKEN} +``` + +See the [Azure integration documentation](https://backstage.io/docs/integrations/azure/locations) for more information. diff --git a/.changeset/slimy-kids-jam.md b/.changeset/slimy-kids-jam.md deleted file mode 100644 index c02daf325e..0000000000 --- a/.changeset/slimy-kids-jam.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': minor ---- - -Added OpenTelemetry spans for catalog processing diff --git a/.changeset/slow-ducks-hunt.md b/.changeset/slow-ducks-hunt.md new file mode 100644 index 0000000000..0aac3056cf --- /dev/null +++ b/.changeset/slow-ducks-hunt.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added support for the `dev/index` entry point for backend plugins and modules. diff --git a/.changeset/slow-queens-visit.md b/.changeset/slow-queens-visit.md new file mode 100644 index 0000000000..2df948e477 --- /dev/null +++ b/.changeset/slow-queens-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-opencost': minor +--- + +New OpenCost plugin provides an port of the latest OpenCost UI to Backstage with updated dependencies. The plugin's README covers installation and configuration diff --git a/.changeset/smart-islands-jog.md b/.changeset/smart-islands-jog.md new file mode 100644 index 0000000000..50bc36191f --- /dev/null +++ b/.changeset/smart-islands-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fix OpenAPI schema for the facets endpoint diff --git a/.changeset/smart-pandas-applaud.md b/.changeset/smart-pandas-applaud.md deleted file mode 100644 index 5f2c297df5..0000000000 --- a/.changeset/smart-pandas-applaud.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-todo-backend': minor -'@backstage/plugin-catalog-backend': minor -'@backstage/plugin-search-backend': minor ---- - -Now performs request validation based on OpenAPI schema through `@backstage/backend-openapi-utils`. Error responses for invalid input, like `"a"` instead of a number, may have changed. diff --git a/.changeset/stale-wombats-talk.md b/.changeset/stale-wombats-talk.md deleted file mode 100644 index f4e0b852f8..0000000000 --- a/.changeset/stale-wombats-talk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-aws': patch ---- - -Added option to configure AWS `accountId` in `AwsS3EntityProvider` diff --git a/.changeset/strange-frogs-count.md b/.changeset/strange-frogs-count.md new file mode 100644 index 0000000000..94fa6b9fb6 --- /dev/null +++ b/.changeset/strange-frogs-count.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Add loading indicator to Table diff --git a/.changeset/strange-shrimps-mix.md b/.changeset/strange-shrimps-mix.md deleted file mode 100644 index fbb08eeeb6..0000000000 --- a/.changeset/strange-shrimps-mix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-linguist-backend': minor ---- - -Adds a processor to the linguist backend which can automatically add language tags to entities diff --git a/.changeset/strong-bobcats-unite.md b/.changeset/strong-bobcats-unite.md deleted file mode 100644 index ef86023d58..0000000000 --- a/.changeset/strong-bobcats-unite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-test-utils': patch ---- - -Add needed constants and constructs to support PostgreSQL version 14 as test database diff --git a/.changeset/stupid-ants-change.md b/.changeset/stupid-ants-change.md new file mode 100644 index 0000000000..c78c5e4250 --- /dev/null +++ b/.changeset/stupid-ants-change.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-insights-backend-module-jsonfc': patch +--- + +Handle extracting facts from 'not' conditions too diff --git a/.changeset/stupid-berries-run.md b/.changeset/stupid-berries-run.md deleted file mode 100644 index f506ce2475..0000000000 --- a/.changeset/stupid-berries-run.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-home': patch ---- - -Make sure the widget name is never empty in the `AddWidgetDialog`. If the title was set to "", the entry would contain an empty string. Use the name as a fallback diff --git a/.changeset/sweet-hairs-complain.md b/.changeset/sweet-hairs-complain.md new file mode 100644 index 0000000000..56dede5617 --- /dev/null +++ b/.changeset/sweet-hairs-complain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-google-provider': minor +--- + +New module for `@backstage/plugin-auth-backend` that adds a Google auth provider. diff --git a/.changeset/sweet-pens-hunt.md b/.changeset/sweet-pens-hunt.md new file mode 100644 index 0000000000..03e79ecebb --- /dev/null +++ b/.changeset/sweet-pens-hunt.md @@ -0,0 +1,24 @@ +--- +'@backstage/config-loader': minor +--- + +Deep visibility now also applies to values that are not covered by the configuration schema. + +For example, given the following configuration schema: + +```ts +// plugins/a/config.schema.ts +export interface Config { + /** @deepVisibility frontend */ + a?: unknown; +} + +// plugins/a/config.schema.ts +export interface Config { + a?: { + b?: string; + }; +} +``` + +All values under `a` are now visible to the frontend, while previously only `a` and `a/b` would've been visible. diff --git a/.changeset/swift-frogs-drop.md b/.changeset/swift-frogs-drop.md new file mode 100644 index 0000000000..3edff21ca2 --- /dev/null +++ b/.changeset/swift-frogs-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-playlist': patch +--- + +Limit the use of the same playlist name when adding a playlist diff --git a/.changeset/tall-countries-float.md b/.changeset/tall-countries-float.md new file mode 100644 index 0000000000..d6c6946b7a --- /dev/null +++ b/.changeset/tall-countries-float.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add `"sideEffects": false` to `package.json` in frontend package templates. This can be added to existing packages using the new `yarn fix` command. diff --git a/.changeset/tall-items-clap.md b/.changeset/tall-items-clap.md new file mode 100644 index 0000000000..613c280cbe --- /dev/null +++ b/.changeset/tall-items-clap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-backend-module-allow-all-policy': minor +--- + +Created package with policy `permissionModuleAllowAllPolicy` diff --git a/.changeset/tall-peas-visit.md b/.changeset/tall-peas-visit.md new file mode 100644 index 0000000000..3e3ca76e46 --- /dev/null +++ b/.changeset/tall-peas-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fix issue with `catalogFileName` not being a required property for `/analyze-location` diff --git a/.changeset/ten-lies-cheat.md b/.changeset/ten-lies-cheat.md deleted file mode 100644 index 1c965a574c..0000000000 --- a/.changeset/ten-lies-cheat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-node': patch ---- - -Export `TemplateExample` from the `createTemplateAction` type. diff --git a/.changeset/ten-otters-appear.md b/.changeset/ten-otters-appear.md deleted file mode 100644 index 4cb3d301ae..0000000000 --- a/.changeset/ten-otters-appear.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-react': patch ---- - -Fixed bug in Workflow component by passing a prop `templateName` down to Stepper component. diff --git a/.changeset/tender-fireants-cheer.md b/.changeset/tender-fireants-cheer.md deleted file mode 100644 index 1d829f673b..0000000000 --- a/.changeset/tender-fireants-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-devtools': patch ---- - -Fix readme typo diff --git a/.changeset/thin-dryers-repeat.md b/.changeset/thin-dryers-repeat.md new file mode 100644 index 0000000000..bcf62f21de --- /dev/null +++ b/.changeset/thin-dryers-repeat.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Add support for installing features as package imports, for example `backend.add(import('my-plugin'))`. diff --git a/.changeset/thin-yaks-hang.md b/.changeset/thin-yaks-hang.md deleted file mode 100644 index 1a1bdb4ab7..0000000000 --- a/.changeset/thin-yaks-hang.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search-backend': patch ---- - -Add missing `configSchema` to package.json diff --git a/.changeset/tidy-cobras-scream.md b/.changeset/tidy-cobras-scream.md deleted file mode 100644 index 2917f11dfc..0000000000 --- a/.changeset/tidy-cobras-scream.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch -'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch -'@backstage/plugin-scaffolder-backend-module-rails': patch ---- - -Import helpers from the node package instead of the backend package diff --git a/.changeset/tough-dolphins-care.md b/.changeset/tough-dolphins-care.md deleted file mode 100644 index 7d9739ec10..0000000000 --- a/.changeset/tough-dolphins-care.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-gcp': minor ---- - -Added GCP catalog plugin with GKE provider diff --git a/.changeset/tough-lies-float.md b/.changeset/tough-lies-float.md deleted file mode 100644 index 2ad3b8623f..0000000000 --- a/.changeset/tough-lies-float.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-scaffolder-node': patch ---- - -Deprecated `executeShellCommand`, `RunCommandOptions`, and `fetchContents` from `@backstage/plugin-scaffolder-backend`, since they are useful for Scaffolder modules (who should not be importing from the plugin package itself). You should now import these from `@backstage/plugin-scaffolder-backend-node` instead. `RunCommandOptions` was renamed in the Node package as `ExecuteShellCommandOptions`, for consistency. diff --git a/.changeset/tricky-melons-fold.md b/.changeset/tricky-melons-fold.md new file mode 100644 index 0000000000..036413ea8e --- /dev/null +++ b/.changeset/tricky-melons-fold.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Backend startup will now fail if any circular service dependencies are detected. diff --git a/.changeset/twelve-elephants-pay.md b/.changeset/twelve-elephants-pay.md new file mode 100644 index 0000000000..08fb983b6f --- /dev/null +++ b/.changeset/twelve-elephants-pay.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-techdocs-backend': minor +'@backstage/plugin-techdocs-node': minor +'@techdocs/cli': minor +--- + +Add optional config and cli option for techdocs to specify default mkdocs plugins. diff --git a/.changeset/unlucky-bags-pump.md b/.changeset/unlucky-bags-pump.md deleted file mode 100644 index a275887410..0000000000 --- a/.changeset/unlucky-bags-pump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Add frontend visibility to provider objects in `auth` config. diff --git a/.changeset/warm-llamas-bake.md b/.changeset/warm-llamas-bake.md new file mode 100644 index 0000000000..3da4a9b94d --- /dev/null +++ b/.changeset/warm-llamas-bake.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Do not unnecessarily notify subscribers when no-op updates to config happen diff --git a/.changeset/warm-panthers-lick.md b/.changeset/warm-panthers-lick.md new file mode 100644 index 0000000000..b06809fac1 --- /dev/null +++ b/.changeset/warm-panthers-lick.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-github-provider': minor +--- + +New module for `@backstage/plugin-auth-backend` that adds a GitHub auth provider. diff --git a/.changeset/weak-tomatoes-dance.md b/.changeset/weak-tomatoes-dance.md new file mode 100644 index 0000000000..446051e16f --- /dev/null +++ b/.changeset/weak-tomatoes-dance.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-github': patch +--- + +Properly support custom `userTransformer` returning `undefined` in `GithubMultiOrgEntityProvider` diff --git a/.changeset/wild-timers-sparkle.md b/.changeset/wild-timers-sparkle.md deleted file mode 100644 index 3df14f3a69..0000000000 --- a/.changeset/wild-timers-sparkle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-microsoft-calendar': patch ---- - -README update - example of apiRef definition and fixed component name diff --git a/.changeset/wise-tables-add.md b/.changeset/wise-tables-add.md new file mode 100644 index 0000000000..987c74cf53 --- /dev/null +++ b/.changeset/wise-tables-add.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Service factory functions are now marked as feature factories that can be installed in the backend. diff --git a/.changeset/witty-ants-cover.md b/.changeset/witty-ants-cover.md new file mode 100644 index 0000000000..7f499a88ae --- /dev/null +++ b/.changeset/witty-ants-cover.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +Allow specifying static widgets to custom home page diff --git a/.changeset/witty-pans-flash.md b/.changeset/witty-pans-flash.md new file mode 100644 index 0000000000..107ef90973 --- /dev/null +++ b/.changeset/witty-pans-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-playlist': patch +--- + +Fix the Ui style of the search bar diff --git a/.changeset/yellow-trains-protect.md b/.changeset/yellow-trains-protect.md new file mode 100644 index 0000000000..4b8c50ac3e --- /dev/null +++ b/.changeset/yellow-trains-protect.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-code-coverage-backend': patch +'@backstage/plugin-tech-insights-backend': patch +'@backstage/plugin-linguist-backend': patch +'@backstage/backend-common': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/backend-tasks': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-bazaar-backend': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Changes needed to support MySQL diff --git a/.changeset/yellow-yaks-behave.md b/.changeset/yellow-yaks-behave.md new file mode 100644 index 0000000000..546d74c3e4 --- /dev/null +++ b/.changeset/yellow-yaks-behave.md @@ -0,0 +1,7 @@ +--- +'@backstage/core-plugin-api': minor +'@backstage/core-app-api': minor +'@backstage/plugin-user-settings': patch +--- + +Introduced experimental support for internationalization. diff --git a/.changeset/young-ants-nail.md b/.changeset/young-ants-nail.md new file mode 100644 index 0000000000..6648098687 --- /dev/null +++ b/.changeset/young-ants-nail.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-github': patch +--- + +Allow github user and team transforms to return any Entity diff --git a/.changeset/young-panthers-mix.md b/.changeset/young-panthers-mix.md new file mode 100644 index 0000000000..cbaf3b3704 --- /dev/null +++ b/.changeset/young-panthers-mix.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-tech-insights-backend': patch +'@backstage/plugin-microsoft-calendar': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/backend-tasks': patch +'@backstage/plugin-cloudbuild': patch +'@backstage/plugin-gcalendar': patch +'@backstage/plugin-xcmetrics': patch +'@backstage/plugin-ilert': patch +--- + +Update to handle invalid luxon values diff --git a/.clomonitor.yml b/.clomonitor.yml new file mode 100644 index 0000000000..1e217dd400 --- /dev/null +++ b/.clomonitor.yml @@ -0,0 +1,9 @@ +# CLOMonitor metadata file +# This file must be located at the root of the repository +# https://clomonitor.io/projects/cncf/backstage + +# Checks exemptions +exemptions: + - check: trademark_disclaimer # Check identifier (see https://github.com/cncf/clomonitor/blob/main/docs/checks.md#exemptions) + # Justification of this exemption (mandatory, it will be displayed on the UI) + reason: 'The Linux Foundation trademark disclaimer and link are contained within the project web site at https://backstage.io in the copyright footer. However, because the site is delivered dynamically via React through JavaScript, the check cannot currently identify it.' diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5c957807cf..922f7588a5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,8 +4,10 @@ # The last matching pattern takes precedence. # https://help.github.com/articles/about-codeowners/ -* @backstage/maintainers -*/yarn.lock @backstage/maintainers @backstage-service +* @backstage/maintainers @backstage/reviewers +yarn.lock @backstage/maintainers @backstage/reviewers @backstage-service +*/yarn.lock @backstage/maintainers @backstage/reviewers @backstage-service +/.changeset/*.md /cypress/src/integration/plugins/techdocs.spec.ts @backstage/techdocs-maintainers /docs/assets/search @backstage/discoverability-maintainers /docs/features/search @backstage/discoverability-maintainers @@ -14,81 +16,67 @@ /packages/cli/src/commands/onboard @backstage/sharks /packages/techdocs-cli @backstage/techdocs-maintainers /packages/techdocs-cli-embedded-app @backstage/techdocs-maintainers -/plugins/adr @backstage/maintainers @kuangp -/plugins/adr-* @backstage/maintainers @kuangp -/plugins/allure @backstage/maintainers @deepak-bhardwaj-ps -/plugins/analytics-module-newrelic-browser @backstage/maintainers @jmezach -/plugins/apache-airflow @backstage/maintainers @cmpadden -/plugins/api-docs @backstage/maintainers @backstage/sda-se-reviewers -/plugins/azure-devops @backstage/maintainers @marleypowell @awanlin -/plugins/azure-devops-backend @backstage/maintainers @marleypowell @awanlin -/plugins/azure-devops-common @backstage/maintainers @marleypowell @awanlin -/plugins/bitbucket-cloud-common @backstage/maintainers @pjungermann -/plugins/bitrise @backstage/maintainers @backstage/sda-se-reviewers -/plugins/catalog @backstage/maintainers @backstage/catalog-maintainers -/plugins/catalog-* @backstage/maintainers @backstage/catalog-maintainers -/plugins/catalog-backend-module-aws @backstage/maintainers @backstage/catalog-maintainers @pjungermann -/plugins/catalog-backend-module-bitbucket-cloud @backstage/maintainers @backstage/catalog-maintainers @pjungermann -/plugins/catalog-backend-module-msgraph @backstage/maintainers @backstage/catalog-maintainers @pjungermann -/plugins/catalog-backend-module-puppetdb @backstage/maintainers @backstage/catalog-maintainers @tdabasinskas -/plugins/catalog-graph @backstage/maintainers @backstage/catalog-maintainers @backstage/sda-se-reviewers -/plugins/circleci @backstage/maintainers @adamdmharvey -/plugins/cloudbuild @backstage/maintainers @trivago/ebarrios -/plugins/code-coverage @backstage/maintainers @alde @nissayeva -/plugins/code-coverage-backend @backstage/maintainers @alde @nissayeva -/plugins/cost-insights @backstage/maintainers @backstage/silver-lining -/plugins/cost-insights-* @backstage/maintainers @backstage/silver-lining -/plugins/devtools @backstage/maintainers @awanlin -/plugins/devtools-backend @backstage/maintainers @awanlin -/plugins/devtools-common @backstage/maintainers @awanlin -/plugins/entity-feedback @backstage/maintainers @kuangp -/plugins/entity-feedback-* @backstage/maintainers @kuangp -/plugins/events-backend @backstage/maintainers @pjungermann -/plugins/events-backend-module-aws-sqs @backstage/maintainers @pjungermann -/plugins/events-backend-module-azure @backstage/maintainers @pjungermann -/plugins/events-backend-module-bitbucket-cloud @backstage/maintainers @pjungermann -/plugins/events-backend-module-gerrit @backstage/maintainers @pjungermann -/plugins/events-backend-module-github @backstage/maintainers @pjungermann -/plugins/events-backend-module-gitlab @backstage/maintainers @pjungermann -/plugins/events-backend-test-utils @backstage/maintainers @pjungermann -/plugins/events-node @backstage/maintainers @pjungermann -/plugins/explore @backstage/maintainers @backstage/sda-se-reviewers -/plugins/explore-react @backstage/maintainers @backstage/sda-se-reviewers -/plugins/fossa @backstage/maintainers @backstage/sda-se-reviewers -/plugins/gcalendar @backstage/maintainers @szubster @ptychu @kielosz @alexrybch -/plugins/git-release-manager @backstage/maintainers @erikengervall +/plugins/adr @backstage/maintainers @backstage/reviewers @kuangp +/plugins/adr-* @backstage/maintainers @backstage/reviewers @kuangp +/plugins/api-docs @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers +/plugins/azure-devops @backstage/maintainers @backstage/reviewers @awanlin +/plugins/azure-devops-backend @backstage/maintainers @backstage/reviewers @awanlin +/plugins/azure-devops-common @backstage/maintainers @backstage/reviewers @awanlin +/plugins/bitbucket-cloud-common @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/bitrise @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers +/plugins/catalog @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers +/plugins/catalog-* @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers +/plugins/catalog-backend-module-aws @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @pjungermann +/plugins/catalog-backend-module-bitbucket-cloud @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @pjungermann +/plugins/catalog-backend-module-msgraph @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @pjungermann +/plugins/catalog-backend-module-puppetdb @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers +/plugins/catalog-graph @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @backstage/sda-se-reviewers +/plugins/circleci @backstage/maintainers @backstage/reviewers @adamdmharvey +/plugins/code-coverage @backstage/maintainers @backstage/reviewers @alde +/plugins/code-coverage-backend @backstage/maintainers @backstage/reviewers @alde +/plugins/cost-insights @backstage/maintainers @backstage/reviewers @backstage/silver-lining +/plugins/cost-insights-* @backstage/maintainers @backstage/reviewers @backstage/silver-lining +/plugins/devtools @backstage/maintainers @backstage/reviewers @awanlin +/plugins/devtools-backend @backstage/maintainers @backstage/reviewers @awanlin +/plugins/devtools-common @backstage/maintainers @backstage/reviewers @awanlin +/plugins/entity-feedback @backstage/maintainers @backstage/reviewers @kuangp +/plugins/entity-feedback-* @backstage/maintainers @backstage/reviewers @kuangp +/plugins/events-backend @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/events-backend-module-aws-sqs @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/events-backend-module-azure @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/events-backend-module-bitbucket-cloud @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/events-backend-module-gerrit @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/events-backend-module-github @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/events-backend-module-gitlab @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/events-backend-test-utils @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/events-node @backstage/maintainers @backstage/reviewers @pjungermann +/plugins/explore @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers +/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/ilert @backstage/maintainers @yacut -/plugins/jenkins @backstage/maintainers @timja -/plugins/jenkins-backend @backstage/maintainers @timja -/plugins/kafka @backstage/maintainers @nirga @andrewthauer -/plugins/kafka-backend @backstage/maintainers @nirga @andrewthauer -/plugins/kubernetes @backstage/maintainers @backstage/kubernetes-maintainers -/plugins/kubernetes-* @backstage/maintainers @backstage/kubernetes-maintainers -/plugins/linguist @backstage/maintainers @awanlin -/plugins/linguist-backend @backstage/maintainers @awanlin -/plugins/linguist-common @backstage/maintainers @awanlin -/plugins/microsoft-calendar @backstage/maintainers @abhay-soni-developer @NishkarshRaj -/plugins/newrelic-dashboard @backstage/maintainers @mufaddal7 +/plugins/kafka @backstage/maintainers @backstage/reviewers @andrewthauer +/plugins/kafka-backend @backstage/maintainers @backstage/reviewers @andrewthauer +/plugins/kubernetes @backstage/kubernetes-maintainers +/plugins/kubernetes-* @backstage/kubernetes-maintainers +/plugins/linguist @backstage/maintainers @backstage/reviewers @awanlin +/plugins/linguist-backend @backstage/maintainers @backstage/reviewers @awanlin +/plugins/linguist-common @backstage/maintainers @backstage/reviewers @awanlin /plugins/permission-* @backstage/permission-maintainers -/plugins/playlist @backstage/maintainers @kuangp -/plugins/playlist-* @backstage/maintainers @kuangp -/plugins/puppetdb @backstage/maintainers @tdabasinskas -/plugins/rollbar @backstage/maintainers @andrewthauer -/plugins/rollbar-backend @backstage/maintainers @andrewthauer -/plugins/scaffolder-backend-module-rails @backstage/maintainers @angeliski -/plugins/scaffolder-backend-module-yeoman @backstage/maintainers @pawelmitka +/plugins/playlist @backstage/maintainers @backstage/reviewers @kuangp +/plugins/playlist-* @backstage/maintainers @backstage/reviewers @kuangp +/plugins/rollbar @backstage/maintainers @backstage/reviewers @andrewthauer +/plugins/rollbar-backend @backstage/maintainers @backstage/reviewers @andrewthauer /plugins/search @backstage/discoverability-maintainers /plugins/search-* @backstage/discoverability-maintainers -/plugins/sonarqube @backstage/maintainers @backstage/sda-se-reviewers +/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/sda-se-reviewers -/tech-insights-backend @backstage/maintainers @xantier @iain-b -/tech-insights-backend-module-jsonfc @backstage/maintainers @xantier @iain-b -/tech-insights-tech-insights-common @backstage/maintainers @xantier @iain-b -/tech-insights-tech-insights-node @backstage/maintainers @xantier @iain-b -yarn.lock @backstage/maintainers @backstage-service +/plugins/user-settings-backend @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers +/plugins/tech-insights-backend @backstage/maintainers @backstage/reviewers @xantier +/plugins/tech-insights-backend-module-jsonfc @backstage/maintainers @backstage/reviewers @xantier +/plugins/tech-insights-tech-insights-common @backstage/maintainers @backstage/reviewers @xantier +/plugins/tech-insights-tech-insights-node @backstage/maintainers @backstage/reviewers @xantier diff --git a/.github/labeler.yml b/.github/labeler.yml index eaba199588..dc048b1744 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -24,6 +24,8 @@ documentation: - docs/**/* microsite: - microsite/**/* +storybook: + - storybook/**/* auth: - plugins/auth-backend/**/* - packages/core-app-api/src/apis/implementations/auth/**/* diff --git a/.github/uffizzi/uffizzi.production.app-config.yaml b/.github/uffizzi/uffizzi.production.app-config.yaml index 77b093e024..34f4c90f0b 100644 --- a/.github/uffizzi/uffizzi.production.app-config.yaml +++ b/.github/uffizzi/uffizzi.production.app-config.yaml @@ -56,72 +56,73 @@ catalog: - allow: [Template] proxy: - '/circleci/api': - target: https://circleci.com/api/v1.1 - headers: - Circle-Token: ${CIRCLECI_AUTH_TOKEN} + endpoints: + '/circleci/api': + target: https://circleci.com/api/v1.1 + headers: + Circle-Token: ${CIRCLECI_AUTH_TOKEN} - '/jenkins/api': - target: http://localhost:8080 - headers: - Authorization: ${JENKINS_BASIC_AUTH_HEADER} + '/jenkins/api': + target: http://localhost:8080 + headers: + Authorization: ${JENKINS_BASIC_AUTH_HEADER} - '/travisci/api': - target: https://api.travis-ci.com - changeOrigin: true - headers: - Authorization: ${TRAVISCI_AUTH_TOKEN} - travis-api-version: '3' + '/travisci/api': + target: https://api.travis-ci.com + changeOrigin: true + headers: + Authorization: ${TRAVISCI_AUTH_TOKEN} + travis-api-version: '3' - '/newrelic/apm/api': - target: https://api.newrelic.com/v2 - headers: - X-Api-Key: ${NEW_RELIC_REST_API_KEY} + '/newrelic/apm/api': + target: https://api.newrelic.com/v2 + headers: + X-Api-Key: ${NEW_RELIC_REST_API_KEY} - '/newrelic/api': - target: https://api.newrelic.com - headers: - X-Api-Key: ${NEW_RELIC_USER_KEY} + '/newrelic/api': + target: https://api.newrelic.com + headers: + X-Api-Key: ${NEW_RELIC_USER_KEY} - '/pagerduty': - target: https://api.pagerduty.com - headers: - Authorization: Token token=${PAGERDUTY_TOKEN} + '/pagerduty': + target: https://api.pagerduty.com + headers: + Authorization: Token token=${PAGERDUTY_TOKEN} - '/buildkite/api': - target: https://api.buildkite.com/v2/ - headers: - Authorization: ${BUILDKITE_TOKEN} + '/buildkite/api': + target: https://api.buildkite.com/v2/ + headers: + Authorization: ${BUILDKITE_TOKEN} - '/sentry/api': - target: https://sentry.io/api/ - allowedMethods: ['GET'] - headers: - Authorization: ${SENTRY_TOKEN} + '/sentry/api': + target: https://sentry.io/api/ + allowedMethods: ['GET'] + headers: + Authorization: ${SENTRY_TOKEN} - '/ilert': - target: https://api.ilert.com - allowedMethods: ['GET', 'POST', 'PUT'] - allowedHeaders: ['Authorization'] - headers: - Authorization: ${ILERT_AUTH_HEADER} + '/ilert': + target: https://api.ilert.com + allowedMethods: ['GET', 'POST', 'PUT'] + allowedHeaders: ['Authorization'] + headers: + Authorization: ${ILERT_AUTH_HEADER} - '/airflow': - target: https://your.airflow.instance.com/api/v1 - headers: - Authorization: ${AIRFLOW_BASIC_AUTH_HEADER} + '/airflow': + target: https://your.airflow.instance.com/api/v1 + headers: + Authorization: ${AIRFLOW_BASIC_AUTH_HEADER} - '/gocd': - target: https://your.gocd.instance.com/go/api - allowedMethods: ['GET'] - allowedHeaders: ['Authorization'] - headers: - Authorization: Basic ${GOCD_AUTH_CREDENTIALS} + '/gocd': + target: https://your.gocd.instance.com/go/api + allowedMethods: ['GET'] + allowedHeaders: ['Authorization'] + headers: + Authorization: Basic ${GOCD_AUTH_CREDENTIALS} - '/dynatrace': - target: https://your.dynatrace.instance.com/api/v2 - headers: - Authorization: 'Api-Token ${DYNATRACE_ACCESS_TOKEN}' + '/dynatrace': + target: https://your.dynatrace.instance.com/api/v2 + headers: + Authorization: 'Api-Token ${DYNATRACE_ACCESS_TOKEN}' techdocs: builder: 'local' # Alternatives - 'external' @@ -279,3 +280,7 @@ gocd: permission: enabled: true + +search: + query: + pageLimit: 50 diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt index ed09c73c91..9fa7af5585 100644 --- a/.github/vale/Vocab/Backstage/accept.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -123,8 +123,11 @@ FireHydrant Firekube Firestore Fiverr +Flightcontrol +flightcontrol Francesco Frontside +Gaurav gerrit Gerrit gitbeaker @@ -164,6 +167,7 @@ inlined inlinehilite integrator's interop +iterable JaCoCo JavaScript jenkins @@ -257,6 +261,7 @@ padding paddings pagerduty pageview +Pandey parallelization parseable Patrik @@ -405,6 +410,7 @@ unbreak Unconference unicode unmanaged +unmount unregister unregistering unregistration diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e395370f62..65844e339a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,9 @@ jobs: - name: verify local dependency ranges run: node scripts/verify-local-dependencies.js + - name: type checking and declarations + run: yarn fix --check + - name: validate config run: yarn backstage-cli config:check --lax diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml index 6fdfd5d5a4..231b81598e 100644 --- a/.github/workflows/deploy_docker-image.yml +++ b/.github/workflows/deploy_docker-image.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [16.x] + node-version: [18.x] steps: - name: checkout diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000000..d247312f89 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,66 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + # branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '41 8 * * 6' + push: + branches: ['master'] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + + steps: + - name: 'Checkout code' + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + with: + persist-credentials: false + + - name: 'Run analysis' + uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # v2.2.0 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + publish_results: true + + # 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@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: 'Upload to code-scanning' + uses: github/codeql-action/upload-sarif@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 + with: + sarif_file: results.sarif diff --git a/.github/workflows/sync_snyk-monitor.yml b/.github/workflows/sync_snyk-monitor.yml index 87dec287d7..8bfa90d726 100644 --- a/.github/workflows/sync_snyk-monitor.yml +++ b/.github/workflows/sync_snyk-monitor.yml @@ -30,7 +30,7 @@ jobs: --remote-repo-url=https://github.com/backstage/backstage env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - NODE_OPTIONS: --max-old-space-size=4096 + NODE_OPTIONS: --max-old-space-size=7168 # 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. @@ -45,7 +45,7 @@ jobs: --sarif-file-output=snyk.sarif env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - NODE_OPTIONS: --max-old-space-size=4096 + NODE_OPTIONS: --max-old-space-size=7168 - name: Upload Snyk report uses: github/codeql-action/upload-sarif@v2 with: diff --git a/.github/workflows/sync_version-packages.yml b/.github/workflows/sync_version-packages.yml index 22421925c7..6d662ea05f 100644 --- a/.github/workflows/sync_version-packages.yml +++ b/.github/workflows/sync_version-packages.yml @@ -27,3 +27,11 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} HUSKY: '0' + + - name: Discord notification + if: ${{ failure() }} + uses: Ilshidur/action-discord@0.3.2 + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + with: + args: 'Version Packages Sync Failed https://github.com/{{GITHUB_REPOSITORY}}/actions/runs/{{GITHUB_RUN_ID}}' diff --git a/.github/workflows/uffizzi-build.yml b/.github/workflows/uffizzi-build.yml index fc62d36563..a018537daf 100644 --- a/.github/workflows/uffizzi-build.yml +++ b/.github/workflows/uffizzi-build.yml @@ -4,6 +4,8 @@ on: types: [opened, synchronize, reopened, closed] paths-ignore: - 'microsite/**' + - 'storybook/**' + - 'contrib/**' - '*.md' jobs: diff --git a/.github/workflows/verify_e2e-kubernetes-noop.yml b/.github/workflows/verify_e2e-kubernetes-noop.yml index ee9dbc69e2..ac49fbc469 100644 --- a/.github/workflows/verify_e2e-kubernetes-noop.yml +++ b/.github/workflows/verify_e2e-kubernetes-noop.yml @@ -6,7 +6,7 @@ on: pull_request: paths-ignore: - 'yarn.lock' - - '.github/workflows/verify_kubernetes.yml' + - '.github/workflows/verify_e2e-kubernetes.yml' - 'packages/backend-common/src/**' jobs: diff --git a/.github/workflows/verify_e2e-kubernetes.yml b/.github/workflows/verify_e2e-kubernetes.yml index c05339f595..4c25c259c5 100644 --- a/.github/workflows/verify_e2e-kubernetes.yml +++ b/.github/workflows/verify_e2e-kubernetes.yml @@ -4,7 +4,7 @@ on: pull_request: paths: - 'yarn.lock' - - '.github/workflows/verify_kubernetes.yml' + - '.github/workflows/verify_e2e-kubernetes.yml' - 'packages/backend-common/src/**' jobs: @@ -35,7 +35,7 @@ jobs: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: bootstrap kind - uses: helm/kind-action@v1.7.0 + uses: helm/kind-action@v1.8.0 - name: kubernetes test working-directory: packages/backend-common diff --git a/ADOPTERS.md b/ADOPTERS.md index 9c76b01ff3..0d5b9fba59 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -35,7 +35,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | | [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 | | [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | -| [DAZN](https://dazn.com/) | [David Rubio Vidal](https://github.com/davidrv87), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com), [Kamil Wolny](https://github.com/mrwolny) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | +| [DAZN](https://dazn.com/) | [David Rubio Vidal](https://github.com/davidrv87), [Marco Crivellaro](https://github.com/crivetechie), [Marian Bazalik](https://github.com/mbazalik) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | | [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | | [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. | | [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | @@ -43,7 +43,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [MavTek](https://www.mavtek.com/) | [@fgascon](https://github.com/fgascon) | Developer portal focused on standardizing practices, centralizing documentation and streamlining developer practices. | | [QuintoAndar](https://www.quintoandar.com.br/) | [@quintoandar](https://github.com/quintoandar) | Developer portal, services catalog and centralization of service metrics. | | [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. | -| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. | +| [creditas.com](https://creditas.com/) | [@rebender](https://github.com/rebender) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. | | [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration | | [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. | | [2U](https://2u.com) | [@danielleEriksen](https://github.com/danielleEriksen), [@sbhatia](https://github.com/sbhatia) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar | @@ -256,3 +256,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [JB Hi-Fi](https://www.jbhifi.com.au) | [@bahman](https://github.com/bahman-jb/) | We use Backstage as a central place for all of our components/apis across multiple teams. It helps us to quickly identify the ownership of a component/api, find the related links and have a quick access to CI/CD pipelines. | | [Zenklub](https://www.zenklub.com.br) | [@zenklub](https://github.com/zenklub), [@gioufop](https://github.com/gioufop) | Developer portal, services catalog and centralization of metrics from Grafana Stack and AWS. Furthermore, centralization of documentation and infra details like Tools, Network services and so on. | | [Platzi](https://platzi.com/) | [@juancarestre](https://github.com/juancarestre/), [Engineering at Platzi](https://github.com/PlatziDev/) | Backstage allow our developers to get easily engaged with all the internal components, technical documentations and software templates. All new developers reduce its onboarding time via Backstage, and after a couple of integrations it allowed us to create new components with in a couple of clicks. | +| [idealo](https://idealo.de) | [Wanis Fahmy](https://github.com/wanisfahmyDE), [Sajjad Pervaiz](https://github.com/sjvaiz), [Tim Heurich](https://github.com/theurichde) | Backstage is our Internal developer portal. Along with other plugins, We use the software catalog, TechDocs and explore plugins to manage, discover and document our software components and platform products. | +| [Localiza&Co](https://www.localiza.com/) | [Augusto Amormino](https://github.com/augustoamormino), [Jonas Soares](https://github.com/jonaopower), [Alexandre Amormino](https://github.com/alexandreamormino), [Greg Almeida](https://github.com/sephh) | We're excited to announce our adoption of Backstage as our Internal Developer Portal! Our mission is to elevate the Developer Experience by streaming information access. Backstage will serve as the ultimate hub for developer resources, including documentation, tools, software insights, and metrics. Through Backstage, we're simplifying processes, offering software templates to empowered and efficient development journey, enhancing self-service capabilities with the embedded all best practices. | +| [V2 Digital](https://v2.digital) | [Joe Patterson](https://github.com/jrwpatterson)| We will be using it to be a corporate dashboard plus our software catalog. | +| [AppsFlyer](https://www.appsflyer.com/) | [Shahar Shmaram](https://github.com/shmaram) | Internal Developer Portal, a catalog of all company resources, custom providers and processors, scaffolder for generating new resources. diff --git a/OWNERS.md b/OWNERS.md index 5a9457d755..6c26078f8c 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -42,6 +42,7 @@ Scope: Discoverability within Backstage, including the home page, information ar | Emma Indal | Spotify | BUX | [emmaindal](http://github.com/emmaindal) | emmaindal#7503 | | Raghunandan Balachandran | Spotify | BUX | [soapraj](http://github.com/soapraj) | raghunandanb#1114 | | Renan Mendes Carvalho | Spotify | BUX | [aitherios](http://github.com/aitherios) | aitherios#0593 | +| Bailey Brooks | Spotify | BUX | [bailey](http://github.com/bailey) | \_bailey4 | ### Helm Charts @@ -89,10 +90,17 @@ Team: @backstage/techdocs-maintainers Scope: The TechDocs plugin and related tooling -| Name | Organization | Team | GitHub | Discord | -| ------------------------ | ------------ | ------------ | ------------------------------------------------ | ------------------ | -| Morgan Bentell | Spotify | Pulp Fiction | [agentbellnorm](http://github.com/agentbellnorm) | morganbentell#9030 | -| Raghunandan Balachandran | Spotify | Pulp Fiction | [soapraj](http://github.com/soapraj) | raghunandanb#1114 | +| Name | Organization | Team | GitHub | Discord | +| ------------------------ | ------------ | ------- | ------------------------------------------------- | ------------------- | +| Alex Lorenzi | Spotify | ProTean | [AlexLorenzi](https://github.com/AlexLorenzi) | `alexlorenzi` | +| Bond Yan | Spotify | ProTean | [byan1197](https://github.com/byan1197) | `bondify` | +| Daniel Lochrie | Spotify | ProTean | [dlochrie](https://github.com/dlochrie) | - | +| Jackson Chen | Spotify | ProTean | [PeaWarrior](https://github.com/PeaWarrior) | `jacksonc#3322` | +| John Philip | Spotify | ProTean | [johnphilip283](https://github.com/johnphilip283) | `john_philip#2399` | +| Morgan Bentell | Spotify | ProTean | [agentbellnorm](http://github.com/agentbellnorm) | `morganbentell` | +| Raghunandan Balachandran | Spotify | BUX | [soapraj](http://github.com/soapraj) | `raghunandanb#1114` | +| Sydney Achinger | Spotify | ProTean | [squid-ney](https://github.com/squid-ney) | - | +| Vojta Burian | Spotify | ProTean | [sankaritan](https://github.com/sankaritan) | `vojtabu` | ## Incubating Project Areas diff --git a/README.md b/README.md index 9c8fbec79b..83f7512a07 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,6 @@ [![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) -> 🏖️ The week beginning the 26th of June, some of the maintainers will be taking a well earned Summer Holiday break. We will be slower to respond to issues and PRs during this time. Releases will continue to be shipped weekly. Normal service will resume on the 24th of July. 🏝️ - ## What is Backstage? [Backstage](https://backstage.io/) is an open platform for building developer portals. Powered by a centralized software catalog, Backstage restores order to your microservices and infrastructure and enables your product teams to ship high-quality code quickly without compromising autonomy. @@ -67,7 +65,7 @@ To engage with our community, you can use the following resources: ## License -Copyright 2020-2022 © The Backstage Authors. All rights reserved. The Linux Foundation has registered trademarks and uses trademarks. For a list of trademarks of The Linux Foundation, please see our Trademark Usage page: https://www.linuxfoundation.org/trademark-usage +Copyright 2020-2023 © The Backstage Authors. All rights reserved. The Linux Foundation has registered trademarks and uses trademarks. For a list of trademarks of The Linux Foundation, please see our Trademark Usage page: https://www.linuxfoundation.org/trademark-usage Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 diff --git a/REVIEWING.md b/REVIEWING.md index 500e15701d..43085090a8 100644 --- a/REVIEWING.md +++ b/REVIEWING.md @@ -20,6 +20,14 @@ When reviewing pull requests it's important to consider our [versioning policy a One other thing to keep in mind, especially when merging pull requests, is where in the release cycle we're currently at. In particular you want to avoid merging any large or risky changes towards the end of each release cycle. If there is a change that is ready to be merged, but you want to hold off until the next main line release, then you can label it with the `merge-after-release` label. +## Configuration + +Pull requests that have [configuration related](https://backstage.io/docs/conf/defining) changes or additions need some extra consideration. + +- Every configuration field consumed by code should have a corresponding `config.d.ts` declaration. +- Carefully consider the `@visibility` of configuration fields in the schema. The default is `backend`. Take extra care that sensitive/secret fields are marked as `@visibility secret` so that they don't accidentally leak. Also ensure that fields that the frontend wants to consume are marked as `@visibility frontend`, otherwise they can't be sent to the browser at all. +- Check that the `config.d.ts` is mentioned in the consuming package's `package.json` as illustrated [in the docs](https://backstage.io/docs/conf/defining). Otherwise it won't get packaged and picked up by the runtime. + ## Changesets We use changesets to track changes in all published packages. Changesets both define what should go into the changelog of each package, but also what kind of version bump should be done for the next release. @@ -52,6 +60,7 @@ Some things that changeset should NOT contain are: - Information related to a different package. - A large amount of content, consider for example a separate migration guide instead, either in the package README or [./docs/](./docs/), and then link to that instead. - Documentation - changesets can describe new features, but it should not be relied on for documenting them. Documentation should either be placed in [TSDoc](https://tsdoc.org) comments, package README, or [./docs/](./docs/). +- Diffs of internal code, for example mirroring what the pull request changes _inside_ a plugin rather than public surfaces. This is not of interest to the reader of a package changelog. Sometimes, however, a small and concise diff can be used in a changeset to illustrate changes that the user will have to make in _their own_ Backstage installation as part of an upgrade, specifically when breaking changes are made to a package. ### When is a changeset needed? diff --git a/app-config.yaml b/app-config.yaml index 999bb38005..37cb61f26e 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -73,6 +73,8 @@ proxy: target: https://api.newrelic.com/v2 headers: X-Api-Key: ${NEW_RELIC_REST_API_KEY} + allowedHeaders: + - link '/newrelic/api': target: https://api.newrelic.com diff --git a/contrib/.devcontainer/devcontainer.json b/contrib/.devcontainer/devcontainer.json index b312102784..14e7d7f0ae 100644 --- a/contrib/.devcontainer/devcontainer.json +++ b/contrib/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ "features": { "ghcr.io/devcontainers/features/common-utils:1": {}, "ghcr.io/devcontainers/features/docker-from-docker:1": {}, - "ghcr.io/devcontainers-contrib/features/mkdocs:1": {} + "ghcr.io/devcontainers-contrib/features/mkdocs:2": {} }, // Use 'forwardPorts' to make a list of ports inside the container available locally. diff --git a/contrib/.devcontainer/postCreate.sh b/contrib/.devcontainer/postCreate.sh index f0631a4af2..88f403de5e 100755 --- a/contrib/.devcontainer/postCreate.sh +++ b/contrib/.devcontainer/postCreate.sh @@ -1,3 +1,6 @@ #!/bin/bash yarn install -pip install mkdocs-techdocs-core \ No newline at end of file +export VIRTUAL_ENV=$HOME/venv +python3 -m venv $VIRTUAL_ENV +export PATH="$VIRTUAL_ENV/bin:$PATH" +python3 -m pip install mkdocs-techdocs-core \ No newline at end of file diff --git a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md index f28a62c018..b88efbb561 100644 --- a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md +++ b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md @@ -29,7 +29,7 @@ In the AWS console, configure ALB Authentication: - Select `OIDC` under `Authenticate` - Set `Issuer` to `https://login.microsoftonline.com/{TENANT_ID}/v2.0`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App. - Set `Authorization endpoint` to `https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App. -- Set `Token endpoint` to `https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App. +- Set `Token endpoint` to `https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App. - Set `User info endpoint` to `https://graph.microsoft.com/oidc/userinfo` - Set `Client ID` to the AAD App `Application (client) Id` - Set `Client secret` to the AAD APP `client secret` diff --git a/docs/accessibility/index.md b/docs/accessibility/index.md index 24685b95ce..b4d63fd4e4 100644 --- a/docs/accessibility/index.md +++ b/docs/accessibility/index.md @@ -15,35 +15,35 @@ There are multiple ways to contribute to making Backstage more accessible, you'l If your plugin lives in the [Backstage main repository](https://github.com/backstage/backstage/) you can modify the [urls in the Lighthouse config](https://github.com/backstage/backstage/blob/39ba2284d73885b7ca8290cb38e2b1e4d983c8d6/lighthouserc.js#L19-L34) to run the Lighthouse checks on urls where your plugin exists as well. E.g. ```diff - ci: { - collect: { - url: [ + ci: { + collect: { + url: [ + /** Software Catalog */ + 'http://localhost:3000/catalog', + 'http://localhost:3000/catalog-import', + 'http://localhost:3000/catalog/default/component/backstage', + ... + /** Your plugin paths */ + 'http://localhost:3000/your-plugin-path, - /** Software Catalog */ - 'http://localhost:3000/catalog', - 'http://localhost:3000/catalog-import', - 'http://localhost:3000/catalog/default/component/backstage', + ], + settings: { ... - ], - settings: { - ... - }, - ... - }, - assert: { + }, ... - }, - }, + }, + assert: { + ... + }, + }, ``` -To make sure the [Accessibility Github workflow](https://github.com/backstage/backstage/blob/master/.github/workflows/verify_accessibility.yml) is running when changes are made to your plugin folders, modify the [list of paths](https://github.com/backstage/backstage/blob/10759b6ad2561bd86183ad940256f9a309c7a6b0/.github/workflows/verify_accessibility.yml#L7-L16). +To make sure the [Accessibility GitHub workflow](https://github.com/backstage/backstage/blob/master/.github/workflows/verify_accessibility.yml) is running when changes are made to your plugin folders, modify the [list of paths](https://github.com/backstage/backstage/blob/10759b6ad2561bd86183ad940256f9a309c7a6b0/.github/workflows/verify_accessibility.yml#L7-L16). ### Run the Lighthouse CLI locally when developing new features If you want to use the Lighthouse CLI and run the checks based on the config you can use the following command: -``` +```shell yarn dlx @lhci/cli@0.11.x autorun ``` diff --git a/docs/assets/permissions/permission-framework-overview.drawio.svg b/docs/assets/permissions/permission-framework-overview.drawio.svg index 3740384748..fe73e92a38 100644 --- a/docs/assets/permissions/permission-framework-overview.drawio.svg +++ b/docs/assets/permissions/permission-framework-overview.drawio.svg @@ -1,4 +1,4 @@ - + -
User
User
Plugin
Plugin
Permission Response
Permission Response
Permission Framework
Permission Framework
Action
(Read request)
Action...
Resources
Resources
Resource read permission
Resource read...
Permission Policy
Permission Policy
Permission Request
Permission Request
Provided by Backstage
Provided by Backstage
Configured by integrators
Configured by integrators
Configured by plugin authors
Configured by plugin authors
Permission Backend
Permission hooks
Integration helpers
...
Permission Backend...
1
1
2
2
3
3
4
4
5
5
User gets a response
User gets a response
Text is not SVG - cannot display
\ No newline at end of file +
User
User
Plugin
Plugin
Permission Response
Permission Response
Permission Framework
Permission Framework
Action
(Read request)
Action...
Resources
Resources
Resource read permission
Resource read...
Permission Policy
Permission Policy
Permission Request
Permission Request
Provided by Backstage
Provided by Backstage
Configured by integrators
Configured by integrators
Configured by plugin authors
Configured by plugin authors
Permission Backend
Permission hooks
Integration helpers
...
Permission Backend...
1
1
2
2
3
3
4
4
5
5
User gets a response
User gets a response
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/backend-system/architecture/03-services.md b/docs/backend-system/architecture/03-services.md index 8a86d90efb..e67d467453 100644 --- a/docs/backend-system/architecture/03-services.md +++ b/docs/backend-system/architecture/03-services.md @@ -40,7 +40,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 patters 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](./07-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. 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 644816b053..750e18baf5 100644 --- a/docs/backend-system/building-plugins-and-modules/01-index.md +++ b/docs/backend-system/building-plugins-and-modules/01-index.md @@ -85,6 +85,8 @@ declare a dependency on the plugin package itself. This is to avoid a direct dependency and potentially cause duplicate installations of the plugin package, while duplicate installations of library packages should always be supported. +To create a Backend module, run `yarn new`, select `backend-module`, and fill out the rest of the prompts. This will create a new package at `plugins/-backend-module-`. + The following is an example of how to create a module that adds a new processor using the `catalogProcessingExtensionPoint`: 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 fdd8cdb1e3..8ede8855e4 100644 --- a/docs/backend-system/building-plugins-and-modules/02-testing.md +++ b/docs/backend-system/building-plugins-and-modules/02-testing.md @@ -118,7 +118,7 @@ describe('MyDatabaseClass', () => { // "physical" databases to test against is much costlier than creating the // "logical" databases within them that the individual tests use. const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3', 'MYSQL_8'], }); // Just an example of how to conveniently bundle up the setup code diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index a51db3c993..b4fcdd5c6c 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -373,7 +373,7 @@ createBackendPlugin({ discovery: coreServices.discovery, }, async init({ discovery }) { - const url = await discoverty.getBaseUrl('derp'); // can also use discovery.getBaseUrl to retrieve external URL + const url = await discovery.getBaseUrl('derp'); // can also use discovery.getExternalBaseUrl to retrieve external URL const response = await fetch(`${url}/hello`); }, }); diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md index dc436129c5..3bbf06d614 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -67,6 +67,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ # in which case you should also move better-sqlite3 to "devDependencies" in package.json. RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && \ apt-get install -y --no-install-recommends libsqlite3-dev # From here on we use the least-privileged `node` user to run the backend. @@ -185,6 +186,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ # in which case you should also move better-sqlite3 to "devDependencies" in package.json. RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && \ apt-get install -y --no-install-recommends libsqlite3-dev USER node @@ -222,6 +224,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ # in which case you should also move better-sqlite3 to "devDependencies" in package.json. RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && \ apt-get install -y --no-install-recommends libsqlite3-dev # From here on we use the least-privileged `node` user to run the backend. diff --git a/docs/deployment/flightcontrol.md b/docs/deployment/flightcontrol.md new file mode 100644 index 0000000000..482e9341ea --- /dev/null +++ b/docs/deployment/flightcontrol.md @@ -0,0 +1,78 @@ +--- +id: flightcontrol +title: Deploying with Flightcontrol +sidebar_label: AWS Fargate via Flightcontrol +description: Deploying Backstage to AWS Fargate via Flightcontrol +--- + +This guide explains how to deploy Backstage to [Flightcontrol](https://www.flightcontrol.dev?ref=backstage), a platform that fully automates deployments to Amazon Web Services (AWS). Flightcontrol supports git-driven and image registry deployments. + +Before you begin, make sure you have a [Flightcontrol account](https://app.flightcontrol.dev/signup?ref=backstage) and a [Github account](https://github.com/login) to follow this guide. + +# Deployment Via Dashboard + +1. Create a new project from the Flightcontrol Dashboard + +2. Select the GitHub repo for your Backstage project + +3. Select `GUI` as the config type: + +4. Then, choose `+ Add Web Server (Fargate)` under Services before entering the following server information: + +| Field Name | Value | +| ----------------- | ----------------- | +| Build Type | Custom Dockerfile | +| Health Check Path | /catalog | +| Port | 7007 | + +5. Click `Create Project` and complete any required steps (like linking your AWS account). + +# Deployment via Code + +1. Create a new project from the Flightcontrol Dashboard + +2. Select the GitHub repo for your Backstage project + +3. Select the `flightcontrol.json` Config Type. + +```json filename="flightcontrol.json" +{ + "$schema": "https://app.flightcontrol.dev/schema.json", + "environments": [ + { + "id": "backstage", + "name": "Backstage", + "region": "us-west-2", + "source": { + "branch": "main" + }, + "services": [ + { + "id": "backstage", + "name": "Backstage", + "type": "fargate", + "buildType": "docker", + "dockerfilePath": "Dockerfile", + "dockerContext": ".", + "healthCheckPath": "/catalog", + "cpu": 0.5, + "memory": 1, + "domain": "backstage.yourapp.com", + "port": 7007, + "minInstances": 1, + "maxInstances": 1 + } + ] + } + ] +} +``` + +# Databases and Redis + +If you need a database or Redis for your Backstage plugins, you can easily add those to your Flightcontrol deployment. For more information, see [the flightcontrol docs](https://www.flightcontrol.dev/docs/guides/flightcontrol/using-code?ref=backstage#redis). + +## Troubleshooting + +- [Flightcontrol Documentation](https://www.flightcontrol.dev/docs?ref=backstage) +- [Troubleshooting](https://www.flightcontrol.dev/docs/troubleshooting?ref=backstage) diff --git a/docs/features/kubernetes/authentication.md b/docs/features/kubernetes/authentication.md index ddeabb5db0..f30521189f 100644 --- a/docs/features/kubernetes/authentication.md +++ b/docs/features/kubernetes/authentication.md @@ -28,6 +28,53 @@ The providers available as server side are: - `localKubectlProxy` - `serviceAccount` +### AWS + +For AWS, in addition to the "kubernetes" configuration, you will have to set up AWS authentication. The AWS server-side authentication provider uses [AWS Identity and Access Management (IAM)][3] to authenticate to the target Account(s), you can read more about it on the page for the [Integration AWS node][4]. + +Using the plugin, you can authenticate to several AWS accounts using either [static AWS Access keys][5] or short-lived Access keys generated by [assuming a role][6], for either case you will need to install the [AWS CLI utility][7] and set it up following the steps in the linked documentation. + +If you have generated static AWS security credentials, the configuration block for AWS will look like this: + +```yaml +aws: + mainAccount: + accounts: + - accountId: '' + accessKeyId: ${AWS_ACCESS_KEY_ID} + secretAccessKey: ${AWS_SECRET_ACCESS_KEY} + region: + accountDefaults: +``` + +If your environment is set up to assume a role, the configuration would instead look like this: + +```yaml +aws: + mainAccount: + roleName: + region: + accounts: + accountDefaults: +``` + +Either of these sections needs to be present for the Kubernetes configuration to use the `aws` `authProvider`. The Kubernetes configuration looks like this: + +```yaml +kubernetes: + serviceLocatorMethod: + type: 'multiTenant' + clusterLocatorMethods: + - type: 'config' + clusters: + - url: https://..eks.amazonaws.com + name: + authProvider: 'aws' + caData: ${EKS_CA_DATA} +``` + +You get both, the cluster `url` and `caData` directly from the AWS console by going to `EKS` > `Your cluster` > `Overview` > `Details`. You will find them under 'API server endpoint' and 'Certificate authority' respectively. + ### Azure The Azure server side authentication provider works by authenticating on the server with @@ -72,3 +119,8 @@ The providers available as client side are: [1]: https://docs.microsoft.com/en-us/azure/aks/managed-aad [2]: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest +[3]: https://docs.aws.amazon.com/IAM/latest/UserGuide/when-to-use-iam.html +[4]: https://github.com/backstage/backstage/blob/master/packages/integration-aws-node/README.md +[5]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html +[6]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use.html +[7]: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html diff --git a/docs/features/software-catalog/api.md b/docs/features/software-catalog/api.md index 3ca7e5ef49..3ccd89b286 100644 --- a/docs/features/software-catalog/api.md +++ b/docs/features/software-catalog/api.md @@ -279,7 +279,65 @@ where the `items` array has _the same length_ and _the same order_ as the input ## Locations -TODO +### `GET /locations` + +Lists locations. + +Response type is JSON, on the form + +```json +[ + { + "data": { + "id": "b9784c38-7118-472f-9e22-5638fc73bab0", + "target": "https://git.example.com/example-project/example-repository/blob/main/catalog-info.yaml", + "type": "url" + } + } +] +``` + +### `POST /locations` + +Adds a location to be ingested by the catalog. + +If successful the response code will be `HTTP/1.1 201 Created` and a JSON on the form + +```json +{ + "entities": [], + "location": { + "id": "b9784c38-7118-472f-9e22-5638fc73bab0", + "target": "https://git.example.com/example-project/example-repository/blob/main/catalog-info.yaml", + "type": "url" + } +} +``` + +If the location already exists the response will be `HTTP/1.1 409 Conflict` and a JSON on the form + +```json +{ + "error": { + "message": "Location url:https://git.example.com/example-project/example-repository/blob/main/catalog-info.yaml already exists", + "name": "ConflictError", + "stack": "ConflictError: Location url:https://git.example.com/example-project/example-repository/blob/main/catalog-info.yaml already exists\n..." + }, + "request": { + "method": "POST", + "url": "/locations" + }, + "response": { + "statusCode": 409 + } +} +``` + +Supports the `?dryRun=true` query parameter, which will perform validation and not write anything to the database. In the event of successfully passing validation, the `entities` field of the response JSON will be populated with entities present in the location. + +### `DELETE /locations/` + +Delete a location by its id. On success response code will be `HTTP/1.1 204 No Content`. ## Other diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md index 5abd266099..bd914cc56c 100644 --- a/docs/features/software-catalog/configuration.md +++ b/docs/features/software-catalog/configuration.md @@ -123,3 +123,43 @@ source that should be mirrored into Backstage. To make Backstage a mirror of this remote source, users cannot also register new entities with e.g. the [catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import) plugin. + +## Clean up orphaned entities + +In short entities can become orphaned through multiple means, such as when a catalog-info YAML file is moved from one place to another in the version control system without updating the registration in the catalog. For safety reasons the default behavior is to just tag the orphaned entities, and keep them around. You can read more about orphaned entities [here](life-of-an-entity.md#orphaning). + +However, if you do with to automatically remove the orphaned entities, you can use the following configuration, and everything with an orphaned entity tag will be eventually deleted. + +```yaml +catalog: + orphanStrategy: delete +``` + +## Processing Interval + +The [processing loop](https://backstage.io/docs/features/software-catalog/life-of-an-entity) is +responsible for running your registered processors on all entities, on a certain +interval. That interval can be configured with the `processingInterval` +app-config parameter. + +```yaml +catalog: + processingInterval: { minutes: 45 } +``` + +The value is a duration object, that has one or more of the fields `years`, +`months`, `weeks`, `days`, `hours`, `minutes`, `seconds`, and `milliseconds`. +You can combine them, for example as `{ hours: 1, minutes: 15 }` which +essentially means that you want the processing loop to visit entities roughly +once every 75 minutes. + +Note that this is only a suggested minimum, and the actual interval may be +longer. Internally, the catalog will scale up this number by a small factor and +choose random numbers in that range to spread out the load. If the catalog is +overloaded and cannot process all entities during the interval, the time taken +between processing runs of any given entity may also be longer than specified +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. diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 0be2bf0292..924c59a6fa 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -978,9 +978,9 @@ way. The entries of this array are [entity references](https://backstage.io/docs/features/software-catalog/references). -| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | -| --------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------- | -| [`Group`](#kind-group) (default) | Same as this entity, typically `default` | [`hasMember`, and reverse `memberOf`](well-known-relations.md#memberof-and-hasmember) | +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------- | +| [`Group`](#kind-group) (default) | Same as this entity, typically `default` | [`parentOf`, and reverse `childOf`](well-known-relations.md#parentof-and-childof) | ### `spec.members` [optional] diff --git a/docs/features/software-catalog/life-of-an-entity.md b/docs/features/software-catalog/life-of-an-entity.md index 618923289e..583be7df20 100644 --- a/docs/features/software-catalog/life-of-an-entity.md +++ b/docs/features/software-catalog/life-of-an-entity.md @@ -222,7 +222,8 @@ either, it becomes _orphaned_. The end result is as follows: - The stitching process injects a `backstage.io/orphan: 'true'` annotation on the child entity. - The child entity is _not_ removed from the catalog, but stays around until - explicitly deleted via the catalog API, or "reclaimed" by the original parent + explicitly deleted via the catalog API, implicitly if `orphanStrategy: delete` + configuration is set, or until it is "reclaimed" by the original parent or another parent starting to reference it. - The catalog page in Backstage for the child entity detects the new annotation and informs users about the orphan status. @@ -259,9 +260,13 @@ entities without explicit owner consent. The catalog therefore takes the stance that entities that often were added by direct user action should also be deleted only by direct user action. -It is possible to use the catalog API to build automated "reaper" systems that -finally delete entities that are orphaned. This is however not something that's -provided out of the box. +However, if you want to delete orphaned entities automatically anyway, you can +enable the automated clean up with the following app-config option. + +``` +catalog: + orphanStrategy: delete +``` ## Implicit Deletion diff --git a/docs/features/software-catalog/references.md b/docs/features/software-catalog/references.md index 6e0304b2e8..2240d376ce 100644 --- a/docs/features/software-catalog/references.md +++ b/docs/features/software-catalog/references.md @@ -23,9 +23,9 @@ compound reference structure. ## String References -This is the most common alternative, that is used in almost all circumstances. +This is the most common alternative and is used in almost all circumstances. -The string is on the form `[:][/]`, that is, it is +The string is of the form `[:][/]`. That is, it is composed of between one and three parts in this specific order, without any additional encoding: diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index 0a4f5ee400..70f5ff5e14 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -100,6 +100,21 @@ alongside the entity's source code, the value of this annotation can point to an absolute URL, matching the location reference string format outlined above, for example: `url:https://github.com/backstage/backstage/tree/master` +### backstage.io/techdocs-entity + +```yaml +# Example: +metadata: + annotations: + backstage.io/techdocs-entity: component:default/example +``` + +The value of this annotation informs of an external entity that owns the TechDocs. +This allows you to reference TechDocs from a single source without either duplicating +the TechDocs in the TechDocs page or needing multiple builds of the same docs. + +This is for situations where you have complex systems where they share a single repo, and likely a single TechDoc location. + ### backstage.io/view-url, backstage.io/edit-url ```yaml diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index c2f14d0ca0..929ba150d2 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -207,5 +207,9 @@ scaffolder backend: | Azure Pipeline Actions | [scaffolder-backend-module-azure-pipelines](https://www.npmjs.com/package/@parfuemerie-douglas/scaffolder-backend-module-azure-pipelines) | [Parfümerie Douglas](https://github.com/Parfuemerie-Douglas) | | Azure Repository Actions | [scaffolder-backend-module-azure-repositories](https://www.npmjs.com/package/@parfuemerie-douglas/scaffolder-backend-module-azure-repositories) | [Parfümerie Douglas](https://github.com/Parfuemerie-Douglas) | | Snyk Import Project | [plugin-scaffolder-backend-module-snyk](https://www.npmjs.com/package/@ma11hewthomas/plugin-scaffolder-backend-module-snyk) | [Matthew Thomas](https://github.com/Ma11hewThomas) | +| JSON Merge Actions | [plugin-scaffolder-json-merge-actions](https://www.npmjs.com/package/@mdude2314/backstage-plugin-scaffolder-json-merge-actions) | [Drew Hill](https://github.com/arhill05) | +| NPM Actions | [plugin-scaffolder-npm-actions](https://www.npmjs.com/package/@mdude2314/backstage-plugin-scaffolder-npm-actions) | [Drew Hill](https://github.com/arhill05) | +| Slack Actions | [plugin-scaffolder-backend-module-slack](https://www.npmjs.com/package/@mdude2314/backstage-plugin-scaffolder-backend-module-slack) | [Drew Hill](https://github.com/arhill05) | +| Microsoft Teams Actions | [plugin-scaffolder-backend-module-ms-teams](https://www.npmjs.com/package/@grvpandey11/backstage-plugin-scaffolder-backend-module-ms-teams) | [Gaurav Pandey](https://github.com/grvpandey11) | Have fun! 🚀 diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index 20575a0b4a..f22cecd22c 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -89,10 +89,8 @@ export const validateKebabCaseValidation = ( then please use `scaffolderPlugin.provide` from there instead and export it part of your `plugin.ts` rather than re-using the `scaffolder.plugin`. */ -import { - scaffolderPlugin, - createScaffolderFieldExtension, -} from '@backstage/plugin-scaffolder'; +import { scaffolderPlugin } from '@backstage/plugin-scaffolder'; +import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react'; import { ValidateKebabCase, validateKebabCaseValidation, @@ -133,7 +131,7 @@ Should look something like this instead: ```tsx import { ValidateKebabCaseFieldExtension } from './scaffolder/ValidateKebabCase'; -import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder'; +import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder-react'; const routes = ( diff --git a/docs/features/software-templates/writing-custom-step-layouts.md b/docs/features/software-templates/writing-custom-step-layouts.md index 69d61b93dc..b965f60f6b 100644 --- a/docs/features/software-templates/writing-custom-step-layouts.md +++ b/docs/features/software-templates/writing-custom-step-layouts.md @@ -4,7 +4,7 @@ title: Writing custom step layouts description: How to override the default step form layout --- -Every form in each step rendered in the frontend uses the default form layout from [react-json-schema-form](https://react-jsonschema-form.readthedocs.io/). It is possible to override this behaviour by supplying a `ui:ObjectFieldTemplate` property for a particular step: +Every form in each step rendered in the frontend uses the default form layout from [react-jsonschema-form](https://rjsf-team.github.io/react-jsonschema-form/docs/). It is possible to override this behaviour by supplying a `ui:ObjectFieldTemplate` property for a particular step: ```yaml parameters: @@ -12,7 +12,7 @@ parameters: ui:ObjectFieldTemplate: TwoColumn ``` -This is the same [field](https://react-jsonschema-form.readthedocs.io/en/latest/advanced-customization/custom-templates/#objectfieldtemplate) used by [react-json-schema-form](https://react-jsonschema-form.readthedocs.io/) but we need to add a couple of steps to ensure that the string value of `TwoColumn` above is resolved to a react component. +This is the same [field](https://rjsf-team.github.io/react-jsonschema-form/docs/advanced-customization/custom-templates#objectfieldtemplate) used by [react-jsonschema-form](https://rjsf-team.github.io/react-jsonschema-form/docs/) but we need to add a couple of steps to ensure that the string value of `TwoColumn` above is resolved to a react component. ## Registering a React component as a custom step layout diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 6e7b66fcc9..135d508513 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -499,6 +499,7 @@ template. These follow the same standard format: - id: fetch-base # A unique id for the step name: Fetch Base # A title displayed in the frontend if: ${{ parameters.name }} # Optional condition, skip the step if not truthy + each: ${{ parameters.iterable }} # Optional iterable, run the same step multiple times action: fetch:template # An action to call input: # Input that is passed as arguments to the action handler url: ./template @@ -510,6 +511,27 @@ By default we ship some [built in actions](./builtin-actions.md) that you can take a look at, or you can [create your own custom actions](./writing-custom-actions.md). +When `each` is provided, the current iteration value is available in the `${{ each }}` input. + +Examples: + +```yaml +each: ['apples', 'oranges'] +input: + values: + fruit: ${{ each.value }} +``` + +```yaml +each: [{ name: 'apple', count: 3 }, { name: 'orange', count: 1 }] +input: + values: + fruit: ${{ each.value.name }} + count: ${{ each.value.count }} +``` + +When `each` is used, the outputs of a repeated step are returned as an array of outputs from each iteration. + ## Outputs Each individual step can output some variables that can be used in the diff --git a/docs/features/techdocs/addons.md b/docs/features/techdocs/addons.md index bd709ee15f..a41995c260 100644 --- a/docs/features/techdocs/addons.md +++ b/docs/features/techdocs/addons.md @@ -122,12 +122,12 @@ page header, TechDocs Addons whose location is `Header` will not be rendered. Addons can, in principle, be provided by any plugin! To make it easier to discover available Addons, we've compiled a list of them here: -| Addon | Package/Plugin | Description | -| ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [``](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.expandablenavigation) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. | -| [``](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.reportissue) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. | -| [``](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.textsize) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. | -| [``](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.lightbox) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. The image size of the light-box image is the same as the image size on the document page. | +| Addon | Package/Plugin | Description | +| ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [``](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.expandablenavigation) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. | +| [``](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.reportissue) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. | +| [``](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.textsize) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. | +| [``](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.lightbox) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. The image size of the light-box image is the same as the image size on the document page. When clicking on the zoom icon it zooms the image to fit in the screen (similar to `background-size: contain`). | Got an Addon to contribute? Feel free to add a row above! diff --git a/docs/features/techdocs/cli.md b/docs/features/techdocs/cli.md index 76589de5a8..87eb67d04a 100644 --- a/docs/features/techdocs/cli.md +++ b/docs/features/techdocs/cli.md @@ -141,6 +141,7 @@ Options: if not found. --etag A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json. + --defaultPlugin Plugins which should be added automatically to the mkdocs.yaml file. (default: []) --omitTechdocsCoreMkdocsPlugin An option to disable automatic addition of techdocs-core plugin to the mkdocs.yaml files. Defaults to false, which means that the techdocs-core plugin is always added to the mkdocs file. --legacyCopyReadmeMdToIndexMd Attempt to ensure an index.md exists falling back to using /README.md or README.md diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index 49ee9d72c2..a861a50c87 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -49,6 +49,15 @@ techdocs: # will be broken in these scenarios. legacyCopyReadmeMdToIndexMd: false + # (Optional) Configures the default plugins which should be added + # automatically to every mkdocs.yaml file. This simplifies the usage as + # e.g. styling plugins can be added once for all. + # Make sure that the defined plugins are installed locally / in the Docker + # image. + # By default, only the techdocs-core plugin will be added (except if + # omitTechdocsCorePlugin: true). + defaultPlugins: ['techdocs-core'] + # techdocs.builder can be either 'local' or 'external'. # Using the default build strategy, if builder is set to 'local' and you open a TechDocs page, # techdocs-backend will try to generate the docs, publish to storage and show the generated docs afterwards. @@ -158,6 +167,12 @@ techdocs: # (Required) Azure Blob Storage Container Name containerName: 'techdocs-storage' + # (Optional) Azure blob storage connection string. + # Can be useful for local testing through azurite + # Defaults to undefined + # if provided, takes higher priority, 'techdocs.publisher.azureBlobStorage.credentials' will become irrelevant + connectionString: '' + # (Required) An account name is required to write to a storage blob container. # https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key credentials: diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index ab554db3bd..c64018ab97 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -188,15 +188,12 @@ import { } from '@backstage/plugin-techdocs'; import { Entity } from '@backstage/catalog-model'; -import { - EntityListDocsGrid, - DocsGroupConfig, -} from '@backstage/plugin-techdocs'; +import { EntityListDocsGrid } from '@backstage/plugin-techdocs'; export type CustomTechDocsHomeProps = { groups?: Array<{ title: React.ReactNode; - filterPredicate: (entity: Entity) => boolean; + filterPredicate: ((entity: Entity) => boolean) | string; }>; }; @@ -678,3 +675,32 @@ entity. If the value of this annotation is `'local'`, the TechDocs backend will and publish the documentation for them. If the value of the `company.com/techdocs-builder` annotation is anything other than `'local'`, the user is responsible for publishing documentation to the appropriate location in the TechDocs external storage. + +## Reference another components TechDocs + +In systems where you might have multiple entities for example a System with a Website and an API, when served from a Monorepo you might want to keep the TechDocs in one location in the repository. + +In this case you can add the `backstage.io/techdocs-entity` annotation and point to the owners `entityRef` and use its TechDocs. This allows the Subcomponents to read the parents docs, filling the TechDocs link on the `AboutCard` element and the Techdocs tab + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: System +metadata: + name: example + namespace: default + title: Example + description: This is the parent entity + annotations: + backstage.io/techdocs-ref: dir:. + +--- +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: example-platfrom + title: Example Application Platform + namespace: default + description: This is the child entity + annotations: + backstage.io/techdocs-entity: system:default/example +``` diff --git a/docs/integrations/azure/locations.md b/docs/integrations/azure/locations.md index 5647a6cba4..7553df4081 100644 --- a/docs/integrations/azure/locations.md +++ b/docs/integrations/azure/locations.md @@ -19,10 +19,10 @@ Using a service principal: integrations: azure: - host: dev.azure.com - credential: - clientId: ${CLIENT_ID} - clientSecret: ${CLIENT_SECRET} - tenantId: ${TENANT_ID} + credentials: + - clientId: ${AZURE_CLIENT_ID} + clientSecret: ${AZURE_CLIENT_SECRET} + tenantId: ${AZURE_TENANT_ID} ``` Using a managed identity: @@ -31,8 +31,8 @@ Using a managed identity: integrations: azure: - host: dev.azure.com - credential: - clientId: ${CLIENT_ID} + credentials: + - clientId: ${AZURE_CLIENT_ID} ``` Using a personal access token (PAT): @@ -41,24 +41,54 @@ Using a personal access token (PAT): integrations: azure: - host: dev.azure.com - token: ${AZURE_TOKEN} + credentials: + - personalAccessToken: ${PERSONAL_ACCESS_TOKEN} ``` +You can use specific credentials for different Azure DevOps organizations by specifying the `organizations` field on the credential: + +```yaml +integrations: + azure: + - host: dev.azure.com + credentials: + - organizations: + - my-org + - my-other-org + clientId: ${AZURE_CLIENT_ID} + clientSecret: ${AZURE_CLIENT_SECRET} + tenantId: ${AZURE_TENANT_ID} + - organizations: + - another-org + clientId: ${AZURE_CLIENT_ID} + - organizations: + - yet-another-org + personalAccessToken: ${PERSONAL_ACCESS_TOKEN} +``` + +If you do not specify the `organizations` field the credential will be used for all organizations for which no other credential is configured. + > Note: An Azure DevOps provider is added automatically at startup for > convenience, so you only need to list it if you want to supply a -> [token](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate), +> [personalAccessToken](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate), > a [service principal](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity), > or a [managed identity](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity) The configuration is a structure with these elements: -- `host`: The DevOps host; only `dev.azure.com` is supported. -- `token` (optional): A personal access token as expected by Azure DevOps. -- `credential`: (optional): A service principal or managed identity +- `credentials`: (optional): A service principal, managed identity, or personal access token + +The `credentials` element is a structure with these elements: + +- `organizations`: (optional): A list of organizations for which this credential should be used. If not specified the credential will be used for all organizations for which no other credential is configured. +- `clientId`: The client ID of the service principal or managed identity (required for service principal and managed identities) +- `clientSecret`: The client secret of the service principal (required for service principal) +- `tenantId`: The tenant ID of the service principal (required for service principal) +- `personalAccessToken`: The personal access token (required for personal access token) > Note: > -> - `token` and `credential` are mutually exclusive. > - You cannot use a service principal or managed identity for Azure DevOps Server (on-premises) organizations > - You can only use a service principal or managed identity for Azure AD backed Azure DevOps organizations -> - The token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API +> - You can only specify one credential per host without any organizations specified +> - The personal access token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API diff --git a/docs/overview/glossary.md b/docs/overview/glossary.md index 97a2823365..ab7d224f4d 100644 --- a/docs/overview/glossary.md +++ b/docs/overview/glossary.md @@ -2,25 +2,25 @@ id: glossary title: Backstage Glossary # prettier-ignore -description: List of all the terms, abbreviations, and phrases used in Backstage, together with their explanations. +description: List of terms, abbreviations, and phrases used in Backstage, together with their explanations. --- -The Backstage Glossary lists all the terms, abbreviations, and phrases used in +The Backstage Glossary lists terms, abbreviations, and phrases used in Backstage, together with their explanations. We encourage you to use the terminology below for clarity and consistency when discussing Backstage. -### Authentication Glossary - -This [page](../auth/glossary.md) directs to the terms and phrases related to -authentication and identity section of Backstage. +See also [Authentication Glossary](../auth/glossary.md), a separate glossary of terms and phrases +specifically related to the authentication and identity section of Backstage. ### Backstage User Profiles There are three main user profiles for Backstage: the integrator, the -contributor, and the software engineer. +contributor, and the end user (typically a software engineer). -| Term | Explanation | -| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Integrator | The **integrator** hosts the Backstage app and configures which plugins are available to use in the app. | -| Contributor | The **contributor** adds functionality to the app by writing plugins. | -| Software Engineer | The **software engineer** uses the app's functionality and interacts with its plugins. In practice, this profile covers the various roles that help deliver software, from the Software Engineer themselves, to Designers, Data Scientists, Product Owners, Engineering Managers, etc. | +| Term | Explanation | +| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Integrator | The **integrator** hosts the Backstage app and configures which plugins are available to use in the app. | +| Contributor | The **contributor** adds functionality to the app by writing plugins. | +| End user | The **end user** uses the app's functionality and interacts with its plugins. This profile covers the various roles that help deliver software. The typical end user is a **software engineer**, but users might also consider themselves _designers_, _data scientists_, _product owners_, _engineering managers_, _technical writers_, and so on. | +| Software engineer | The **software engineer** is an **end user** who uses the app's functionality and interacts with its plugins in the course of writing and documenting code. This user is more likely to embed documentation in the code files they produce, and create rough drafts of conceptual pages in collaboration with a **technical writer** or _technical editor_. | +| Technical writer | The **technical writer** is an **end user** who uses the app's functionality and interacts with its plugins in the course of writing and editing documentation. This user is more likely to produce and customize templates and produce conceptual pages to supplement documentation embedded in code files. | diff --git a/docs/overview/versioning-policy.md b/docs/overview/versioning-policy.md index a44a6a932d..c38ba404b5 100644 --- a/docs/overview/versioning-policy.md +++ b/docs/overview/versioning-policy.md @@ -53,7 +53,7 @@ bound to the regular cadence and are instead released whenever needed. ## Next Release Line -Release cadence: Weekly +Release cadence: Weekly, specifically on Tuesdays. The next release line is a weekly release of the project. Consuming these releases gives you early access to upcoming functionality in Backstage. There diff --git a/docs/plugins/integrating-plugin-into-software-catalog.md b/docs/plugins/integrating-plugin-into-software-catalog.md index a44e6d01c0..1ec3df69cf 100644 --- a/docs/plugins/integrating-plugin-into-software-catalog.md +++ b/docs/plugins/integrating-plugin-into-software-catalog.md @@ -37,7 +37,7 @@ You can access the currently selected entity using the backstage api import { useEntity } from '@backstage/plugin-catalog-react'; export const MyPluginEntityContent = () => { - const { entity, loading, error, refresh } = useEntity(); + const entity = useEntity(); // Do something with the entity data... }; diff --git a/docs/plugins/internationalization.md b/docs/plugins/internationalization.md new file mode 100644 index 0000000000..eac6ad85e2 --- /dev/null +++ b/docs/plugins/internationalization.md @@ -0,0 +1,61 @@ +--- +id: internationalization +title: Internationalization (Experimental) +description: Documentation on adding internationalization to the plugin +--- + +## Overview + +The Backstage core function provides internationalization for plugins + +## For a plugin developer + +When you are creating your plugin, you have the possibility to use `createTranslationRef` to define all messages for your plugin. For example + +```typescript jsx +import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; + +/** @alpha */ +export const userSettingsTranslationRef = createTranslationRef({ + id: 'user-settings', + messages: { + language: 'Language', + change_the_language: 'Change the language', + }, +}); +``` + +And the using this messages in your components like: + +```typescript jsx +const t = useTranslationRef(userSettingsTranslationRef); + +return ( + +); +``` + +## For an application developer overwrite plugin messages + +```diff typescript jsx +const app = createApp({ ++ __experimentalI18n: { ++ supportedLanguages: ['zh', 'en'], ++ messages: [ ++ createTranslationResource({ ++ ref: userSettingsTranslationRef, ++ messages: { ++ zh: { ++ select_lng: '选择中文-app', ++ }, ++ }, ++ }), ++ ], ++ }, + ... +}) +``` diff --git a/docs/publishing.md b/docs/publishing.md index 12e4878d5a..be94406a40 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -13,11 +13,55 @@ npm. Releases are handled by changesets and trigger whenever the "Version Packages" PR is merged. This is typically done every Tuesday around noon CET. +## Next Line Release Process + +- PR Checks: Ensure there are no outstanding PRs pending to be merged for this version. If there are any, reach out to maintainers and relevant owners of the affected code reminding them of the deadline for the release. +- [optional] Lock main branch + - Lock the main branch to prevent any new merges. + - Note: Admin rights are required to lock the branch. If you lack the necessary permissions, contact a core maintainer to perform this action on your behalf. +- Check [`Version Packages (next)` Pull Request](https://github.com/backstage/backstage/pulls?q=is%3Aopen+is%3Apr+in%3Atitle+%22Version+Packages+%28next%29%22) + - Verify the version we are shipping is correct, by looking at the version packages PR title. It should be "Version Packages (next)" + - Check [`.changeset/pre.json`](https://github.com/backstage/backstage/blob/master/.changeset/pre.json) if the `mode` is set to `pre`. If you encounter `mode: "exit"` or if it's not defined, it indicates a mainline release. +- Verify that there are no active/unfinished `sync_version-packages` actions running (https://github.com/backstage/backstage/actions/workflows/sync_version-packages.yml) + - Locking the main branch will prevent new ones to be created, but be sure to check for running actions again after unlocking, since it may cause pending auto-merge PRs to be merged. +- Check [`Version Packages (next)` Pull Request](https://github.com/backstage/backstage/pulls?q=is%3Aopen+is%3Apr+in%3Atitle+%22Version+Packages+%28next%29%22) for sufficient approval to be merged + - Check generated `changelog` in the changed files of the pull request under `docs/releases` to see if there are any unexpected major bumps e.g. by searching the file + - Review & approve changes + - Reach out to core maintainer to merge the pull request + - Heads-up: The microsite building step can be skipped as long as the `prettier` task passes & everything else looks green + +Merging the `Version Packages (next)` Pull Request will trigger the deployment workflows. Follow along the [deployment workflow](https://github.com/backstage/backstage/actions/workflows/deploy_packages.yml). If you notice flakiness (e.g. if the build is flaky or if the release step runs into an error with releasing to npm) just restart the workflow. + +Congratulations on the release! There should be now a post in the [`#announcements` channel](https://discord.com/channels/687207715902193673/705123584468582400) in Discord linking to the release tag - check if links & tag look as expected. Finally unlock the main branch again. Merging PRs in master directly after release should be done with caution as it potential complicates fixing issues introduced in the release. + +## Switching Release Modes + +- To enter pre-release mode: `yarn changeset pre enter next` & create PR + merge changes +- To exit pre-release mode: `yarn changeset pre exit` & create PR + merge changes + - Has to be done before the mainline release +- It's not time critical; Affects the next release happening + ## Emergency Release Process **This emergency release process is intended only for the Backstage maintainers.** +Given one or more PRs towards master that we want to create a patch release for, run the following script from the repo root: + +```bash +./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. + +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. + +If the above process fails, you can fall back to the manual process documented below. + +### Old Process + +This is the old and manual process that we used before the patch script, provided here as a reference: + For this example we will be using the `@backstage/plugin-foo` package as an example and assume that it is currently version `6.5.0` in the master branch. diff --git a/docs/releases/v1.16.0.md b/docs/releases/v1.16.0.md index 5435d15994..37a458cc32 100644 --- a/docs/releases/v1.16.0.md +++ b/docs/releases/v1.16.0.md @@ -84,4 +84,4 @@ Below you can find a list of links and references to help you learn about and st - [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.16.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://mailchi.mp/spotify/backstage-community) if you want to be informed about what is happening in the world of Backstage. +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.17.0-changelog.md b/docs/releases/v1.17.0-changelog.md new file mode 100644 index 0000000000..a67226ca9e --- /dev/null +++ b/docs/releases/v1.17.0-changelog.md @@ -0,0 +1,2999 @@ +# Release v1.17.0 + +## @backstage/backend-app-api@0.5.0 + +### Minor Changes + +- b9c57a4f857e: **BREAKING**: Renamed `configServiceFactory` to `rootConfigServiceFactory`. +- a6d7983f349c: **BREAKING**: Removed the `services` option from `createBackend`. Service factories are now `BackendFeature`s and should be installed with `backend.add(...)` instead. The following should be migrated: + + ```ts + const backend = createBackend({ services: [myCustomServiceFactory] }); + ``` + + To instead pass the service factory via `backend.add(...)`: + + ```ts + const backend = createBackend(); + backend.add(customRootLoggerServiceFactory); + ``` + +### Patch Changes + +- e65c4896f755: Do not throw in backend.stop, if start failed +- c7aa4ff1793c: Allow modules to register extension points. +- 57a10c6c69cc: Add validation to make sure that extension points do not cross plugin boundaries. +- cc9256a33bcc: Added new experimental `featureDiscoveryServiceFactory`, available as an `/alpha` export. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config-loader@1.4.0 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/cli-node@0.1.3 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/backend-tasks@0.5.5 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/backend-defaults@0.2.0 + +### Minor Changes + +- d008aefef808: **BREAKING**: Removing shared environments concept from the new experimental backend system. +- a6d7983f349c: **BREAKING**: Removed the `services` option from `createBackend`. Service factories are now `BackendFeature`s and should be installed with `backend.add(...)` instead. The following should be migrated: + + ```ts + const backend = createBackend({ services: [myCustomServiceFactory] }); + ``` + + To instead pass the service factory via `backend.add(...)`: + + ```ts + const backend = createBackend(); + backend.add(customRootLoggerServiceFactory); + ``` + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-app-api@0.5.0 + - @backstage/backend-plugin-api@0.6.0 + +## @backstage/backend-plugin-api@0.6.0 + +### Minor Changes + +- c49785f00cab: **BREAKING**: It is no longer possible to declare options as being required with `createServiceFactory`. +- 629cbd194a87: **BREAKING**: Renamed `coreServices.config` to `coreServices.rootConfig`. +- 51987dbdaf87: **BREAKING**: Removed the ability to define options for plugins and modules. Existing options should be migrated to instead use either static configuration or extension points. +- d008aefef808: **BREAKING**: Removing shared environments concept from the new experimental backend system. + +### Patch Changes + +- c7aa4ff1793c: Allow modules to register extension points. + +- cc9256a33bcc: Added new experimental `featureDiscoveryServiceRef`, available as an `/alpha` export. + +- a6d7983f349c: **BREAKING**: Removed the `services` option from `createBackend`. Service factories are now `BackendFeature`s and should be installed with `backend.add(...)` instead. The following should be migrated: + + ```ts + const backend = createBackend({ services: [myCustomServiceFactory] }); + ``` + + To instead pass the service factory via `backend.add(...)`: + + ```ts + const backend = createBackend(); + backend.add(customRootLoggerServiceFactory); + ``` + +- Updated dependencies + - @backstage/plugin-auth-node@0.2.17 + - @backstage/backend-tasks@0.5.5 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/backend-test-utils@0.2.0 + +### Minor Changes + +- b9c57a4f857e: **BREAKING**: Renamed `mockServices.config` to `mockServices.rootConfig`. +- a6d7983f349c: **BREAKING**: Removed the `services` option from `createBackend`. Service factories are now `BackendFeature`s and should be installed with `backend.add(...)` instead. The following should be migrated: + + ```ts + const backend = createBackend({ services: [myCustomServiceFactory] }); + ``` + + To instead pass the service factory via `backend.add(...)`: + + ```ts + const backend = createBackend(); + backend.add(customRootLoggerServiceFactory); + ``` + +### Patch Changes + +- ae9304818136: Add needed constants and constructs to support PostgreSQL version 14 as test database +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-app-api@0.5.0 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + +## @backstage/config-loader@1.4.0 + +### Minor Changes + +- 2f1859585998: Loading invalid TypeScript configuration schemas will now throw an error rather than silently being ignored. + + In particular this includes defining any additional types other than `Config` in the schema file, or use of unsupported types such as `Record` or `Partial`. + +- cd514545d1d0: Adds a new `deepVisibility` schema keyword that sets child visibility recursively to the defined value, respecting preexisting values or child `deepVisibility`. + + Example usage: + + ```ts + export interface Config { + /** + * Enforces a default of `secret` instead of `backend` for this object. + * @deepVisibility secret + */ + mySecretProperty: { + type: 'object'; + properties: { + secretValue: { + type: 'string'; + }; + + verySecretProperty: { + type: 'string'; + }; + }; + }; + } + ``` + + Example of a schema that would not be allowed: + + ```ts + export interface Config { + /** + * Set the top level property to secret, enforcing a default of `secret` instead of `backend` for this object. + * @deepVisibility secret + */ + mySecretProperty: { + type: 'object'; + properties: { + frontendUrl: { + /** + * We can NOT override the visibility to reveal a property to the front end. + * @visibility frontend + */ + type: 'string'; + }; + + verySecretProperty: { + type: 'string'; + }; + }; + }; + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/integration@1.6.0 + +### Minor Changes + +- 443afcf7f567: Added `buildGerritGitilesArchiveUrl()` to construct a Gitiles URL to download an archive. + Gitiles URL that uses an authenticated prefix (`/a/`) can now be parsed by the integration. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-app-node@0.1.0 + +### Minor Changes + +- 9fbe95ef6503: Added the `app` plugin node library, initially providing an extension point that can be used to configure a static fallback handler. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0 + +## @backstage/plugin-catalog-backend@1.12.0 + +### Minor Changes + +- b8cccd8ee858: Support configuring applicable kinds for `AnnotateScmSlugEntityProcessor` +- f32252cdf631: Added OpenTelemetry spans for catalog processing +- ebeb77586975: Now performs request validation based on OpenAPI schema through `@backstage/backend-openapi-utils`. Error responses for invalid input, like `"a"` instead of a number, may have changed. + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- b8d6b22acd57: Internal refactor for load test +- Updated dependencies + - @backstage/plugin-search-backend-module-catalog@0.1.4 + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/plugin-events-node@0.2.9 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.0 + +### Minor Changes + +- 290eff6692aa: Added GCP catalog plugin with GKE provider + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/backend-tasks@0.5.5 + - @backstage/config@1.0.8 + - @backstage/plugin-kubernetes-common@0.6.5 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.2.0 + +### Minor Changes + +- 5156a94c2e2a: **BREAKING**: Fixing typo in exported module. You will have to rename the import to the correct spelling. `UnprocessedEntites` -> `UnprocessedEntities` + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/catalog-model@1.4.1 + +## @backstage/plugin-kubernetes@0.10.0 + +### Minor Changes + +- 4b6d41737aad: **BREAKING**: Fixed typos in exported module attributes. Many users may see no changes, but if you have customized the plugin output, you may have to rename the following: + + - Component attribute: `` to `` + - Interface attribute: `DetectedError.occuranceCount` to `DetectedError.occurrenceCount`. + +### Patch Changes + +- 562e34d2693f: Fix container memory limits +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-kubernetes-common@0.6.5 + +## @backstage/plugin-linguist-backend@0.4.0 + +### Minor Changes + +- d440f1dd0e72: Adds a processor to the linguist backend which can automatically add language tags to entities +- 0896d85d5ab5: **BREAKING**: Removed the alpha export plugin options from exported `linguistPlugin()` (used by the new backend system) in favour of static config. + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- ca5e591cb86a: Fixed bug in LinguistBackendClient.ts file where if the linguistJsOptions is specified and sent over to the linguist-js package it would get changed (another attribute would be added) causing future entities of the batch to fail with an error +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-proxy-backend@0.3.0 + +### Minor Changes + +- 7daf65bfcfa1: Defining proxy endpoints directly under the root `proxy` configuration key is deprecated. Endpoints should now be declared under `proxy.endpoints` instead. The `skipInvalidProxies` and `reviveConsumedRequestBodies` can now also be configured through static configuration. + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/config@1.0.8 + +## @backstage/plugin-scaffolder-backend@1.16.0 + +### Minor Changes + +- e514aac3eac0: Introduce `each` property on action steps, allowing them to be ran repeatedly. + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` + +- 349611126ae2: Removed the options from the alpha `scaffolderPlugin` export. To extend the scaffolder plugin you instead now use the available extension points, `scaffolderActionsExtensionPoint`, `scaffolderTaskBrokerExtensionPoint`, and `scaffolderTemplatingExtensionPoint`. + +- 33c76caef72a: Added examples for the `fs:delete` and `fs:rename` actions + +- e07a4914f621: Deprecated the following type exports, which have been moved to `@backstage/plugin-scaffolder-node` instead: + + - `TemplateFilter` + - `TemplateGlobal` + - `TaskStatus` + - `TaskCompletionState` + - `SerializedTask` + - `TaskEventType` + - `SerializedTaskEvent` + - `TaskBrokerDispatchResult` + - `TaskBrokerDispatchOptions` + - `TaskContext` + - `TaskBroker` + +- 0b1d775be05b: Adds examples to a few scaffolder actions. + +- 88bc6e27a213: The `concurrentTasksLimit` option can now be configured via static configuration as well. Setting it to 0 will now also disable the task worker. + +- 0f873325068d: Add examples for `github:repo:create` and `github:repo:push` scaffolder actions. + +- 5c28ebc79fd6: Updated dependency `esbuild` to `^0.19.0`. + +- d3b31a791eb1: Deprecated `executeShellCommand`, `RunCommandOptions`, and `fetchContents` from `@backstage/plugin-scaffolder-backend`, since they are useful for Scaffolder modules (who should not be importing from the plugin package itself). You should now import these from `@backstage/plugin-scaffolder-backend-node` instead. `RunCommandOptions` was renamed in the Node package as `ExecuteShellCommandOptions`, for consistency. + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-catalog-backend@1.12.0 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-scaffolder-node@0.2.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-scaffolder-common@1.4.0 + +### Minor Changes + +- e514aac3eac0: Introduce `each` property on action steps, allowing them to be ran repeatedly. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-scaffolder-node@0.2.0 + +### Minor Changes + +- e514aac3eac0: Introduce `each` property on action steps, allowing them to be ran repeatedly. + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- e07a4914f621: Added several new types that were moved from `@backstage/plugin-scaffolder-backend`. +- 349611126ae2: Added two new alpha extension points, `scaffolderTaskBrokerExtensionPoint` and `scaffolderTemplatingExtensionPoint`. +- 0b1d775be05b: Export `TemplateExample` from the `createTemplateAction` type. +- d3b31a791eb1: Deprecated `executeShellCommand`, `RunCommandOptions`, and `fetchContents` from `@backstage/plugin-scaffolder-backend`, since they are useful for Scaffolder modules (who should not be importing from the plugin package itself). You should now import these from `@backstage/plugin-scaffolder-backend-node` instead. `RunCommandOptions` was renamed in the Node package as `ExecuteShellCommandOptions`, for consistency. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/integration@1.6.0 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-search-backend@1.4.0 + +### Minor Changes + +- ebeb77586975: Now performs request validation based on OpenAPI schema through `@backstage/backend-openapi-utils`. Error responses for invalid input, like `"a"` instead of a number, may have changed. + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- 951ab6c9db58: Add missing `configSchema` to package.json +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/plugin-search-backend-node@1.2.4 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-todo-backend@0.2.0 + +### Minor Changes + +- ebeb77586975: Now performs request validation based on OpenAPI schema through `@backstage/backend-openapi-utils`. Error responses for invalid input, like `"a"` instead of a number, may have changed. + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- ad3fb5401729: Fixed a bug where filter queries from the frontend would always fail validation. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/integration@1.6.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/app-defaults@1.4.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.9.1 + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/theme@0.4.1 + - @backstage/plugin-permission-react@0.4.14 + +## @backstage/backend-common@0.19.2 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` + +- 443afcf7f567: To improve performance, `GerritUrlReader.readTree()` now uses Gitiles to fetch an archive instead of cloning the repository. + If `gitilesBaseUrl` is not configured, `readTree` still uses Git to clone the repository. + + Added `stripFirstDirectory` option to `ReadTreeResponseFactory.fromTarArchive()`, allowing to disable stripping first directory + for `tar` archives. + +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. + +- Updated dependencies + - @backstage/backend-app-api@0.5.0 + - @backstage/config-loader@1.4.0 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/integration@1.6.0 + - @backstage/integration-aws-node@0.1.5 + - @backstage/backend-dev-utils@0.1.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/backend-openapi-utils@0.0.3 + +### Patch Changes + +- ebeb77586975: Add a new `createRouter` method for generating an `express` router that validates against your spec. Also fixes a bug with the query parameters type resolution. +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/errors@1.2.1 + +## @backstage/backend-tasks@0.5.5 + +### Patch Changes + +- dfd1b6b2fc33: Make `readTaskScheduleDefinitionFromConfig` properly handle bad inputs +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/cli@0.22.10 + +### Patch Changes + +- 3f67cefb4780: Reload the frontend when app config changes +- cebbf8a27f3c: Enable to print the config schema not merged with the `--no-merge` flag +- 5c28ebc79fd6: Updated dependency `esbuild` to `^0.19.0`. +- 971bdd6a4732: Bumped internal `nodemon` dependency. +- Updated dependencies + - @backstage/config-loader@1.4.0 + - @backstage/cli-node@0.1.3 + - @backstage/integration@1.6.0 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/eslint-plugin@0.1.3 + - @backstage/release-manifests@0.0.9 + - @backstage/types@1.1.0 + +## @backstage/cli-node@0.1.3 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/core-app-api@1.9.1 + +### Patch Changes + +- 9ae4e7e63836: Fixed a bug that could cause `navigate` analytics events to be misattributed to the plugin mounted on the root route (e.g. the `home` plugin at `/`) when the route that was navigated to wasn't associated with a routable extension. +- Updated dependencies + - @backstage/core-plugin-api@1.5.3 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## @backstage/core-components@0.13.4 + +### Patch Changes + +- 3d86be999fdf: Prefer simple `theme.spacing` without string interpolation +- Updated dependencies + - @backstage/core-plugin-api@1.5.3 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/version-bridge@1.0.4 + +## @backstage/create-app@0.5.4 + +### Patch Changes + +- b441642fbe0d: Bumped create-app version. +- 572abc7edf55: Bumped create-app version. +- 74f77f151a96: Bumped create-app version. +- 5cc0ac5ef3d1: Bump to a newer version of the `concurrently` library +- 46c9a798e41d: Updated the `app-config.yaml` template to use `proxy.endpoints`. +- 971bdd6a4732: Bumped internal `nodemon` dependency. +- Updated dependencies + - @backstage/cli-common@0.1.12 + +## @backstage/dev-utils@1.0.18 + +### Patch Changes + +- 254ad469f053: Removed deprecated calls to `app.getProvider()` and `app.getRouter()` in `DevAppBuilder` +- Updated dependencies + - @backstage/core-app-api@1.9.1 + - @backstage/integration-react@1.1.16 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/app-defaults@1.4.2 + - @backstage/core-plugin-api@1.5.3 + - @backstage/test-utils@1.4.2 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/integration-react@1.1.16 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/integration@1.6.0 + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## @backstage/repo-tools@0.3.3 + +### Patch Changes + +- 75702e85862a: Bumped `@microsoft/api-extractor` dependency to `^7.36.4`, and `@microsoft/api-documenter` to `^7.22.33`. +- 1f3337ebc707: Introducing a new, experimental command `backstage-repo-tools generate-catalog-info`, which can be used to create standardized `catalog-info.yaml` files for each Backstage package in a Backstage monorepo. It can also be used to automatically fix existing `catalog-info.yaml` files with the correct metadata (including `metadata.name`, `metadata.title`, and `metadata.description` introspected from the package's `package.json`, as well as `spec.owner` introspected from `CODEOWNERS`), e.g. in a post-commit hook. +- ebeb77586975: Update `schema openapi generate` command to now create a default router that can be imported and used directly. +- Updated dependencies + - @backstage/cli-node@0.1.3 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + +## @techdocs/cli@1.4.5 + +### Patch Changes + +- 971bdd6a4732: Bumped internal `nodemon` dependency. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-techdocs-node@1.7.4 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + +## @backstage/test-utils@1.4.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.9.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.14 + +## @backstage/plugin-adr@0.6.4 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/integration-react@1.1.16 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/plugin-search-react@1.6.4 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-adr-common@0.2.12 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-adr-backend@0.3.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/integration@1.6.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-adr-common@0.2.12 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-adr-common@0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.6.0 + - @backstage/catalog-model@1.4.1 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-airbrake@0.3.21 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/dev-utils@1.0.18 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/test-utils@1.4.2 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-airbrake-backend@0.2.21 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/config@1.0.8 + +## @backstage/plugin-allure@0.1.37 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-analytics-module-ga@0.1.32 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## @backstage/plugin-analytics-module-ga4@0.1.3 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## @backstage/plugin-analytics-module-newrelic-browser@0.0.1 + +### Patch Changes + +- ec7357258853: Introduced the New Relic Browser analytics module. Check out the plugins [README.md](https://github.com/backstage/backstage/tree/master/plugins/analytics-module-newrelic-browser) for more details! +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/config@1.0.8 + +## @backstage/plugin-apache-airflow@0.2.14 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + +## @backstage/plugin-api-docs@0.9.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog@1.12.1 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-apollo-explorer@0.1.14 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/theme@0.4.1 + +## @backstage/plugin-app-backend@0.3.48 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- d564ad142b17: Migrated the alpha `appBackend` export to use static configuration and extension points rather than accepting options. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config-loader@1.4.0 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-app-node@0.1.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + +## @backstage/plugin-auth-backend@0.18.6 + +### Patch Changes + +- 16452cd007ae: Updated `frameHandler` to return `undefined` when using the redirect flow instead of returning `postMessageReponse` which was causing errors +- 9dad4b0e61bd: Updated config schema to match what was being used in code +- bb70a9c3886a: Add frontend visibility to provider objects in `auth` config. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-auth-node@0.2.17 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-azure-devops@0.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-devops-backend@0.3.27 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/config@1.0.8 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-sites@0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-azure-sites-backend@0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config@1.0.8 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-badges@0.2.45 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-badges-backend@0.2.3 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-bazaar@0.2.12 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/cli@0.22.10 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog@1.12.1 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-bazaar-backend@0.2.11 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-bitbucket-cloud-common@0.2.9 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/integration@1.6.0 + +## @backstage/plugin-bitrise@0.1.48 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-catalog@1.12.1 + +### Patch Changes + +- 3d86be999fdf: Prefer simple `theme.spacing` without string interpolation +- 136cea792bd4: Fixed an issue causing `EntitySwitch` to unmount its children once entity refresh was invoked +- Updated dependencies + - @backstage/integration-react@1.1.16 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/core-plugin-api@1.5.3 + - @backstage/plugin-search-react@1.6.4 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-catalog-backend-module-aws@0.2.3 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- b4222908b0c3: Added option to configure AWS `accountId` in `AwsS3EntityProvider` +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/integration-aws-node@0.1.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-kubernetes-common@0.6.5 + +## @backstage/plugin-catalog-backend-module-azure@0.1.19 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.2.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.9 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/integration@1.6.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.1.15 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.9 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/plugin-events-node@0.2.9 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-common@1.0.15 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.13 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.16 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-catalog-backend-module-github@0.3.3 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- 81c231c9c9ee: Fixed a bug where the visibility filter was case sensitive and casting was inconsistent. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-catalog-backend@1.12.0 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/plugin-events-node@0.2.9 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + +## @backstage/plugin-catalog-backend-module-gitlab@0.2.4 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- e6c721439f37: Added option to skip forked repos in GitlabDiscoveryEntityProvider +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- 2fe1f5973ff7: Filter Gitlab archived projects through APIs +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.1 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- e2b6396a1274: Export new alpha `incrementalIngestionProvidersExtensionPoint` for registering incremental providers, rather than the providers being passed as options to the backend module. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-catalog-backend@1.12.0 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/plugin-events-node@0.2.9 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.15 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.7 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- fb93323201bf: The alpha `catalogModuleMicrosoftGraphOrgEntityProvider` export no longer accepts options. Transformers are now instead configured via the `microsoftGraphOrgEntityProviderTransformExtensionPoint`. +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-common@1.0.15 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-catalog-backend@1.12.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/integration@1.6.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.5 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-graph@0.2.33 + +### Patch Changes + +- 64ee2c0c7ca5: Propagate entity spec to EntityNode so that spec info such as type can be used for graph node customization +- 62dc7a2b1ad1: Added maximum depth parameter to the catalogGraphParams in CatalogGraphCard. +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-import@0.9.11 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.16 + - @backstage/integration@1.6.0 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.15 + +## @backstage/plugin-catalog-node@1.4.1 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + +## @backstage/plugin-catalog-react@1.8.1 + +### Patch Changes + +- aa3feedce10a: Allow specifying screen size when catalog filters are hidden in drawer +- Updated dependencies + - @backstage/integration@1.6.0 + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.14 + +## @backstage/plugin-catalog-unprocessed-entities@0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-cicd-statistics@0.1.23 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.17 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/plugin-cicd-statistics@0.1.23 + +## @backstage/plugin-circleci@0.3.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-cloudbuild@0.3.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-code-climate@0.1.21 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-code-coverage@0.2.14 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-code-coverage-backend@0.2.14 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/integration@1.6.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-codescene@0.1.16 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-config-schema@0.1.44 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-cost-insights@0.12.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-cost-insights-common@0.1.1 + +## @backstage/plugin-devtools@0.1.3 + +### Patch Changes + +- 48b6a04ad045: Fix readme typo +- Updated dependencies + - @backstage/plugin-devtools-common@0.1.3 + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-react@0.4.14 + +## @backstage/plugin-devtools-backend@0.1.3 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` + +- 12a8c94eda8d: Add package repository and homepage metadata + +- 2b4f77a4e900: Add DevTools configuration to enable dependency listing to be filtered with custom prefixes. For instance, in your `app-config.yaml`: + + ```yaml + devTools: + info: + packagePrefixes: + - @backstage/ + - @roadiehq/backstage- + - @spotify/backstage- + ``` + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config-loader@1.4.0 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-devtools-common@0.1.3 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-devtools-common@0.1.3 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-dynatrace@7.0.1 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-entity-feedback@0.2.4 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/plugin-entity-feedback-common@0.1.2 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-entity-feedback-backend@0.1.6 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-entity-feedback-common@0.1.2 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + +## @backstage/plugin-entity-feedback-common@0.1.2 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata + +## @backstage/plugin-entity-validation@0.1.6 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.15 + +## @backstage/plugin-events-backend@0.2.9 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-events-node@0.2.9 + - @backstage/config@1.0.8 + +## @backstage/plugin-events-backend-module-aws-sqs@0.2.3 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-events-node@0.2.9 + - @backstage/backend-tasks@0.5.5 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + +## @backstage/plugin-events-backend-module-azure@0.1.10 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-events-node@0.2.9 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.1.10 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-events-node@0.2.9 + +## @backstage/plugin-events-backend-module-gerrit@0.1.10 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-events-node@0.2.9 + +## @backstage/plugin-events-backend-module-github@0.1.10 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-events-node@0.2.9 + - @backstage/config@1.0.8 + +## @backstage/plugin-events-backend-module-gitlab@0.1.10 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-events-node@0.2.9 + - @backstage/config@1.0.8 + +## @backstage/plugin-events-backend-test-utils@0.1.10 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/plugin-events-node@0.2.9 + +## @backstage/plugin-events-node@0.2.9 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0 + +## @backstage/plugin-explore@0.4.7 + +### Patch Changes + +- 41a26248c91c: Support `material-ui` overrides in plugin-explore components +- 83467b0534ee: Don't put "?" in URL if no query parameters. +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/plugin-search-react@1.6.4 + - @backstage/plugin-explore-react@0.0.30 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-explore-backend@0.0.10 + +### Patch Changes + +- eda2a699f40d: Moved the config example from the "Tools as Code" section to the "Tools as Config" section of the README +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/plugin-search-backend-module-explore@0.1.4 + - @backstage/backend-common@0.19.2 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-firehydrant@0.2.5 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-fossa@0.2.53 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-gcalendar@0.3.17 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-gcp-projects@0.3.40 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/theme@0.4.1 + +## @backstage/plugin-git-release-manager@0.3.34 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/integration@1.6.0 + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/theme@0.4.1 + +## @backstage/plugin-github-actions@0.6.2 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.16 + - @backstage/integration@1.6.0 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-github-deployments@0.1.52 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/integration-react@1.1.16 + - @backstage/integration@1.6.0 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-github-issues@0.2.10 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/integration@1.6.0 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-github-pull-requests-board@0.1.15 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.6.0 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-gitops-profiles@0.3.39 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## @backstage/plugin-gocd@0.1.27 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-graphiql@0.2.53 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/theme@0.4.1 + +## @backstage/plugin-graphql-backend@0.1.38 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-graphql@0.3.22 + +## @backstage/plugin-graphql-voyager@0.1.6 + +### Patch Changes + +- bb1e1c2b26cc: Fix typo in install instructions. +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/theme@0.4.1 + +## @backstage/plugin-home@0.5.5 + +### Patch Changes + +- a559ff68de7e: Now, user can provide a language (optional) as input to receive a greeting in that specific language. Example: `` +- 6743d3917a52: Make sure the widget name is never empty in the `AddWidgetDialog`. If the title was set to "", the entry would contain an empty string. Use the name as a fallback +- Updated dependencies + - @backstage/plugin-home-react@0.1.2 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## @backstage/plugin-home-react@0.1.2 + +### Patch Changes + +- bf67dce73174: Make `title` optional when defining the `createCardExtension` +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + +## @backstage/plugin-ilert@0.2.10 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-jenkins@0.8.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-jenkins-common@0.1.18 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-jenkins-backend@0.2.3 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-jenkins-common@0.1.18 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-jenkins-common@0.1.18 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-kafka@0.3.21 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## @backstage/plugin-kafka-backend@0.2.41 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-kubernetes-backend@0.11.3 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- bbf4e9c894b5: Fixed a bug where the proxy was not rewriting WebSocket request paths properly. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/integration-aws-node@0.1.5 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-kubernetes-common@0.6.5 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-lighthouse@0.4.6 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-lighthouse-common@0.1.2 + +## @backstage/plugin-lighthouse-backend@0.2.4 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-lighthouse-common@0.1.2 + +## @backstage/plugin-linguist@0.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-linguist-common@0.1.1 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- d440f1dd0e72: Exported new LanguageType type alias + +## @backstage/plugin-microsoft-calendar@0.1.6 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- eea2922e749a: README update - example of apiRef definition and fixed component name +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-newrelic@0.3.39 + +### Patch Changes + +- 2167b7eab09b: The newrelic plugin now supports pagination when retrieving results from newrelic. It will no longer truncate results. To see all applications, the link header will need to be allowed through the proxy (see the newrelic plugin readme). +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/theme@0.4.1 + +## @backstage/plugin-newrelic-dashboard@0.2.14 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-nomad@0.1.2 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## @backstage/plugin-nomad-backend@0.1.2 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-octopus-deploy@0.2.3 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-org@0.6.11 + +### Patch Changes + +- 50331203e2a2: Fixed bug in MembersListCard component where the url for the members was static which was breaking the component when catalog route was changed +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-org-react@0.1.10 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-pagerduty@0.6.2 + +### Patch Changes + +- 5003fc966741: Add new `disableChangeEvents` prop to `EntityPagerDutyCard` to hide the Change Events tab and disable fetching of change events for the PagerDuty service. +- Updated dependencies + - @backstage/plugin-home-react@0.1.2 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-periskop@0.1.19 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-periskop-backend@0.1.19 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/config@1.0.8 + +## @backstage/plugin-permission-backend@0.5.23 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- 0d6ddc62d8da: Refactor backend plugin creation parameter from callback to object. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-permission-node@0.7.11 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-playlist@0.1.13 + +### Patch Changes + +- d1e0588324d1: Displaying an alert popup each time the Playlist is created or deleted +- 12a8c94eda8d: Add package repository and homepage metadata +- 8a997547f02a: Adding alert popup whenever user delete the entities from playlist entity table +- Updated dependencies + - @backstage/plugin-playlist-common@0.1.9 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/plugin-search-react@1.6.4 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.14 + +## @backstage/plugin-playlist-backend@0.3.4 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-playlist-common@0.1.9 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-playlist-common@0.1.9 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-puppetdb@0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-rollbar@0.4.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-rollbar-backend@0.1.45 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config@1.0.8 + +## @backstage/plugin-scaffolder@1.14.2 + +### Patch Changes + +- 8a0490fb669e: Fix the get entities query in the `MyGroupsPicker` to query the `kind=Group` entities. +- Updated dependencies + - @backstage/integration-react@1.1.16 + - @backstage/integration@1.6.0 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/plugin-scaffolder-react@1.5.2 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-react@0.4.14 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.1 + +### Patch Changes + +- 0a9b3b14e904: Added example for the `confluence:transform:markdown` that will show in the installed actions list +- 12a8c94eda8d: Add package repository and homepage metadata +- 0d347efc8f18: Use `fetchContents` directly instead of a `fetchPlainAction` +- c186c631b429: Import helpers from the node package instead of the backend package +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-scaffolder-node@0.2.0 + - @backstage/integration@1.6.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.24 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- c186c631b429: Import helpers from the node package instead of the backend package +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-scaffolder-node@0.2.0 + - @backstage/integration@1.6.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.2.0 + - @backstage/integration@1.6.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.17 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- c186c631b429: Import helpers from the node package instead of the backend package +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-scaffolder-node@0.2.0 + - @backstage/integration@1.6.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.8 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.2.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.21 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.2.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + +## @backstage/plugin-scaffolder-react@1.5.2 + +### Patch Changes + +- ba9ee98a37bd: Fixed bug in Workflow component by passing a prop `templateName` down to Stepper component. +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## @backstage/plugin-search@1.3.4 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/plugin-search-react@1.6.4 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-backend-module-catalog@0.1.4 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 29f77f923c71: Ensure that all services are dependency injected into the module instead of taken from options +- 12a8c94eda8d: Add package repository and homepage metadata +- ec1032129e47: Breaking change in the alpha export `searchModuleCatalogCollator`: Moved collator settings from module options into app-config. You are now expected to set up the catalog collator under the `search.collators.catalog` configuration key. There is also a new `catalogCollatorExtensionPoint` extension point for the module, wherein you can set custom transformers. +- d4f19a16bd52: Add User Entity email to the search index so that users can be found by their email. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-search-backend-node@1.2.4 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-backend-module-elasticsearch@1.3.3 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` + +- 12a8c94eda8d: Add package repository and homepage metadata + +- 631eb3816b48: Index templates can now be configured through configuration under the `search.elasticsearch.indexTemplates`. In addition, the `ElasticSearchSearchEngine.fromConfig` now also accepts a `LoggerService` as the `logger` option as well as a new `translator` option. + + The alpha `searchModuleElasticsearchEngine` export no longer accepts options and a new alpha `elasticsearchTranslatorExtensionPoint` export has been added which lets you customize the query translator. + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-search-backend-node@1.2.4 + - @backstage/integration-aws-node@0.1.5 + - @backstage/config@1.0.8 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-backend-module-explore@0.1.4 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 29f77f923c71: Ensure that all services are dependency injected into the module instead of taken from options +- 12a8c94eda8d: Add package repository and homepage metadata +- 6694e79ab396: Breaking change for the alpha export moved `schedule` from module options into app-config for the new backend system. You can now pass in a `TaskScheduleDefinitionConfig` through the `search.collators.explore.schedule` configuration key. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-search-backend-node@1.2.4 + - @backstage/backend-tasks@0.5.5 + - @backstage/config@1.0.8 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-backend-module-pg@0.5.9 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-search-backend-node@1.2.4 + - @backstage/config@1.0.8 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-backend-module-techdocs@0.1.4 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 29f77f923c71: Ensure that all services are dependency injected into the module instead of taken from options +- 12a8c94eda8d: Add package repository and homepage metadata +- 6694e79ab396: **BREAKING:** Moved `schedule` & `collators` settings from module options into app-config for the new backend system. You can now pass in a `TaskScheduleDefinitionConfig` through the `search.collators.techdocs.schedule` configuration key & configure the `TechDocsCollatorFactory` with the key `search.collators.techdocs`. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-search-backend-node@1.2.4 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/backend-tasks@0.5.5 + - @backstage/plugin-techdocs-node@1.7.4 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-backend-node@1.2.4 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- e3e9bc10298b: Fix Lunr search engine highlight by ignoring invalid metadata positions. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-react@1.6.4 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-sentry@0.5.6 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-shortcuts@0.3.13 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- ee52a48b63d5: Limit the use of the duplicate shortcut name when adding a shortcut +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-sonarqube@0.7.2 + +### Patch Changes + +- b2ccddefbdc6: Remove sonarQube card disable class +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-sonarqube-react@0.1.7 + +## @backstage/plugin-sonarqube-backend@0.2.2 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- a24d9df8757c: Added optional `externalBaseUrl` config for setting a different frontend URL +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-splunk-on-call@0.4.10 + +### Patch Changes + +- 3d86be999fdf: Prefer simple `theme.spacing` without string interpolation +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-stack-overflow@0.1.19 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/plugin-home-react@0.1.2 + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/plugin-search-react@1.6.4 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-stack-overflow-backend@0.2.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config@1.0.8 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-stackstorm@0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-tech-insights@0.3.13 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-tech-insights-common@0.2.11 + +## @backstage/plugin-tech-insights-backend@0.5.14 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-tasks@0.5.5 + - @backstage/plugin-tech-insights-node@0.4.6 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-tech-insights-common@0.2.11 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.32 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-tech-insights-node@0.4.6 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-tech-insights-common@0.2.11 + +## @backstage/plugin-tech-insights-node@0.4.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-tasks@0.5.5 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-tech-insights-common@0.2.11 + +## @backstage/plugin-tech-radar@0.6.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/theme@0.4.1 + +## @backstage/plugin-techdocs@1.6.6 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.16 + - @backstage/integration@1.6.0 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/plugin-search-react@1.6.4 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-techdocs-react@1.1.9 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.17 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.9.1 + - @backstage/integration-react@1.1.16 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog@1.12.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/test-utils@1.4.2 + - @backstage/plugin-search-react@1.6.4 + - @backstage/plugin-techdocs@1.6.6 + - @backstage/theme@0.4.1 + - @backstage/plugin-techdocs-react@1.1.9 + +## @backstage/plugin-techdocs-backend@1.6.5 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- Updated dependencies + - @backstage/plugin-search-backend-module-techdocs@0.1.4 + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/integration@1.6.0 + - @backstage/plugin-techdocs-node@1.7.4 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-techdocs-module-addons-contrib@1.0.16 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.16 + - @backstage/integration@1.6.0 + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/theme@0.4.1 + - @backstage/plugin-techdocs-react@1.1.9 + +## @backstage/plugin-techdocs-node@1.7.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/integration@1.6.0 + - @backstage/integration-aws-node@0.1.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-techdocs-react@1.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/version-bridge@1.0.4 + +## @backstage/plugin-todo@0.2.23 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-user-settings@0.7.6 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.9.1 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-user-settings-backend@0.1.12 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-vault@0.1.15 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-vault-backend@0.3.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-tasks@0.5.5 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-xcmetrics@0.2.41 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- 3d86be999fdf: Prefer simple `theme.spacing` without string interpolation +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## example-app@0.2.86 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.9.1 + - @backstage/plugin-catalog-graph@0.2.33 + - @backstage/plugin-playlist@0.1.13 + - @backstage/plugin-pagerduty@0.6.2 + - @backstage/plugin-scaffolder@1.14.2 + - @backstage/plugin-explore@0.4.7 + - @backstage/plugin-org@0.6.11 + - @backstage/integration-react@1.1.16 + - @backstage/plugin-microsoft-calendar@0.1.6 + - @backstage/plugin-newrelic-dashboard@0.2.14 + - @backstage/plugin-entity-feedback@0.2.4 + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/plugin-apache-airflow@0.2.14 + - @backstage/plugin-octopus-deploy@0.2.3 + - @backstage/plugin-stack-overflow@0.1.19 + - @backstage/plugin-code-coverage@0.2.14 + - @backstage/plugin-tech-insights@0.3.13 + - @backstage/plugin-dynatrace@7.0.1 + - @backstage/plugin-gcalendar@0.3.17 + - @backstage/plugin-shortcuts@0.3.13 + - @backstage/plugin-airbrake@0.3.21 + - @backstage/plugin-kafka@0.3.21 + - @backstage/plugin-nomad@0.1.2 + - @backstage/plugin-adr@0.6.4 + - @backstage/plugin-newrelic@0.3.39 + - @backstage/cli@0.22.10 + - @backstage/plugin-kubernetes@0.10.0 + - @backstage/core-components@0.13.4 + - @backstage/plugin-home@0.5.5 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/plugin-scaffolder-react@1.5.2 + - @backstage/plugin-devtools@0.1.3 + - @backstage/app-defaults@1.4.2 + - @backstage/core-plugin-api@1.5.3 + - @backstage/plugin-api-docs@0.9.7 + - @backstage/plugin-azure-devops@0.3.3 + - @backstage/plugin-azure-sites@0.1.10 + - @backstage/plugin-badges@0.2.45 + - @backstage/plugin-catalog-import@0.9.11 + - @backstage/plugin-catalog-unprocessed-entities@0.1.2 + - @backstage/plugin-circleci@0.3.21 + - @backstage/plugin-cloudbuild@0.3.21 + - @backstage/plugin-cost-insights@0.12.10 + - @backstage/plugin-gcp-projects@0.3.40 + - @backstage/plugin-github-actions@0.6.2 + - @backstage/plugin-gocd@0.1.27 + - @backstage/plugin-graphiql@0.2.53 + - @backstage/plugin-jenkins@0.8.3 + - @backstage/plugin-lighthouse@0.4.6 + - @backstage/plugin-linguist@0.1.6 + - @backstage/plugin-puppetdb@0.1.4 + - @backstage/plugin-rollbar@0.4.21 + - @backstage/plugin-search@1.3.4 + - @backstage/plugin-search-react@1.6.4 + - @backstage/plugin-sentry@0.5.6 + - @backstage/plugin-stackstorm@0.1.5 + - @backstage/plugin-tech-radar@0.6.7 + - @backstage/plugin-techdocs@1.6.6 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.16 + - @backstage/plugin-todo@0.2.23 + - @backstage/plugin-user-settings@0.7.6 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-react@0.4.14 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-techdocs-react@1.1.9 + - @internal/plugin-catalog-customized@0.0.13 + +## example-backend@0.2.86 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-module-elasticsearch@1.3.3 + - @backstage/plugin-search-backend-module-pg@0.5.9 + - @backstage/plugin-azure-devops-backend@0.3.27 + - @backstage/plugin-kubernetes-backend@0.11.3 + - @backstage/plugin-lighthouse-backend@0.2.4 + - @backstage/plugin-permission-backend@0.5.23 + - @backstage/plugin-scaffolder-backend@1.16.0 + - @backstage/plugin-devtools-backend@0.1.3 + - @backstage/plugin-techdocs-backend@1.6.5 + - @backstage/backend-common@0.19.2 + - @backstage/plugin-catalog-backend@1.12.0 + - @backstage/plugin-badges-backend@0.2.3 + - @backstage/plugin-events-backend@0.2.9 + - @backstage/plugin-search-backend@1.4.0 + - @backstage/plugin-kafka-backend@0.2.41 + - @backstage/plugin-proxy-backend@0.3.0 + - @backstage/plugin-todo-backend@0.2.0 + - @backstage/plugin-app-backend@0.3.48 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.1 + - @backstage/plugin-auth-backend@0.18.6 + - @backstage/plugin-explore-backend@0.0.10 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.17 + - @backstage/plugin-entity-feedback-backend@0.1.6 + - @backstage/plugin-code-coverage-backend@0.2.14 + - @backstage/plugin-search-backend-node@1.2.4 + - @backstage/plugin-linguist-backend@0.4.0 + - @backstage/plugin-playlist-backend@0.3.4 + - @backstage/plugin-jenkins-backend@0.2.3 + - @backstage/plugin-nomad-backend@0.1.2 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/plugin-events-node@0.2.9 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - example-app@0.2.86 + - @backstage/plugin-adr-backend@0.3.6 + - @backstage/plugin-azure-sites-backend@0.1.10 + - @backstage/plugin-graphql-backend@0.1.38 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/plugin-rollbar-backend@0.1.45 + - @backstage/plugin-tech-insights-backend@0.5.14 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.32 + - @backstage/plugin-tech-insights-node@0.4.6 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-common@1.2.5 + +## example-backend-next@0.0.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-module-techdocs@0.1.4 + - @backstage/plugin-search-backend-module-catalog@0.1.4 + - @backstage/plugin-search-backend-module-explore@0.1.4 + - @backstage/plugin-azure-devops-backend@0.3.27 + - @backstage/plugin-kubernetes-backend@0.11.3 + - @backstage/plugin-lighthouse-backend@0.2.4 + - @backstage/plugin-permission-backend@0.5.23 + - @backstage/plugin-scaffolder-backend@1.16.0 + - @backstage/backend-defaults@0.2.0 + - @backstage/plugin-devtools-backend@0.1.3 + - @backstage/plugin-techdocs-backend@1.6.5 + - @backstage/plugin-catalog-backend@1.12.0 + - @backstage/plugin-badges-backend@0.2.3 + - @backstage/plugin-search-backend@1.4.0 + - @backstage/plugin-proxy-backend@0.3.0 + - @backstage/plugin-todo-backend@0.2.0 + - @backstage/plugin-app-backend@0.3.48 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0 + - @backstage/plugin-entity-feedback-backend@0.1.6 + - @backstage/plugin-search-backend-node@1.2.4 + - @backstage/plugin-linguist-backend@0.4.0 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/backend-tasks@0.5.5 + - @backstage/plugin-adr-backend@0.3.6 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/plugin-permission-common@0.7.7 + +## e2e-test@0.2.6 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.4 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + +## techdocs-cli-embedded-app@0.2.85 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.9.1 + - @backstage/integration-react@1.1.16 + - @backstage/cli@0.22.10 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog@1.12.1 + - @backstage/app-defaults@1.4.2 + - @backstage/core-plugin-api@1.5.3 + - @backstage/test-utils@1.4.2 + - @backstage/plugin-techdocs@1.6.6 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-techdocs-react@1.1.9 + +## @internal/plugin-catalog-customized@0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.12.1 + - @backstage/plugin-catalog-react@1.8.1 + +## @internal/plugin-todo-list@1.0.16 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/theme@0.4.1 + +## @internal/plugin-todo-list-backend@1.0.16 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 diff --git a/docs/releases/v1.17.0-next.2-changelog.md b/docs/releases/v1.17.0-next.2-changelog.md new file mode 100644 index 0000000000..d1da6fa2be --- /dev/null +++ b/docs/releases/v1.17.0-next.2-changelog.md @@ -0,0 +1,1430 @@ +# Release v1.17.0-next.2 + +## @backstage/plugin-app-node@0.1.0-next.0 + +### Minor Changes + +- 9fbe95ef6503: Added the `app` plugin node library, initially providing an extension point that can be used to configure a static fallback handler. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + +## @backstage/plugin-catalog-backend@1.12.0-next.2 + +### Minor Changes + +- b8cccd8ee858: Support configuring applicable kinds for `AnnotateScmSlugEntityProcessor` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-module-catalog@0.1.4-next.2 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-events-node@0.2.9-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## @backstage/plugin-linguist-backend@0.4.0-next.2 + +### Minor Changes + +- d440f1dd0e72: Adds a processor to the linguist backend which can automatically add language tags to entities + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-linguist-common@0.1.1-next.1 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## @backstage/plugin-proxy-backend@0.3.0-next.2 + +### Minor Changes + +- 7daf65bfcfa1: Defining proxy endpoints directly under the root `proxy` configuration key is deprecated. Endpoints should now be declared under `proxy.endpoints` instead. The `skipInvalidProxies` and `reviveConsumedRequestBodies` can now also be configured through static configuration. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/backend-app-api@0.5.0-next.2 + +### Patch Changes + +- e65c4896f755: Do not throw in backend.stop, if start failed +- cc9256a33bcc: Added new experimental `featureDiscoveryServiceFactory`, available as an `/alpha` export. +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + - @backstage/config-loader@1.4.0-next.1 + +## @backstage/backend-common@0.19.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.5.0-next.2 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/config-loader@1.4.0-next.1 + +## @backstage/backend-defaults@0.2.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.5.0-next.2 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/backend-plugin-api@0.6.0-next.2 + +### Patch Changes + +- cc9256a33bcc: Added new experimental `featureDiscoveryServiceRef`, available as an `/alpha` export. +- Updated dependencies + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## @backstage/backend-tasks@0.5.5-next.2 + +### Patch Changes + +- dfd1b6b2fc33: Make `readTaskScheduleDefinitionFromConfig` properly handle bad inputs +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/backend-test-utils@0.2.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.5.0-next.2 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## @backstage/create-app@0.5.4-next.2 + +### Patch Changes + +- Bumped create-app version. + +## @backstage/dev-utils@1.0.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/integration-react@1.1.16-next.1 + +## @techdocs/cli@1.4.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-techdocs-node@1.7.4-next.2 + +## @backstage/plugin-adr@0.6.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/integration-react@1.1.16-next.1 + +## @backstage/plugin-adr-backend@0.3.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-airbrake@0.3.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/dev-utils@1.0.18-next.2 + +## @backstage/plugin-airbrake-backend@0.2.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-allure@0.1.37-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-api-docs@0.9.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/plugin-catalog@1.12.1-next.2 + +## @backstage/plugin-app-backend@0.3.48-next.2 + +### Patch Changes + +- d564ad142b17: Migrated the alpha `appBackend` export to use static configuration and extension points rather than accepting options. +- Updated dependencies + - @backstage/plugin-app-node@0.1.0-next.0 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/config-loader@1.4.0-next.1 + +## @backstage/plugin-auth-backend@0.18.6-next.2 + +### Patch Changes + +- 16452cd007ae: Updated `frameHandler` to return `undefined` when using the redirect flow instead of returning `postMessageReponse` which was causing errors +- bb70a9c3886a: Add frontend visibility to provider objects in `auth` config. +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## @backstage/plugin-auth-node@0.2.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-azure-devops@0.3.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-azure-devops-backend@0.3.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-azure-sites@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-azure-sites-backend@0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-badges@0.2.45-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-badges-backend@0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## @backstage/plugin-bazaar@0.2.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/plugin-catalog@1.12.1-next.2 + - @backstage/cli@0.22.10-next.1 + +## @backstage/plugin-bazaar-backend@0.2.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## @backstage/plugin-bitrise@0.1.48-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-catalog@1.12.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/integration-react@1.1.16-next.1 + +## @backstage/plugin-catalog-backend-module-aws@0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + +## @backstage/plugin-catalog-backend-module-azure@0.1.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.2.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-events-node@0.2.9-next.2 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + +## @backstage/plugin-catalog-backend-module-github@0.3.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.0-next.2 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-events-node@0.2.9-next.2 + +## @backstage/plugin-catalog-backend-module-gitlab@0.2.4-next.2 + +### Patch Changes + +- 2fe1f5973ff7: Filter Gitlab archived projects through APIs +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.0-next.2 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-events-node@0.2.9-next.2 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.2.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## @backstage/plugin-catalog-graph@0.2.33-next.2 + +### Patch Changes + +- 62dc7a2b1ad1: Added maximum depth parameter to the catalogGraphParams in CatalogGraphCard. +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-catalog-import@0.9.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/integration-react@1.1.16-next.1 + +## @backstage/plugin-catalog-node@1.4.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + +## @backstage/plugin-catalog-react@1.8.1-next.1 + +### Patch Changes + +- aa3feedce10a: Allow specifying screen size when catalog filters are hidden in drawer + +## @backstage/plugin-cicd-statistics@0.1.23-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-cicd-statistics@0.1.23-next.1 + +## @backstage/plugin-circleci@0.3.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-cloudbuild@0.3.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-code-climate@0.1.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-code-coverage@0.2.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-code-coverage-backend@0.2.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-cost-insights@0.12.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-devtools-backend@0.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + - @backstage/config-loader@1.4.0-next.1 + +## @backstage/plugin-dynatrace@7.0.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-entity-feedback@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-entity-feedback-backend@0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## @backstage/plugin-entity-validation@0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-events-backend@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-events-node@0.2.9-next.2 + +## @backstage/plugin-events-backend-module-aws-sqs@0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-events-node@0.2.9-next.2 + +## @backstage/plugin-events-backend-module-azure@0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/plugin-events-node@0.2.9-next.2 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/plugin-events-node@0.2.9-next.2 + +## @backstage/plugin-events-backend-module-gerrit@0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/plugin-events-node@0.2.9-next.2 + +## @backstage/plugin-events-backend-module-github@0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/plugin-events-node@0.2.9-next.2 + +## @backstage/plugin-events-backend-module-gitlab@0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/plugin-events-node@0.2.9-next.2 + +## @backstage/plugin-events-backend-test-utils@0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.9-next.2 + +## @backstage/plugin-events-node@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + +## @backstage/plugin-explore@0.4.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/plugin-explore-react@0.0.30 + +## @backstage/plugin-explore-backend@0.0.10-next.2 + +### Patch Changes + +- eda2a699f40d: Moved the config example from the "Tools as Code" section to the "Tools as Config" section of the README +- Updated dependencies + - @backstage/plugin-search-backend-module-explore@0.1.4-next.2 + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-firehydrant@0.2.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-fossa@0.2.53-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-github-actions@0.6.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/integration-react@1.1.16-next.1 + +## @backstage/plugin-github-deployments@0.1.52-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/integration-react@1.1.16-next.1 + +## @backstage/plugin-github-issues@0.2.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-github-pull-requests-board@0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-gocd@0.1.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-graphql-backend@0.1.38-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-graphql-voyager@0.1.6-next.2 + +### Patch Changes + +- bb1e1c2b26cc: Fix typo in install instructions. + +## @backstage/plugin-home@0.5.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/plugin-home-react@0.1.2-next.0 + +## @backstage/plugin-ilert@0.2.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-jenkins@0.8.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-jenkins-backend@0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## @backstage/plugin-kafka@0.3.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-kafka-backend@0.2.41-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-kubernetes@0.9.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-kubernetes-backend@0.11.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## @backstage/plugin-lighthouse@0.4.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-lighthouse-backend@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + +## @backstage/plugin-linguist@0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-linguist-common@0.1.1-next.1 + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-linguist-common@0.1.1-next.1 + +### Patch Changes + +- d440f1dd0e72: Exported new LanguageType type alias + +## @backstage/plugin-newrelic-dashboard@0.2.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-nomad@0.1.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-nomad-backend@0.1.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-octopus-deploy@0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-org@0.6.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-org-react@0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-pagerduty@0.6.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/plugin-home-react@0.1.2-next.0 + +## @backstage/plugin-periskop@0.1.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-periskop-backend@0.1.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-permission-backend@0.5.23-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## @backstage/plugin-permission-node@0.7.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## @backstage/plugin-playlist@0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-playlist-backend@0.3.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## @backstage/plugin-puppetdb@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-rollbar@0.4.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-rollbar-backend@0.1.45-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-scaffolder@1.14.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/plugin-scaffolder-react@1.5.2-next.1 + - @backstage/integration-react@1.1.16-next.1 + +## @backstage/plugin-scaffolder-backend@1.15.2-next.2 + +### Patch Changes + +- 33c76caef72a: Added examples for the fs:delete and fs:rename actions +- 0b1d775be05b: Adds examples to a few scaffolder actions. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.0-next.2 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/plugin-scaffolder-node@0.1.6-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.6-next.2 + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.6-next.2 + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.6-next.2 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.6-next.2 + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.6-next.2 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.6-next.2 + +## @backstage/plugin-scaffolder-node@0.1.6-next.2 + +### Patch Changes + +- 0b1d775be05b: Export `TemplateExample` from the `createTemplateAction` type. +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-scaffolder-react@1.5.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-search@1.3.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-search-backend@1.4.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-search-backend-node@1.2.4-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## @backstage/plugin-search-backend-module-catalog@0.1.4-next.2 + +### Patch Changes + +- 29f77f923c71: Ensure that all services are dependency injected into the module instead of taken from options +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-search-backend-node@1.2.4-next.2 + +## @backstage/plugin-search-backend-module-elasticsearch@1.3.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-search-backend-node@1.2.4-next.2 + +## @backstage/plugin-search-backend-module-explore@0.1.4-next.2 + +### Patch Changes + +- 29f77f923c71: Ensure that all services are dependency injected into the module instead of taken from options +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-search-backend-node@1.2.4-next.2 + +## @backstage/plugin-search-backend-module-pg@0.5.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-search-backend-node@1.2.4-next.2 + +## @backstage/plugin-search-backend-module-techdocs@0.1.4-next.2 + +### Patch Changes + +- 29f77f923c71: Ensure that all services are dependency injected into the module instead of taken from options +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-search-backend-node@1.2.4-next.2 + - @backstage/plugin-techdocs-node@1.7.4-next.2 + +## @backstage/plugin-search-backend-node@1.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-sentry@0.5.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-sonarqube@0.7.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-sonarqube-backend@0.2.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-splunk-on-call@0.4.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-stack-overflow-backend@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-tech-insights@0.3.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-tech-insights-backend@0.5.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-tech-insights-node@0.4.6-next.2 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.32-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-tech-insights-node@0.4.6-next.2 + +## @backstage/plugin-tech-insights-node@0.4.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-techdocs@1.6.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/integration-react@1.1.16-next.1 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.12.1-next.2 + - @backstage/plugin-techdocs@1.6.6-next.2 + - @backstage/integration-react@1.1.16-next.1 + +## @backstage/plugin-techdocs-backend@1.6.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-module-techdocs@0.1.4-next.2 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-techdocs-node@1.7.4-next.2 + +## @backstage/plugin-techdocs-node@1.7.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + +## @backstage/plugin-todo@0.2.23-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-todo-backend@0.2.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + +## @backstage/plugin-user-settings@0.7.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-user-settings-backend@0.1.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## @backstage/plugin-vault@0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + +## @backstage/plugin-vault-backend@0.3.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + +## example-app@0.2.86-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-graph@0.2.33-next.2 + - @backstage/plugin-linguist-common@0.1.1-next.1 + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/plugin-linguist@0.1.6-next.2 + - @backstage/plugin-adr@0.6.4-next.2 + - @backstage/plugin-airbrake@0.3.21-next.2 + - @backstage/plugin-api-docs@0.9.7-next.2 + - @backstage/plugin-azure-devops@0.3.3-next.1 + - @backstage/plugin-azure-sites@0.1.10-next.1 + - @backstage/plugin-badges@0.2.45-next.1 + - @internal/plugin-catalog-customized@0.0.13-next.2 + - @backstage/plugin-catalog-import@0.9.11-next.2 + - @backstage/plugin-circleci@0.3.21-next.1 + - @backstage/plugin-cloudbuild@0.3.21-next.1 + - @backstage/plugin-code-coverage@0.2.14-next.2 + - @backstage/plugin-cost-insights@0.12.10-next.1 + - @backstage/plugin-dynatrace@7.0.1-next.2 + - @backstage/plugin-entity-feedback@0.2.4-next.2 + - @backstage/plugin-explore@0.4.7-next.1 + - @backstage/plugin-github-actions@0.6.2-next.2 + - @backstage/plugin-gocd@0.1.27-next.1 + - @backstage/plugin-home@0.5.5-next.1 + - @backstage/plugin-jenkins@0.8.3-next.2 + - @backstage/plugin-kafka@0.3.21-next.2 + - @backstage/plugin-kubernetes@0.9.4-next.1 + - @backstage/plugin-lighthouse@0.4.6-next.1 + - @backstage/plugin-newrelic-dashboard@0.2.14-next.2 + - @backstage/plugin-nomad@0.1.2-next.2 + - @backstage/plugin-octopus-deploy@0.2.3-next.2 + - @backstage/plugin-org@0.6.11-next.2 + - @backstage/plugin-pagerduty@0.6.2-next.1 + - @backstage/plugin-playlist@0.1.13-next.2 + - @backstage/plugin-puppetdb@0.1.4-next.1 + - @backstage/plugin-rollbar@0.4.21-next.1 + - @backstage/plugin-scaffolder@1.14.2-next.2 + - @backstage/plugin-scaffolder-react@1.5.2-next.1 + - @backstage/plugin-search@1.3.4-next.1 + - @backstage/plugin-sentry@0.5.6-next.1 + - @backstage/plugin-tech-insights@0.3.13-next.2 + - @backstage/plugin-techdocs@1.6.6-next.2 + - @backstage/plugin-todo@0.2.23-next.1 + - @backstage/plugin-user-settings@0.7.6-next.1 + - @backstage/cli@0.22.10-next.1 + - @backstage/integration-react@1.1.16-next.1 + - @backstage/plugin-apache-airflow@0.2.14-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.1.2-next.0 + - @backstage/plugin-devtools@0.1.3-next.1 + - @backstage/plugin-gcalendar@0.3.17-next.1 + - @backstage/plugin-gcp-projects@0.3.40-next.0 + - @backstage/plugin-graphiql@0.2.53-next.0 + - @backstage/plugin-microsoft-calendar@0.1.6-next.1 + - @backstage/plugin-newrelic@0.3.39-next.0 + - @backstage/plugin-shortcuts@0.3.13-next.1 + - @backstage/plugin-stack-overflow@0.1.19-next.1 + - @backstage/plugin-stackstorm@0.1.5-next.0 + - @backstage/plugin-tech-radar@0.6.7-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.16-next.1 + +## example-backend@0.2.86-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.18.6-next.2 + - @backstage/plugin-scaffolder-backend@1.15.2-next.2 + - @backstage/plugin-explore-backend@0.0.10-next.2 + - @backstage/plugin-catalog-backend@1.12.0-next.2 + - @backstage/plugin-proxy-backend@0.3.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/plugin-app-backend@0.3.48-next.2 + - @backstage/plugin-linguist-backend@0.4.0-next.2 + - @backstage/plugin-techdocs-backend@1.6.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - example-app@0.2.86-next.2 + - @backstage/plugin-adr-backend@0.3.6-next.2 + - @backstage/plugin-azure-devops-backend@0.3.27-next.2 + - @backstage/plugin-badges-backend@0.2.3-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-devtools-backend@0.1.3-next.2 + - @backstage/plugin-entity-feedback-backend@0.1.6-next.2 + - @backstage/plugin-events-backend@0.2.9-next.2 + - @backstage/plugin-events-node@0.2.9-next.2 + - @backstage/plugin-kafka-backend@0.2.41-next.2 + - @backstage/plugin-kubernetes-backend@0.11.3-next.2 + - @backstage/plugin-lighthouse-backend@0.2.4-next.2 + - @backstage/plugin-permission-backend@0.5.23-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-search-backend@1.4.0-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.3.3-next.2 + - @backstage/plugin-search-backend-module-pg@0.5.9-next.2 + - @backstage/plugin-search-backend-node@1.2.4-next.2 + - @backstage/plugin-todo-backend@0.2.0-next.2 + - @backstage/plugin-tech-insights-backend@0.5.14-next.2 + - @backstage/plugin-tech-insights-node@0.4.6-next.2 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.1-next.2 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.17-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + - @backstage/plugin-azure-sites-backend@0.1.10-next.2 + - @backstage/plugin-code-coverage-backend@0.2.14-next.2 + - @backstage/plugin-graphql-backend@0.1.38-next.2 + - @backstage/plugin-jenkins-backend@0.2.3-next.2 + - @backstage/plugin-nomad-backend@0.1.2-next.2 + - @backstage/plugin-playlist-backend@0.3.4-next.2 + - @backstage/plugin-rollbar-backend@0.1.45-next.2 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.32-next.2 + +## example-backend-next@0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-module-techdocs@0.1.4-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.4-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.4-next.2 + - @backstage/plugin-scaffolder-backend@1.15.2-next.2 + - @backstage/plugin-catalog-backend@1.12.0-next.2 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/plugin-proxy-backend@0.3.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/plugin-app-backend@0.3.48-next.2 + - @backstage/plugin-linguist-backend@0.4.0-next.2 + - @backstage/plugin-techdocs-backend@1.6.5-next.2 + - @backstage/backend-defaults@0.2.0-next.2 + - @backstage/plugin-adr-backend@0.3.6-next.2 + - @backstage/plugin-azure-devops-backend@0.3.27-next.2 + - @backstage/plugin-badges-backend@0.2.3-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0-next.2 + - @backstage/plugin-devtools-backend@0.1.3-next.2 + - @backstage/plugin-entity-feedback-backend@0.1.6-next.2 + - @backstage/plugin-kubernetes-backend@0.11.3-next.2 + - @backstage/plugin-lighthouse-backend@0.2.4-next.2 + - @backstage/plugin-permission-backend@0.5.23-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-search-backend@1.4.0-next.2 + - @backstage/plugin-search-backend-node@1.2.4-next.2 + - @backstage/plugin-todo-backend@0.2.0-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + +## e2e-test@0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.4-next.2 + +## techdocs-cli-embedded-app@0.2.85-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.12.1-next.2 + - @backstage/plugin-techdocs@1.6.6-next.2 + - @backstage/cli@0.22.10-next.1 + - @backstage/integration-react@1.1.16-next.1 + +## @internal/plugin-catalog-customized@0.0.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/plugin-catalog@1.12.1-next.2 + +## @internal/plugin-todo-list-backend@1.0.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 diff --git a/docs/releases/v1.17.0.md b/docs/releases/v1.17.0.md new file mode 100644 index 0000000000..36414e6608 --- /dev/null +++ b/docs/releases/v1.17.0.md @@ -0,0 +1,72 @@ +--- +id: v1.17.0 +title: v1.17.0 +description: Backstage Release v1.17.0 +--- + +These are the release notes for the v1.17.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 + +### Changes to the New Backend System + +For this release we had another look at the new backend system, in particular how well it fits together with the concept of plugin installation without code modification, and we identified a number of breaking changes that we wanted to make. + +We have removed the ability to define options for plugins and modules, and force them to be optional for services. Options have been moved either to static configuration or extension points, and modules now also have the ability to register extension points. + +Service factories are now considered `BackendFeature`s and are installed via `backend.add(...)` rather than via the `services` option, which has been removed. + +The configuration service has also been renamed to `rootConfig` in order to match other root-scoped services and make space for a potential plugin-scoped configuration service in the future. + +The shared environments concept has been removed. We are still interested in having some way to define reusable presets in the new system, but shared environments were too limited. + +We expect these to be the last broad breaking changes to the new backend system, and are likely to start encouraging use of it in the next release. + +### Catalog Telemetry with Processing Tracing + +The Catalog has been instrumented with some simple OpenTelemetry Spans so you can now dig into the processing loop and see it in action using tracing. + +Contributed by [@mikebryant](https://github.com/mikebryant) in [#17534](https://github.com/backstage/backstage/pull/17534) + +### New `each` property for Scaffolder Actions + +You can now define an `each` iterable in the `steps` for a template, which will run a given action for each entry in the iterable. + +Contributed by [@alexef](https://github.com/alexef) in [#18157](https://github.com/backstage/backstage/pull/18157) + +### Updated proxy configuration + +The shape of the proxy plugin configuration has been changed in order to make space for additional keys. Rather than defining the proxy endpoints directly under `proxy`, they should now be defined at `proxy.endpoints` instead. + +### OpenAPI Support for Backend Plugins + +You can now use OpenAPI specs to generate typed routers for your backend plugins with request validation using `createOpenApiRouter()`. There’s been some movement to support this for some of the plugins in the monorepo like `todo`, `search` and `catalog`. + +Contributed by [@sennyeya](https://github.com/sennyeya) in [#17875](https://github.com/backstage/backstage/pull/17875) + +### New plugin: `@backstage/plugin-catalog-backend-module-gcp` + +Allows ingestion of GKE Clusters as Resources into the Catalog. Contributed by [@mclarke47](https://github.com/mclarke47) in [#18759](https://github.com/backstage/backstage/pull/18759) + +## 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.17.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.18.0-next.0-changelog.md b/docs/releases/v1.18.0-next.0-changelog.md new file mode 100644 index 0000000000..16c71179e5 --- /dev/null +++ b/docs/releases/v1.18.0-next.0-changelog.md @@ -0,0 +1,2753 @@ +# Release v1.18.0-next.0 + +## @backstage/config-loader@1.5.0-next.0 + +### Minor Changes + +- 9606ba0939e6: Deep visibility now also applies to values that are not covered by the configuration schema. + + For example, given the following configuration schema: + + ```ts + // plugins/a/config.schema.ts + export interface Config { + /** @deepVisibility frontend */ + a?: unknown; + } + + // plugins/a/config.schema.ts + export interface Config { + a?: { + b?: string; + }; + } + ``` + + All values under `a` are now visible to the frontend, while previously only `a` and `a/b` would've been visible. + +### Patch Changes + +- f9657b891b00: Do not unnecessarily notify subscribers when no-op updates to config happen +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/core-app-api@1.10.0-next.0 + +### Minor Changes + +- 18619f793c94: Fixed two bugs in how the `OAuth2Session` type represents the underlying data. The `expiresAt` and `backstageIdentity` are now both optional, since that's what they are in practice. This is not considered a breaking change since it was effectively a bug in the modelling of the state that this type represents, and the type was not used in any other external contract. +- 18619f793c94: The `OAuth` class which is used by all OAuth providers will now consider both the session expiration of both the Backstage identity as well as the upstream identity provider, and refresh the session with either of them is about to expire. + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## @backstage/core-plugin-api@1.6.0-next.0 + +### Minor Changes + +- 18619f793c94: Added the optional `expiresAt` field that may now be part of a `BackstageIdentityResponse`. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## @backstage/integration@1.7.0-next.0 + +### Minor Changes + +- 5f1a92b9f19f: Added `AzureDevOpsCredentialsProvider` to support multiple Azure DevOps organizations and **deprecated** `AzureIntegrationConfig.credential` and `AzureIntegrationConfig.token` in favour of `AzureIntegrationConfig.credentials`. You can now use specific credentials for different Azure DevOps (Server) organizations by specifying the `organizations` field on a credential: + + ```yaml + integrations: + azure: + - host: dev.azure.com + credentials: + - organizations: + - my-org + - my-other-org + clientId: ${AZURE_CLIENT_ID} + clientSecret: ${AZURE_CLIENT_SECRET} + tenantId: ${AZURE_TENANT_ID} + - organizations: + - yet-another-org + personalAccessToken: ${PERSONAL_ACCESS_TOKEN} + ``` + + See the [Azure integration documentation](https://backstage.io/docs/integrations/azure/locations) for more information. + +### Patch Changes + +- cb2e19d82d95: Gitiles: Fixed auth prefix issue +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.1.0-next.0 + +### Minor Changes + +- 8513cd7d00e3: New module for `@backstage/plugin-auth-backend` that adds a GCP IAP auth provider. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-auth-backend-module-github-provider@0.1.0-next.0 + +### Minor Changes + +- 23af27f5ce79: New module for `@backstage/plugin-auth-backend` that adds a GitHub auth provider. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + +## @backstage/plugin-auth-backend-module-google-provider@0.1.0-next.0 + +### Minor Changes + +- 8513cd7d00e3: New module for `@backstage/plugin-auth-backend` that adds a Google auth provider. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + +## @backstage/plugin-auth-node@0.3.0-next.0 + +### Minor Changes + +- 8513cd7d00e3: Introduced a new system for building auth providers for `@backstage/plugin-auth-backend`, which both increases the amount of code re-use across providers, and also works better with the new backend system. + + Many existing types have been moved from `@backstage/plugin-auth-backend` in order to avoid a direct dependency on the plugin from modules. + + Auth provider integrations are now primarily implemented through a pattern of creating "authenticators", which are in turn specific to each kind of integrations. Initially there are two types: `createOAuthAuthenticator` and `createProxyAuthenticator`. These come paired with functions that let you create the corresponding route handlers, `createOAuthRouteHandlers` and `createProxyAuthRouteHandlers`, as well as provider factories, `createOAuthProviderFactory` and `createProxyAuthProviderFactory`. This new authenticator pattern allows the sign-in logic to be separated from the auth integration logic, allowing it to be completely re-used across all providers of the same kind. + + The new provider factories also implement a new declarative way to configure sign-in resolvers, rather than configuration through code. Sign-in resolvers can now be configured through the `resolvers` configuration key, where the first resolver that provides an identity will be used, for example: + + ```yaml + auth: + providers: + google: + development: + clientId: ... + clientSecret: ... + signIn: + resolvers: + - resolver: emailMatchingUserEntityAnnotation + - resolver: emailLocalPartMatchingUserEntityName + ``` + + These configurable resolvers are created with a new `createSignInResolverFactory` function, which creates a sign-in resolver factory, optionally with an options schema that will be used both when configuring the sign-in resolver through configuration and code. + + The internal helpers from `@backstage/plugin-auth-backend` that were used to implement auth providers using passport strategies have now also been made available as public API, through `PassportHelpers` and `PassportOAuthAuthenticatorHelper`. + +### Patch Changes + +- 18619f793c94: The `BackstageIdentityResponse` interface now has an optional `expiresInSeconds` field that can be used to signal session expiration. The `prepareBackstageIdentityResponse` utility will now also read the expiration from the provided token, and include it in the response. +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-opencost@0.2.0-next.0 + +### Minor Changes + +- 1380a689ab56: New OpenCost plugin provides an port of the latest OpenCost UI to Backstage with updated dependencies. The plugin's README covers installation and configuration + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @backstage/app-defaults@1.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-permission-react@0.4.15-next.0 + +## @backstage/backend-app-api@0.5.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-node@0.7.13-next.0 + +## @backstage/backend-common@0.19.4-next.0 + +### Patch Changes + +- 5f1a92b9f19f: Use `DefaultAzureDevOpsCredentialsProvider` to retrieve credentials for Azure DevOps. +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/backend-app-api@0.5.2-next.0 + - @backstage/backend-dev-utils@0.1.1 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/integration-aws-node@0.1.5 + - @backstage/types@1.1.0 + +## @backstage/backend-defaults@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-app-api@0.5.2-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + +## @backstage/backend-plugin-api@0.6.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/backend-tasks@0.5.7-next.0 + +### Patch Changes + +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/backend-test-utils@0.2.2-next.0 + +### Patch Changes + +- 9fb3b5373c45: Extended `mockService` to also include mocked variants, for example `mockServices.lifecycle.mock()`. The returned mocked implementation will have a `factory` property which is a service factory for itself. You can also pass a partial implementation of the service to the mock function to use a mock implementation of specific methods. +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-app-api@0.5.2-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + +## @backstage/cli@0.22.12-next.0 + +### Patch Changes + +- 278d9326eb40: Added the ability to create a plain backend module with the `new` command. +- 71d4368ae5cc: Added support for the `dev/index` entry point for backend plugins and modules. +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/eslint-plugin@0.1.3 + - @backstage/release-manifests@0.0.9 + - @backstage/types@1.1.0 + +## @backstage/core-components@0.13.5-next.0 + +### Patch Changes + +- 47782f4bfa5b: Add loading indicator to Table +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/version-bridge@1.0.4 + +## @backstage/create-app@0.5.5-next.0 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.12 + +## @backstage/dev-utils@1.0.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/app-defaults@1.4.3-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/test-utils@1.4.3-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/integration-react@1.1.18-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## @backstage/repo-tools@0.3.4-next.0 + +### Patch Changes + +- 0109d3f11159: The `generate-catalog-info` command now uses the first listed `CODEOWNER` as Component owner when initially + creating the `catalog-info.yaml` file. It continues to allow any one listed `CODEOWNER` when updating + entity metadata. +- ec13d3e86028: Fixed a bug with the `generate-catalog-info` command that could cause the `--dry-run` flag to indicate changes to files when no changes would actually be made if the command were run without the flag. +- db60a16e0a54: Added a `--ci` flag to the `generate-catalog-info` command. This flag behaves similarly to the same flag on `api-reports`: if `catalog-info.yaml` files would have been added or modified, then the process exits with status code `1`, and instructions are printed. +- Updated dependencies + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + +## @techdocs/cli@1.4.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/plugin-techdocs-node@1.7.6-next.0 + +## @backstage/test-utils@1.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.15-next.0 + +## @backstage/plugin-adr@0.6.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-adr-common@0.2.14-next.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + +## @backstage/plugin-adr-backend@0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-adr-common@0.2.14-next.0 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-adr-common@0.2.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-airbrake@0.3.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/dev-utils@1.0.20-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/test-utils@1.4.3-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-airbrake-backend@0.2.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + +## @backstage/plugin-allure@0.1.39-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-analytics-module-ga@0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## @backstage/plugin-analytics-module-ga4@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## @backstage/plugin-analytics-module-newrelic-browser@0.0.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + +## @backstage/plugin-apache-airflow@0.2.15-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + +## @backstage/plugin-api-docs@0.9.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-api-docs-module-protoc-gen-doc@0.1.3-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency + +## @backstage/plugin-apollo-explorer@0.1.15-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @backstage/plugin-app-backend@0.3.50-next.0 + +### Patch Changes + +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-app-node@0.1.2-next.0 + +## @backstage/plugin-app-node@0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + +## @backstage/plugin-auth-backend@0.18.8-next.0 + +### Patch Changes + +- 7944d43f4790: Added `authPlugin` export for the new backend system. The plugin does not include any built-in auth providers, they must instead be added by installing additional modules, for example `authModuleGoogleProvider` from `@backstage/plugin-auth-backend-module-google-provider`. +- 8513cd7d00e3: Deprecated several exports that are now available from `@backstage/plugin-auth-node` instead. +- 7944d43f4790: Added the ability to disable the built-in auth providers by passing `disableDefaultProviderFactories` to `createRouter`. +- 7944d43f4790: The algorithm used when generating Backstage tokens can be configured via `auth.identityTokenAlgorithm`. +- Updated dependencies + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.1.0-next.0 + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/plugin-auth-backend-module-google-provider@0.1.0-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-azure-devops@0.3.5-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-devops-common@0.3.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-azure-devops-backend@0.3.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-sites@0.1.12-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-sites-common@0.1.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-azure-sites-backend@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-badges@0.2.47-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-badges-backend@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-bazaar@0.2.15-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/cli@0.22.12-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-bazaar-backend@0.2.13-next.0 + +### Patch Changes + +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-bitbucket-cloud-common@0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + +## @backstage/plugin-bitrise@0.1.50-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-catalog@1.12.4-next.0 + +### Patch Changes + +- 832eef72485b: Added title to props for `HasCards`. +- 163a41035e42: Fixed an issue where `EntitySwitch` was preventing the display of entity errors. +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + +## @backstage/plugin-catalog-backend@1.12.2-next.0 + +### Patch Changes + +- 149361e81622: Fix to the `limit` parameter on entity queries. +- 0198aa596fd9: Fixed a link to the frontend Backstage plugin that had pointed to itself. +- 41d1b2d628ea: Fix OpenAPI schema for the facets endpoint +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/plugin-search-backend-module-catalog@0.1.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-catalog-backend-module-aws@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/integration-aws-node@0.1.5 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-kubernetes-common@0.6.5 + +## @backstage/plugin-catalog-backend-module-azure@0.1.21-next.0 + +### Patch Changes + +- 94f96508491d: Improve consistency of results from the `AzureDevOpsEntityProvider`. +- 5f1a92b9f19f: Use `DefaultAzureDevOpsCredentialsProvider` to retrieve credentials for Azure DevOps. +- 044b4f2fb1e3: Remove duplications from Azure search before committing the new locations to the catalog. +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.11-next.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-bitbucket-cloud-common@0.2.11-next.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-kubernetes-common@0.6.5 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-github@0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + +## @backstage/plugin-catalog-graph@0.2.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-catalog-import@0.9.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-catalog-node@1.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + +## @backstage/plugin-catalog-react@1.8.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.15-next.0 + +## @backstage/plugin-catalog-unprocessed-entities@0.1.3-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-cicd-statistics@0.1.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/plugin-cicd-statistics@0.1.25-next.0 + +## @backstage/plugin-circleci@0.3.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-cloudbuild@0.3.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-code-climate@0.1.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-code-coverage@0.2.16-next.0 + +### Patch Changes + +- 1d8f4f0a7486: Use fetchApi to ensure authorization is used when fetching code-coverage data +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-code-coverage-backend@0.2.16-next.0 + +### Patch Changes + +- 3f1367b956c6: Added support for LCOV coverage reports +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-codescene@0.1.17-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-config-schema@0.1.45-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-cost-insights@0.12.12-next.0 + +### Patch Changes + +- a54cd411639e: Updated dependency `@types/pluralize` to `^0.0.30`. +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-cost-insights-common@0.1.1 + +## @backstage/plugin-devtools@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-devtools-common@0.1.3 + - @backstage/plugin-permission-react@0.4.15-next.0 + +## @backstage/plugin-devtools-backend@0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-devtools-common@0.1.3 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + +## @backstage/plugin-dynatrace@7.0.3-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-entity-feedback@0.2.6-next.0 + +### Patch Changes + +- 5e2e06db747a: Improve README to note that Backstage identity is required to be configured +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-entity-feedback-common@0.1.2 + +## @backstage/plugin-entity-feedback-backend@0.1.8-next.0 + +### Patch Changes + +- 740155e8e976: Improve backend logging if method calls fail +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-entity-feedback-common@0.1.2 + +## @backstage/plugin-entity-validation@0.1.8-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-events-backend@0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-events-backend-module-aws-sqs@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-events-backend-module-azure@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-events-backend-module-gerrit@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-events-backend-module-github@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-events-backend-module-gitlab@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-events-backend-test-utils@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.11-next.0 + +## @backstage/plugin-events-node@0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + +## @backstage/plugin-explore@0.4.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-explore-react@0.0.31-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + +## @backstage/plugin-explore-backend@0.0.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-backend-module-explore@0.1.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-explore-react@0.0.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/plugin-explore-common@0.0.1 + +## @backstage/plugin-firehydrant@0.2.7-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-fossa@0.2.55-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-gcalendar@0.3.18-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-gcp-projects@0.3.41-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @backstage/plugin-git-release-manager@0.3.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @backstage/plugin-github-actions@0.6.4-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-github-deployments@0.1.54-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-github-issues@0.2.12-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-github-pull-requests-board@0.1.17-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-gitops-profiles@0.3.40-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## @backstage/plugin-gocd@0.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-graphiql@0.2.54-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @backstage/plugin-graphql-backend@0.1.40-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-graphql@0.3.22 + +## @backstage/plugin-graphql-voyager@0.1.7-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @backstage/plugin-home@0.5.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home-react@0.1.3-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-home-react@0.1.3-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + +## @backstage/plugin-ilert@0.2.12-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-jenkins@0.8.5-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-jenkins-common@0.1.18 + +## @backstage/plugin-jenkins-backend@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-jenkins-common@0.1.18 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + +## @backstage/plugin-kafka@0.3.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-kafka-backend@0.2.43-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-kubernetes@0.10.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-kubernetes-common@0.6.5 + +## @backstage/plugin-kubernetes-backend@0.11.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/integration-aws-node@0.1.5 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-kubernetes-common@0.6.5 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + +## @backstage/plugin-lighthouse@0.4.8-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-lighthouse-common@0.1.2 + +## @backstage/plugin-lighthouse-backend@0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-lighthouse-common@0.1.2 + +## @backstage/plugin-linguist@0.1.8-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-linguist-common@0.1.1 + +## @backstage/plugin-linguist-backend@0.4.2-next.0 + +### Patch Changes + +- b2de501bda31: When creating the router using `createRouterFromConfig` or using the plugin for the new backend system the `linguist.useSourceLocation` configuration is now optional. +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-linguist-common@0.1.1 + +## @backstage/plugin-microsoft-calendar@0.1.7-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-newrelic@0.3.40-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @backstage/plugin-newrelic-dashboard@0.2.16-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-nomad@0.1.4-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-nomad-backend@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-octopus-deploy@0.2.5-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-org@0.6.13-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-org-react@0.1.12-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-pagerduty@0.6.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home-react@0.1.3-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-periskop@0.1.21-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-periskop-backend@0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + +## @backstage/plugin-permission-backend@0.5.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + +## @backstage/plugin-permission-node@0.7.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-permission-react@0.4.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-permission-common@0.7.7 + +## @backstage/plugin-playlist@0.1.15-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.15-next.0 + - @backstage/plugin-playlist-common@0.1.9 + - @backstage/plugin-search-react@1.6.5-next.0 + +## @backstage/plugin-playlist-backend@0.3.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-playlist-common@0.1.9 + +## @backstage/plugin-proxy-backend@0.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + +## @backstage/plugin-puppetdb@0.1.6-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-rollbar@0.4.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-rollbar-backend@0.1.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + +## @backstage/plugin-scaffolder@1.14.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/plugin-scaffolder-react@1.5.4-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-permission-react@0.4.15-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + +## @backstage/plugin-scaffolder-backend@1.16.3-next.0 + +### Patch Changes + +- a4989552d828: Add examples for `publish:github` and `publish:gitlab` scaffolder actions. +- 5f1a92b9f19f: Use `DefaultAzureDevOpsCredentialsProvider` to retrieve credentials for Azure DevOps. +- fb57a4694fc6: Fixed the plugin and module ID of the alpha `catalogModuleTemplateKind` export. +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## @backstage/plugin-scaffolder-node@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-common@1.4.0 + +## @backstage/plugin-scaffolder-react@1.5.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + +## @backstage/plugin-search@1.3.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + +## @backstage/plugin-search-backend@1.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-backend-module-catalog@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-backend-module-elasticsearch@1.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/integration-aws-node@0.1.5 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-backend-module-explore@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-backend-module-pg@0.5.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-backend-module-techdocs@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-techdocs-node@1.7.6-next.0 + +## @backstage/plugin-search-backend-node@1.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-search-react@1.6.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-sentry@0.5.8-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-shortcuts@0.3.14-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-sonarqube@0.7.4-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-sonarqube-react@0.1.8-next.0 + +## @backstage/plugin-sonarqube-backend@0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-sonarqube-react@0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/catalog-model@1.4.1 + +## @backstage/plugin-splunk-on-call@0.4.12-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-stack-overflow@0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home-react@0.1.3-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + +## @backstage/plugin-stack-overflow-backend@0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-stackstorm@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-tech-insights@0.3.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-tech-insights-common@0.2.11 + +## @backstage/plugin-tech-insights-backend@0.5.16-next.0 + +### Patch Changes + +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-tech-insights-common@0.2.11 + - @backstage/plugin-tech-insights-node@0.4.8-next.0 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.34-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-tech-insights-common@0.2.11 + - @backstage/plugin-tech-insights-node@0.4.8-next.0 + +## @backstage/plugin-tech-insights-node@0.4.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-tech-insights-common@0.2.11 + +## @backstage/plugin-tech-radar@0.6.8-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @backstage/plugin-techdocs@1.6.8-next.0 + +### Patch Changes + +- 88c9525a36f3: Fixed bug in styles that caused next and previous links in footer to overlap page content. +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + - @backstage/plugin-techdocs-react@1.1.10-next.0 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/plugin-techdocs@1.6.8-next.0 + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/test-utils@1.4.3-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-search-react@1.6.5-next.0 + - @backstage/plugin-techdocs-react@1.1.10-next.0 + +## @backstage/plugin-techdocs-backend@1.6.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-backend-module-techdocs@0.1.6-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-techdocs-node@1.7.6-next.0 + +## @backstage/plugin-techdocs-module-addons-contrib@1.0.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-techdocs-react@1.1.10-next.0 + +## @backstage/plugin-techdocs-node@1.7.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/integration-aws-node@0.1.5 + - @backstage/plugin-search-common@1.2.5 + +## @backstage/plugin-techdocs-react@1.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/version-bridge@1.0.4 + +## @backstage/plugin-todo@0.2.25-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-todo-backend@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## @backstage/plugin-user-settings@0.7.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-user-settings-backend@0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-vault@0.1.17-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @backstage/plugin-vault-backend@0.3.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## @backstage/plugin-xcmetrics@0.2.42-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## example-app@0.2.87-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-unprocessed-entities@0.1.3-next.0 + - @backstage/integration-react@1.1.18-next.0 + - @backstage/plugin-microsoft-calendar@0.1.7-next.0 + - @backstage/plugin-newrelic-dashboard@0.2.16-next.0 + - @backstage/plugin-apache-airflow@0.2.15-next.0 + - @backstage/plugin-github-actions@0.6.4-next.0 + - @backstage/plugin-octopus-deploy@0.2.5-next.0 + - @backstage/plugin-azure-devops@0.3.5-next.0 + - @backstage/plugin-gcp-projects@0.3.41-next.0 + - @backstage/plugin-azure-sites@0.1.12-next.0 + - @backstage/plugin-cloudbuild@0.3.23-next.0 + - @backstage/plugin-lighthouse@0.4.8-next.0 + - @backstage/plugin-tech-radar@0.6.8-next.0 + - @backstage/plugin-dynatrace@7.0.3-next.0 + - @backstage/plugin-gcalendar@0.3.18-next.0 + - @backstage/plugin-shortcuts@0.3.14-next.0 + - @backstage/plugin-airbrake@0.3.23-next.0 + - @backstage/plugin-circleci@0.3.23-next.0 + - @backstage/plugin-graphiql@0.2.54-next.0 + - @backstage/plugin-linguist@0.1.8-next.0 + - @backstage/plugin-newrelic@0.3.40-next.0 + - @backstage/plugin-playlist@0.1.15-next.0 + - @backstage/plugin-puppetdb@0.1.6-next.0 + - @backstage/plugin-jenkins@0.8.5-next.0 + - @backstage/plugin-rollbar@0.4.23-next.0 + - @backstage/plugin-badges@0.2.47-next.0 + - @backstage/plugin-sentry@0.5.8-next.0 + - @backstage/plugin-kafka@0.3.23-next.0 + - @backstage/plugin-nomad@0.1.4-next.0 + - @backstage/plugin-todo@0.2.25-next.0 + - @backstage/plugin-org@0.6.13-next.0 + - @backstage/cli@0.22.12-next.0 + - @backstage/plugin-techdocs@1.6.8-next.0 + - @backstage/plugin-entity-feedback@0.2.6-next.0 + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/plugin-code-coverage@0.2.16-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/plugin-cost-insights@0.12.12-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/plugin-adr@0.6.6-next.0 + - @backstage/plugin-catalog-import@0.9.13-next.0 + - @backstage/plugin-scaffolder@1.14.4-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.18-next.0 + - @backstage/plugin-home@0.5.7-next.0 + - @backstage/plugin-pagerduty@0.6.4-next.0 + - @backstage/plugin-stack-overflow@0.1.20-next.0 + - @backstage/plugin-api-docs@0.9.10-next.0 + - @internal/plugin-catalog-customized@0.0.14-next.0 + - @backstage/plugin-catalog-graph@0.2.35-next.0 + - @backstage/plugin-explore@0.4.9-next.0 + - @backstage/plugin-scaffolder-react@1.5.4-next.0 + - @backstage/app-defaults@1.4.3-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-devtools@0.1.4-next.0 + - @backstage/plugin-gocd@0.1.29-next.0 + - @backstage/plugin-kubernetes@0.10.2-next.0 + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/plugin-permission-react@0.4.15-next.0 + - @backstage/plugin-search@1.3.6-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + - @backstage/plugin-stackstorm@0.1.6-next.0 + - @backstage/plugin-tech-insights@0.3.15-next.0 + - @backstage/plugin-techdocs-react@1.1.10-next.0 + - @backstage/plugin-user-settings@0.7.8-next.0 + +## example-backend@0.2.87-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/plugin-auth-backend@0.18.8-next.0 + - @backstage/plugin-code-coverage-backend@0.2.16-next.0 + - @backstage/plugin-scaffolder-backend@1.16.3-next.0 + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/plugin-linguist-backend@0.4.2-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/plugin-entity-feedback-backend@0.1.8-next.0 + - @backstage/plugin-tech-insights-backend@0.5.16-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/plugin-app-backend@0.3.50-next.0 + - example-app@0.2.87-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-adr-backend@0.3.8-next.0 + - @backstage/plugin-azure-devops-backend@0.3.29-next.0 + - @backstage/plugin-azure-sites-backend@0.1.12-next.0 + - @backstage/plugin-badges-backend@0.2.5-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.2-next.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-devtools-backend@0.1.5-next.0 + - @backstage/plugin-events-backend@0.2.11-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + - @backstage/plugin-explore-backend@0.0.12-next.0 + - @backstage/plugin-graphql-backend@0.1.40-next.0 + - @backstage/plugin-jenkins-backend@0.2.5-next.0 + - @backstage/plugin-kafka-backend@0.2.43-next.0 + - @backstage/plugin-kubernetes-backend@0.11.5-next.0 + - @backstage/plugin-lighthouse-backend@0.2.6-next.0 + - @backstage/plugin-nomad-backend@0.1.4-next.0 + - @backstage/plugin-permission-backend@0.5.25-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-playlist-backend@0.3.6-next.0 + - @backstage/plugin-proxy-backend@0.3.2-next.0 + - @backstage/plugin-rollbar-backend@0.1.47-next.0 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.3-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.19-next.0 + - @backstage/plugin-search-backend@1.4.2-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.3.5-next.0 + - @backstage/plugin-search-backend-module-pg@0.5.11-next.0 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.34-next.0 + - @backstage/plugin-tech-insights-node@0.4.8-next.0 + - @backstage/plugin-techdocs-backend@1.6.7-next.0 + - @backstage/plugin-todo-backend@0.2.2-next.0 + +## example-backend-next@0.0.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/plugin-scaffolder-backend@1.16.3-next.0 + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/plugin-linguist-backend@0.4.2-next.0 + - @backstage/plugin-entity-feedback-backend@0.1.8-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/plugin-app-backend@0.3.50-next.0 + - @backstage/backend-defaults@0.2.2-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/plugin-adr-backend@0.3.8-next.0 + - @backstage/plugin-azure-devops-backend@0.3.29-next.0 + - @backstage/plugin-badges-backend@0.2.5-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.2-next.0 + - @backstage/plugin-devtools-backend@0.1.5-next.0 + - @backstage/plugin-kubernetes-backend@0.11.5-next.0 + - @backstage/plugin-lighthouse-backend@0.2.6-next.0 + - @backstage/plugin-permission-backend@0.5.25-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-proxy-backend@0.3.2-next.0 + - @backstage/plugin-search-backend@1.4.2-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.6-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.6-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.6-next.0 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-techdocs-backend@1.6.7-next.0 + - @backstage/plugin-todo-backend@0.2.2-next.0 + +## e2e-test@0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.5-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + +## techdocs-cli-embedded-app@0.2.86-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/cli@0.22.12-next.0 + - @backstage/plugin-techdocs@1.6.8-next.0 + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/app-defaults@1.4.3-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/test-utils@1.4.3-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-techdocs-react@1.1.10-next.0 + +## @internal/plugin-catalog-customized@0.0.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## @internal/plugin-todo-list@1.0.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## @internal/plugin-todo-list-backend@1.0.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 diff --git a/docs/releases/v1.18.0-next.1-changelog.md b/docs/releases/v1.18.0-next.1-changelog.md new file mode 100644 index 0000000000..9e5212dfc5 --- /dev/null +++ b/docs/releases/v1.18.0-next.1-changelog.md @@ -0,0 +1,2857 @@ +# Release v1.18.0-next.1 + +## @backstage/config@1.1.0-next.0 + +### Minor Changes + +- 62f448edb0b5: Added a `readDurationFromConfig` function + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @techdocs/cli@1.5.0-next.1 + +### Minor Changes + +- 10a86bd4ae12: Add optional config and cli option for techdocs to specify default mkdocs plugins. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-techdocs-node@1.8.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/cli-common@0.1.12 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.1.0-next.0 + +### Minor Changes + +- 080cc7794700: New module for `@backstage/plugin-auth-backend` that adds a GitLab auth provider. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + +## @backstage/plugin-catalog-backend@1.13.0-next.1 + +### Minor Changes + +- 62f448edb0b5: Allow configuring the processing interval in your app-config, under the `catalog.processingInterval` key. + +### Patch Changes + +- 1fd2109739c1: Changed the processing loop task pipeline implementation from recursive to iterative +- 0f8a97777489: Update OpenAPI schema to relax the encoding validation of all request parameters. +- 2d32d8a611e3: Fixed validation of the `fullTextFilterFields` query parameter. +- 618257f3e413: Fix issue with `catalogFileName` not being a required property for `/analyze-location` +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.7-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-events-node@0.2.12-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/plugin-scaffolder-common@1.4.1-next.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-search@1.4.0-next.1 + +### Minor Changes + +- b78f570f44d3: The SearchPage component can now be configured via app-config.yaml with default query parameters to define how it behaves when it is first loaded or reset. Check out the following example: + + ```yaml + search: + query: + pageLimit: 50 + ``` + + Acceptable values for `pageLimit` are `10`, `25`, `50` or `100`. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-search-react@1.7.0-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-search-react@1.7.0-next.1 + +### Minor Changes + +- b78f570f44d3: The SearchPage component can now be configured via app-config.yaml with default query parameters to define how it behaves when it is first loaded or reset. Check out the following example: + + ```yaml + search: + query: + pageLimit: 50 + ``` + + Acceptable values for `pageLimit` are `10`, `25`, `50` or `100`. + +### Patch Changes + +- 45f8a95e1068: Optionally initializes the search context with default settings for search queries only when the config is defined, rather than always overriding it. +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-techdocs-backend@1.7.0-next.1 + +### Minor Changes + +- 5985d458ee30: Add a `techdocs.publisher.azureBlobStorage.connectionString` app-config setting, which can be leveraged for improved Azurite support. +- 10a86bd4ae12: Add optional config and cli option for techdocs to specify default mkdocs plugins. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/plugin-techdocs-node@1.8.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.7-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.0-next.1 + +### Minor Changes + +- 86c19906fe4b: Enable zoom icon for techdocs images inside lightbox + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/integration@1.7.0-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/plugin-techdocs-react@1.1.10-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-techdocs-node@1.8.0-next.1 + +### Minor Changes + +- 5985d458ee30: Add a `techdocs.publisher.azureBlobStorage.connectionString` app-config setting, which can be leveraged for improved Azurite support. +- 10a86bd4ae12: Add optional config and cli option for techdocs to specify default mkdocs plugins. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/integration-aws-node@0.1.6-next.0 + - @backstage/errors@1.2.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/app-defaults@1.4.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-app-api@1.10.0-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-permission-react@0.4.15-next.1 + - @backstage/theme@0.4.1 + +## @backstage/backend-app-api@0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/config-loader@1.5.0-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/backend-common@0.19.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-app-api@0.5.3-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/config-loader@1.5.0-next.1 + - @backstage/integration-aws-node@0.1.6-next.0 + - @backstage/backend-dev-utils@0.1.1 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/backend-defaults@0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.5.3-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + +## @backstage/backend-plugin-api@0.6.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/types@1.1.0 + +## @backstage/backend-tasks@0.5.8-next.1 + +### Patch Changes + +- 62f448edb0b5: Use `readDurationFromConfig` from the config package +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/backend-test-utils@0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-app-api@0.5.3-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/types@1.1.0 + +## @backstage/catalog-client@1.4.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/errors@1.2.1 + +## @backstage/catalog-model@1.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/cli@0.22.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/config-loader@1.5.0-next.1 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + - @backstage/eslint-plugin@0.1.3 + - @backstage/release-manifests@0.0.9 + - @backstage/types@1.1.0 + +## @backstage/config-loader@1.5.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/core-app-api@1.10.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## @backstage/core-components@0.13.5-next.1 + +### Patch Changes + +- 0c9907645aab: Fixed an issue causing `StructuredMetadataTable` to crash in case metadata contained `null` values. +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/version-bridge@1.0.4 + +## @backstage/core-plugin-api@1.6.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## @backstage/create-app@0.5.5-next.1 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.12 + +## @backstage/dev-utils@1.0.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/app-defaults@1.4.3-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-app-api@1.10.0-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/test-utils@1.4.3-next.1 + - @backstage/theme@0.4.1 + +## @backstage/integration@1.7.0-next.1 + +### Patch Changes + +- 2d2fc9d20ebb: Additional fix for Gitiles auth links +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/errors@1.2.1 + +## @backstage/integration-aws-node@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/errors@1.2.1 + +## @backstage/integration-react@1.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/repo-tools@0.3.4-next.1 + +### Patch Changes + +- 6f874cdb04eb: Fixed a bug with the `generate-catalog-info` command that could cause `metadata.description` values to be overwritten by `package.json` description values only because unrelated attributes were being changed. +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + +## @backstage/test-utils@1.4.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/core-app-api@1.10.0-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-react@0.4.15-next.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-adr@0.6.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/plugin-search-react@1.7.0-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-adr-common@0.2.15-next.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-adr-backend@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-adr-common@0.2.15-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-adr-common@0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-airbrake@0.3.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/dev-utils@1.0.21-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/test-utils@1.4.3-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-airbrake-backend@0.2.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + +## @backstage/plugin-allure@0.1.40-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-analytics-module-ga@0.1.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-analytics-module-ga4@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-analytics-module-newrelic-browser@0.0.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + +## @backstage/plugin-apache-airflow@0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + +## @backstage/plugin-api-docs@0.9.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/plugin-catalog@1.12.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-apollo-explorer@0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-app-backend@0.3.51-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/config-loader@1.5.0-next.1 + - @backstage/plugin-app-node@0.1.3-next.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-app-node@0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.1 + +## @backstage/plugin-auth-backend@0.18.9-next.1 + +### Patch Changes + +- 080cc7794700: Migrated the GitLab auth provider to be implemented using the new `@backstage/plugin-auth-backend-module-gitlab-provider` module. +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.0-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.0-next.1 + - @backstage/plugin-auth-backend-module-google-provider@0.1.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.1.0-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-auth-backend-module-github-provider@0.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + +## @backstage/plugin-auth-backend-module-google-provider@0.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + +## @backstage/plugin-auth-node@0.3.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-azure-devops@0.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-devops-backend@0.3.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-sites@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-azure-sites-backend@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-badges@0.2.48-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-badges-backend@0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + +## @backstage/plugin-bazaar@0.2.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/plugin-catalog@1.12.5-next.1 + - @backstage/cli@0.22.13-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-bazaar-backend@0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-bitbucket-cloud-common@0.2.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.1 + +## @backstage/plugin-bitrise@0.1.51-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-catalog@1.12.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/plugin-search-react@1.7.0-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/plugin-scaffolder-common@1.4.1-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-catalog-backend-module-aws@0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.6-next.0 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/integration-aws-node@0.1.6-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-backend-module-azure@0.1.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.2.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.12-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.1.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.12-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-events-node@0.2.12-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.6-next.0 + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-catalog-backend-module-github@0.3.8-next.1 + +### Patch Changes + +- 3c44761b9191: Allow github user and team transforms to return any Entity +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-events-node@0.2.12-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-events-node@0.2.12-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.0 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + +## @backstage/plugin-catalog-common@1.0.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-catalog-graph@0.2.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-graphql@0.3.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-import@0.9.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + +## @backstage/plugin-catalog-node@1.4.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-react@1.8.4-next.1 + +### Patch Changes + +- bd817209ddd7: Export the `EntityAutocompletePicker` component. +- 7029ba1ce0af: Added delete alert popup when user delete the entity +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/integration@1.7.0-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-react@0.4.15-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## @backstage/plugin-catalog-unprocessed-entities@0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-cicd-statistics@0.1.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-cicd-statistics@0.1.26-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + +## @backstage/plugin-circleci@0.3.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-cloudbuild@0.3.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-code-climate@0.1.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-code-coverage@0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-code-coverage-backend@0.2.17-next.1 + +### Patch Changes + +- d409ed988aed: Correct the line hits calculation for Cobertura reports +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + +## @backstage/plugin-codescene@0.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-config-schema@0.1.45-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-cost-insights@0.12.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-cost-insights-common@0.1.1 + +## @backstage/plugin-devtools@0.1.4-next.1 + +### Patch Changes + +- 12e644aa4eef: Show resource utilization in `DevTools` plugin +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/plugin-devtools-common@0.1.4-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-permission-react@0.4.15-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-devtools-backend@0.1.6-next.1 + +### Patch Changes + +- 12e644aa4eef: Show resource utilization in `DevTools` plugin +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-devtools-common@0.1.4-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/config-loader@1.5.0-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-devtools-common@0.1.4-next.0 + +### Patch Changes + +- 12e644aa4eef: Show resource utilization in `DevTools` plugin +- Updated dependencies + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/types@1.1.0 + +## @backstage/plugin-dynatrace@7.0.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-entity-feedback@0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-entity-feedback-common@0.1.2 + +## @backstage/plugin-entity-feedback-backend@0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-entity-feedback-common@0.1.2 + +## @backstage/plugin-entity-validation@0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-events-backend@0.2.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-events-node@0.2.12-next.1 + +## @backstage/plugin-events-backend-module-aws-sqs@0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-events-node@0.2.12-next.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-events-backend-module-azure@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-events-node@0.2.12-next.1 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-events-node@0.2.12-next.1 + +## @backstage/plugin-events-backend-module-gerrit@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-events-node@0.2.12-next.1 + +## @backstage/plugin-events-backend-module-github@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-events-node@0.2.12-next.1 + +## @backstage/plugin-events-backend-module-gitlab@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-events-node@0.2.12-next.1 + +## @backstage/plugin-events-backend-test-utils@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.12-next.1 + +## @backstage/plugin-events-node@0.2.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.1 + +## @backstage/plugin-explore@0.4.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/plugin-search-react@1.7.0-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-explore-react@0.0.31-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-explore-backend@0.0.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/plugin-search-backend-module-explore@0.1.7-next.1 + - @backstage/types@1.1.0 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-explore-react@0.0.31-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-explore-common@0.0.1 + +## @backstage/plugin-firehydrant@0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-fossa@0.2.56-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-gcalendar@0.3.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-gcp-projects@0.3.41-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-git-release-manager@0.3.37-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/integration@1.7.0-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-github-actions@0.6.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/integration@1.7.0-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-github-deployments@0.1.55-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/integration@1.7.0-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-github-issues@0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/integration@1.7.0-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-github-pull-requests-board@0.1.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/integration@1.7.0-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-gitops-profiles@0.3.40-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-gocd@0.1.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-graphiql@0.2.54-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-graphql-backend@0.1.41-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/plugin-catalog-graphql@0.3.23-next.0 + +## @backstage/plugin-graphql-voyager@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-home@0.5.8-next.1 + +### Patch Changes + +- 98bb77c1f0b8: Align zod dependency to align with other packages. +- 1853ffa09b50: Allow specifying static widgets to custom home page +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-home-react@0.1.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-home-react@0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + +## @backstage/plugin-ilert@0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-jenkins@0.8.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-jenkins-common@0.1.19-next.0 + +## @backstage/plugin-jenkins-backend@0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + - @backstage/plugin-jenkins-common@0.1.19-next.0 + +## @backstage/plugin-jenkins-common@0.1.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + +## @backstage/plugin-kafka@0.3.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-kafka-backend@0.2.44-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/errors@1.2.1 + +## @backstage/plugin-kubernetes@0.10.3-next.1 + +### Patch Changes + +- 6a5e04e20e6e: fix logs dialog min height +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.6-next.0 + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-kubernetes-backend@0.11.6-next.1 + +### Patch Changes + +- ccf00accb408: Add AWS Annotations to Kubernetes Cluster Resource +- 47ea122590f5: fix "undefined" kind for custom resources +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.6-next.0 + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/integration-aws-node@0.1.6-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-kubernetes-common@0.6.6-next.0 + +### Patch Changes + +- ccf00accb408: Add AWS Annotations to Kubernetes Cluster Resource +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-permission-common@0.7.8-next.0 + +## @backstage/plugin-lighthouse@0.4.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-lighthouse-common@0.1.3-next.0 + - @backstage/theme@0.4.1 + +## @backstage/plugin-lighthouse-backend@0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-lighthouse-common@0.1.3-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/types@1.1.0 + +## @backstage/plugin-lighthouse-common@0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + +## @backstage/plugin-linguist@0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-linguist-common@0.1.1 + +## @backstage/plugin-linguist-backend@0.4.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-linguist-common@0.1.1 + +## @backstage/plugin-microsoft-calendar@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-newrelic@0.3.40-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-newrelic-dashboard@0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-nomad@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-nomad-backend@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-octopus-deploy@0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-opencost@0.2.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-org@0.6.14-next.1 + +### Patch Changes + +- 43a2137bb829: Ensure direct relations are shown for User entities while keeping support for aggregating closest parent group ownership +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-org-react@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/theme@0.4.1 + +## @backstage/plugin-pagerduty@0.6.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/plugin-home-react@0.1.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-periskop@0.1.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-periskop-backend@0.1.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + +## @backstage/plugin-permission-backend@0.5.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-permission-common@0.7.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-permission-node@0.7.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/errors@1.2.1 + +## @backstage/plugin-permission-react@0.4.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + +## @backstage/plugin-playlist@0.1.16-next.1 + +### Patch Changes + +- ec4b0f8cd957: change color of delete icon to secondary in playlist table +- b5ba33a92750: Limit the use of the same playlist name when adding a playlist +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/plugin-search-react@1.7.0-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-react@0.4.15-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-playlist-common@0.1.10-next.0 + +## @backstage/plugin-playlist-backend@0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + - @backstage/plugin-playlist-common@0.1.10-next.0 + +## @backstage/plugin-playlist-common@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.8-next.0 + +## @backstage/plugin-proxy-backend@0.3.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + +## @backstage/plugin-puppetdb@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-rollbar@0.4.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-rollbar-backend@0.1.48-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + +## @backstage/plugin-scaffolder@1.14.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/plugin-scaffolder-react@1.5.5-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-permission-react@0.4.15-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/plugin-scaffolder-common@1.4.1-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-scaffolder-backend@1.16.6-next.1 + +### Patch Changes + +- f3c0b95e3ef1: Add examples for `github:actions:dispatch` scaffolder actions. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/plugin-scaffolder-common@1.4.1-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-scaffolder-node@0.2.3-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-scaffolder-node@0.2.3-next.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-scaffolder-common@1.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/types@1.1.0 + +## @backstage/plugin-scaffolder-node@0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-scaffolder-common@1.4.1-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-scaffolder-react@1.5.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-scaffolder-common@1.4.1-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## @backstage/plugin-search-backend@1.4.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.1 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-search-backend-module-catalog@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-search-backend-node@1.2.7-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-search-backend-module-elasticsearch@1.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/integration-aws-node@0.1.6-next.0 + - @backstage/plugin-search-backend-node@1.2.7-next.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-search-backend-module-explore@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.1 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-search-backend-module-pg@0.5.12-next.1 + +### Patch Changes + +- 4ccf9204bc95: Added `indexerBatchSize` option to be able to control the size of the batches being indexed. Also added a debug log entry to list out all the entities in the batch +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-search-backend-module-techdocs@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/plugin-techdocs-node@1.8.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-search-backend-node@1.2.7-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-search-backend-node@1.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/errors@1.2.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-search-common@1.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/types@1.1.0 + +## @backstage/plugin-sentry@0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-shortcuts@0.3.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-sonarqube@0.7.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-sonarqube-react@0.1.8-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-sonarqube-backend@0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-sonarqube-react@0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + +## @backstage/plugin-splunk-on-call@0.4.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-stack-overflow@0.1.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-search-react@1.7.0-next.1 + - @backstage/plugin-home-react@0.1.3-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-stack-overflow-backend@0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-stackstorm@0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-tech-insights@0.3.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-tech-insights-common@0.2.11 + +## @backstage/plugin-tech-insights-backend@0.5.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-tech-insights-node@0.4.9-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-tech-insights-common@0.2.11 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/plugin-tech-insights-node@0.4.9-next.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-tech-insights-common@0.2.11 + +## @backstage/plugin-tech-insights-node@0.4.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/types@1.1.0 + - @backstage/plugin-tech-insights-common@0.2.11 + +## @backstage/plugin-tech-radar@0.6.8-next.1 + +### Patch Changes + +- 80b4d0b34f14: Fixed the width of the tech radar dialog so that the content fits +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-techdocs@1.6.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/plugin-search-react@1.7.0-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/plugin-techdocs-react@1.1.10-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/plugin-search-react@1.7.0-next.1 + - @backstage/plugin-catalog@1.12.5-next.1 + - @backstage/plugin-techdocs@1.6.9-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/plugin-techdocs-react@1.1.10-next.1 + - @backstage/core-app-api@1.10.0-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/test-utils@1.4.3-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-techdocs-react@1.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/version-bridge@1.0.4 + +## @backstage/plugin-todo@0.2.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-todo-backend@0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/errors@1.2.1 + +## @backstage/plugin-user-settings@0.7.9-next.1 + +### Patch Changes + +- ce77b23423cb: conditionally rendering the user email in user profile card +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-app-api@1.10.0-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-user-settings-backend@0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-vault@0.1.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-vault-backend@0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-xcmetrics@0.2.42-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## example-app@0.2.87-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-playlist@0.1.16-next.1 + - @backstage/plugin-tech-radar@0.6.8-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-org@0.6.14-next.1 + - @backstage/plugin-home@0.5.8-next.1 + - @backstage/plugin-devtools@0.1.4-next.1 + - @backstage/plugin-user-settings@0.7.9-next.1 + - @backstage/plugin-search-react@1.7.0-next.1 + - @backstage/plugin-search@1.4.0-next.1 + - @backstage/plugin-kubernetes@0.10.3-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.0-next.1 + - @backstage/plugin-adr@0.6.7-next.1 + - @backstage/plugin-airbrake@0.3.24-next.1 + - @backstage/plugin-api-docs@0.9.11-next.1 + - @backstage/plugin-azure-devops@0.3.6-next.1 + - @backstage/plugin-azure-sites@0.1.13-next.1 + - @backstage/plugin-badges@0.2.48-next.1 + - @internal/plugin-catalog-customized@0.0.14-next.1 + - @backstage/plugin-catalog-graph@0.2.36-next.1 + - @backstage/plugin-catalog-import@0.9.14-next.1 + - @backstage/plugin-circleci@0.3.24-next.1 + - @backstage/plugin-cloudbuild@0.3.24-next.1 + - @backstage/plugin-code-coverage@0.2.17-next.1 + - @backstage/plugin-cost-insights@0.12.13-next.1 + - @backstage/plugin-dynatrace@7.0.4-next.1 + - @backstage/plugin-entity-feedback@0.2.7-next.1 + - @backstage/plugin-explore@0.4.10-next.1 + - @backstage/plugin-github-actions@0.6.5-next.1 + - @backstage/plugin-gocd@0.1.30-next.1 + - @backstage/plugin-jenkins@0.8.6-next.1 + - @backstage/plugin-kafka@0.3.24-next.1 + - @backstage/plugin-lighthouse@0.4.9-next.1 + - @backstage/plugin-linguist@0.1.9-next.1 + - @backstage/plugin-newrelic-dashboard@0.2.17-next.1 + - @backstage/plugin-nomad@0.1.5-next.1 + - @backstage/plugin-octopus-deploy@0.2.6-next.1 + - @backstage/plugin-pagerduty@0.6.5-next.1 + - @backstage/plugin-puppetdb@0.1.7-next.1 + - @backstage/plugin-rollbar@0.4.24-next.1 + - @backstage/plugin-scaffolder@1.14.5-next.1 + - @backstage/plugin-scaffolder-react@1.5.5-next.1 + - @backstage/plugin-sentry@0.5.9-next.1 + - @backstage/plugin-tech-insights@0.3.16-next.1 + - @backstage/plugin-techdocs@1.6.9-next.1 + - @backstage/plugin-todo@0.2.26-next.1 + - @backstage/app-defaults@1.4.3-next.1 + - @backstage/cli@0.22.13-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/plugin-apache-airflow@0.2.15-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.1.3-next.1 + - @backstage/plugin-gcalendar@0.3.18-next.1 + - @backstage/plugin-gcp-projects@0.3.41-next.1 + - @backstage/plugin-graphiql@0.2.54-next.1 + - @backstage/plugin-microsoft-calendar@0.1.7-next.1 + - @backstage/plugin-newrelic@0.3.40-next.1 + - @backstage/plugin-shortcuts@0.3.14-next.1 + - @backstage/plugin-stack-overflow@0.1.20-next.1 + - @backstage/plugin-stackstorm@0.1.6-next.1 + - @backstage/plugin-techdocs-react@1.1.10-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-app-api@1.10.0-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-permission-react@0.4.15-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## example-app-next@0.0.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-playlist@0.1.16-next.1 + - @backstage/plugin-tech-radar@0.6.8-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-org@0.6.14-next.1 + - @backstage/plugin-home@0.5.8-next.1 + - @backstage/plugin-devtools@0.1.4-next.1 + - @backstage/plugin-user-settings@0.7.9-next.1 + - @backstage/plugin-search-react@1.7.0-next.1 + - @backstage/plugin-search@1.4.0-next.1 + - @backstage/plugin-kubernetes@0.10.3-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.0-next.1 + - @backstage/plugin-adr@0.6.7-next.1 + - @backstage/plugin-airbrake@0.3.24-next.1 + - @backstage/plugin-api-docs@0.9.11-next.1 + - @backstage/plugin-azure-devops@0.3.6-next.1 + - @backstage/plugin-azure-sites@0.1.13-next.1 + - @backstage/plugin-badges@0.2.48-next.1 + - @internal/plugin-catalog-customized@0.0.14-next.1 + - @backstage/plugin-catalog-graph@0.2.36-next.1 + - @backstage/plugin-catalog-import@0.9.14-next.1 + - @backstage/plugin-circleci@0.3.24-next.1 + - @backstage/plugin-cloudbuild@0.3.24-next.1 + - @backstage/plugin-code-coverage@0.2.17-next.1 + - @backstage/plugin-cost-insights@0.12.13-next.1 + - @backstage/plugin-dynatrace@7.0.4-next.1 + - @backstage/plugin-entity-feedback@0.2.7-next.1 + - @backstage/plugin-explore@0.4.10-next.1 + - @backstage/plugin-github-actions@0.6.5-next.1 + - @backstage/plugin-gocd@0.1.30-next.1 + - @backstage/plugin-jenkins@0.8.6-next.1 + - @backstage/plugin-kafka@0.3.24-next.1 + - @backstage/plugin-lighthouse@0.4.9-next.1 + - @backstage/plugin-linguist@0.1.9-next.1 + - @backstage/plugin-newrelic-dashboard@0.2.17-next.1 + - @backstage/plugin-octopus-deploy@0.2.6-next.1 + - @backstage/plugin-pagerduty@0.6.5-next.1 + - @backstage/plugin-puppetdb@0.1.7-next.1 + - @backstage/plugin-rollbar@0.4.24-next.1 + - @backstage/plugin-scaffolder@1.14.5-next.1 + - @backstage/plugin-scaffolder-react@1.5.5-next.1 + - @backstage/plugin-sentry@0.5.9-next.1 + - @backstage/plugin-tech-insights@0.3.16-next.1 + - @backstage/plugin-techdocs@1.6.9-next.1 + - @backstage/plugin-todo@0.2.26-next.1 + - @backstage/app-defaults@1.4.3-next.1 + - @backstage/cli@0.22.13-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/plugin-apache-airflow@0.2.15-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.1.3-next.1 + - @backstage/plugin-gcalendar@0.3.18-next.1 + - @backstage/plugin-gcp-projects@0.3.41-next.1 + - @backstage/plugin-graphiql@0.2.54-next.1 + - @backstage/plugin-microsoft-calendar@0.1.7-next.1 + - @backstage/plugin-newrelic@0.3.40-next.1 + - @backstage/plugin-shortcuts@0.3.14-next.1 + - @backstage/plugin-stack-overflow@0.1.20-next.1 + - @backstage/plugin-stackstorm@0.1.6-next.1 + - @backstage/plugin-techdocs-react@1.1.10-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-app-api@1.10.0-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/frontend-app-api@0.0.1-next.0 + - @backstage/plugin-permission-react@0.4.15-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/frontend-plugin-api@0.0.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## example-backend@0.2.87-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-module-pg@0.5.12-next.1 + - @backstage/plugin-kubernetes-backend@0.11.6-next.1 + - @backstage/plugin-catalog-backend@1.13.0-next.1 + - @backstage/plugin-auth-backend@0.18.9-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/plugin-devtools-backend@0.1.6-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/plugin-techdocs-backend@1.7.0-next.1 + - @backstage/plugin-scaffolder-backend@1.16.6-next.1 + - @backstage/plugin-code-coverage-backend@0.2.17-next.1 + - example-app@0.2.87-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-adr-backend@0.3.9-next.1 + - @backstage/plugin-app-backend@0.3.51-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-azure-devops-backend@0.3.30-next.1 + - @backstage/plugin-azure-sites-backend@0.1.13-next.1 + - @backstage/plugin-badges-backend@0.2.6-next.1 + - @backstage/plugin-entity-feedback-backend@0.1.9-next.1 + - @backstage/plugin-events-backend@0.2.12-next.1 + - @backstage/plugin-explore-backend@0.0.13-next.1 + - @backstage/plugin-graphql-backend@0.1.41-next.1 + - @backstage/plugin-jenkins-backend@0.2.6-next.1 + - @backstage/plugin-kafka-backend@0.2.44-next.1 + - @backstage/plugin-lighthouse-backend@0.2.7-next.1 + - @backstage/plugin-linguist-backend@0.4.3-next.1 + - @backstage/plugin-nomad-backend@0.1.5-next.1 + - @backstage/plugin-permission-backend@0.5.26-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/plugin-playlist-backend@0.3.7-next.1 + - @backstage/plugin-proxy-backend@0.3.3-next.1 + - @backstage/plugin-rollbar-backend@0.1.48-next.1 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.4-next.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.20-next.1 + - @backstage/plugin-search-backend@1.4.3-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.7-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.3.6-next.1 + - @backstage/plugin-search-backend-module-explore@0.1.7-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.7-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.1 + - @backstage/plugin-tech-insights-backend@0.5.17-next.1 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.35-next.1 + - @backstage/plugin-tech-insights-node@0.4.9-next.1 + - @backstage/plugin-todo-backend@0.2.3-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.3-next.1 + - @backstage/plugin-events-node@0.2.12-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-search-common@1.2.6-next.0 + +## example-backend-next@0.0.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-backend@0.11.6-next.1 + - @backstage/plugin-catalog-backend@1.13.0-next.1 + - @backstage/plugin-devtools-backend@0.1.6-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/plugin-techdocs-backend@1.7.0-next.1 + - @backstage/plugin-scaffolder-backend@1.16.6-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-adr-backend@0.3.9-next.1 + - @backstage/plugin-app-backend@0.3.51-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-azure-devops-backend@0.3.30-next.1 + - @backstage/plugin-badges-backend@0.2.6-next.1 + - @backstage/plugin-entity-feedback-backend@0.1.9-next.1 + - @backstage/plugin-lighthouse-backend@0.2.7-next.1 + - @backstage/plugin-linguist-backend@0.4.3-next.1 + - @backstage/plugin-permission-backend@0.5.26-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/plugin-proxy-backend@0.3.3-next.1 + - @backstage/plugin-search-backend@1.4.3-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.7-next.1 + - @backstage/plugin-search-backend-module-explore@0.1.7-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.7-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.1 + - @backstage/plugin-todo-backend@0.2.3-next.1 + - @backstage/backend-defaults@0.2.3-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.3-next.1 + +## e2e-test@0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.5-next.1 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + +## @backstage/frontend-app-api@0.0.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-graphiql@0.2.54-next.1 + - @backstage/frontend-plugin-api@0.0.0 + - @backstage/types@1.1.0 + +## techdocs-cli-embedded-app@0.2.86-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-catalog@1.12.5-next.1 + - @backstage/plugin-techdocs@1.6.9-next.1 + - @backstage/app-defaults@1.4.3-next.1 + - @backstage/cli@0.22.13-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/plugin-techdocs-react@1.1.10-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-app-api@1.10.0-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/test-utils@1.4.3-next.1 + - @backstage/theme@0.4.1 + +## @internal/plugin-catalog-customized@0.0.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/plugin-catalog@1.12.5-next.1 + +## @internal/plugin-todo-list@1.0.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## @internal/plugin-todo-list-backend@1.0.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/errors@1.2.1 + +## @internal/plugin-todo-list-common@1.0.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.8-next.0 diff --git a/docs/releases/v1.18.0-next.2-changelog.md b/docs/releases/v1.18.0-next.2-changelog.md new file mode 100644 index 0000000000..e28718c022 --- /dev/null +++ b/docs/releases/v1.18.0-next.2-changelog.md @@ -0,0 +1,2977 @@ +# Release v1.18.0-next.2 + +## @backstage/core-app-api@1.10.0-next.2 + +### Minor Changes + +- 6e30769cc627: Introduced experimental support for internationalization. + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## @backstage/core-plugin-api@1.6.0-next.2 + +### Minor Changes + +- 6e30769cc627: Introduced experimental support for internationalization. + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## @backstage/plugin-catalog@1.13.0-next.2 + +### Minor Changes + +- e44f45ac4515: This change allows a new annotation of `backstage.io/techdocs-entity` this ref allows you to reference another entity for its TechDocs. This allows you have a single TechDoc for all items in a system, for example you might have a frontend and a backend in the same repo. This would allow you to have TechDocs build under a `System` entity while referencing the system e.g.: `backstage.io/techdocs-entity: system:default/example` that will show the systems docs in both the TechDocs button and the TechDocs tab without needing to do duplicate builds and filling the TechDocs page with garbage. + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.19-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/plugin-search-react@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-scaffolder-common@1.4.1-next.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.1.0-next.0 + +### Minor Changes + +- 5f7b2153526b: Created package with policy `permissionModuleAllowAllPolicy` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + +## @backstage/plugin-techdocs@1.7.0-next.2 + +### Minor Changes + +- e44f45ac4515: This change allows a new annotation of `backstage.io/techdocs-entity` this ref allows you to reference another entity for its TechDocs. This allows you have a single TechDoc for all items in a system, for example you might have a frontend and a backend in the same repo. This would allow you to have TechDocs build under a `System` entity while referencing the system e.g.: `backstage.io/techdocs-entity: system:default/example` that will show the systems docs in both the TechDocs button and the TechDocs tab without needing to do duplicate builds and filling the TechDocs page with garbage. + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/integration-react@1.1.19-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/plugin-search-react@1.7.0-next.2 + - @backstage/plugin-techdocs-react@1.1.10-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/app-defaults@1.4.3-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/core-app-api@1.10.0-next.2 + - @backstage/plugin-permission-react@0.4.15-next.2 + - @backstage/theme@0.4.1 + +## @backstage/backend-app-api@0.5.3-next.2 + +### Patch Changes + +- 37a20c7f14aa: Adds include and exclude configuration to feature discovery of backend packages + Adds alpha modules to feature discovery +- 3fc64b9e2f8f: Extension points are now tracked via their ID rather than reference, in order to support package duplication. +- b219d097b3f4: Backend startup will now fail if any circular service dependencies are detected. +- Updated dependencies + - @backstage/config-loader@1.5.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/backend-common@0.19.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.5.3-next.2 + - @backstage/config-loader@1.5.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/integration@1.7.0-next.2 + - @backstage/integration-aws-node@0.1.6-next.1 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/backend-dev-utils@0.1.1 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/backend-defaults@0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.5.3-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + +## @backstage/backend-plugin-api@0.6.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/types@1.1.0 + +## @backstage/backend-tasks@0.5.8-next.2 + +### Patch Changes + +- 814feeed7343: Update to handle invalid luxon values +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/backend-test-utils@0.2.3-next.2 + +### Patch Changes + +- 58cb5e5cea7b: Introduced a new utility for testing service factories, `ServiceFactoryTester`. +- Updated dependencies + - @backstage/backend-app-api@0.5.3-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/types@1.1.0 + +## @backstage/catalog-client@1.4.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + +## @backstage/catalog-model@1.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/cli@0.22.13-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- ea779492ad88: Updated dependency `run-script-webpack-plugin` to `^0.2.0`. +- Updated dependencies + - @backstage/release-manifests@0.0.10-next.0 + - @backstage/config-loader@1.5.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + - @backstage/eslint-plugin@0.1.3 + - @backstage/types@1.1.0 + +## @backstage/config@1.1.0-next.1 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/config-loader@1.5.0-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/core-components@0.13.5-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- db92d1244897: Updated dependency `rc-progress` to `3.5.1`. +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/version-bridge@1.0.4 + +## @backstage/create-app@0.5.5-next.2 + +### Patch Changes + +- 8db70ecbb325: Updated Dockerfile to include `apt-get update` when installing the SQLite dependency +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/cli-common@0.1.12 + +## @backstage/dev-utils@1.0.21-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/integration-react@1.1.19-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/app-defaults@1.4.3-next.2 + - @backstage/core-app-api@1.10.0-next.2 + - @backstage/test-utils@1.4.3-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/integration@1.7.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/errors@1.2.1 + +## @backstage/integration-aws-node@0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/errors@1.2.1 + +## @backstage/integration-react@1.1.19-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/integration@1.7.0-next.2 + - @backstage/theme@0.4.1 + +## @backstage/release-manifests@0.0.10-next.0 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency + +## @backstage/repo-tools@0.3.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + +## @techdocs/cli@1.5.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-techdocs-node@1.8.0-next.2 + - @backstage/cli-common@0.1.12 + +## @backstage/test-utils@1.4.3-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/core-app-api@1.10.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-permission-react@0.4.15-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-adr@0.6.7-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/integration-react@1.1.19-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/plugin-search-react@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-adr-common@0.2.15-next.2 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-adr-backend@0.3.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-adr-common@0.2.15-next.2 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-adr-common@0.2.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-airbrake@0.3.24-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/test-utils@1.4.3-next.2 + - @backstage/dev-utils@1.0.21-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-airbrake-backend@0.2.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + +## @backstage/plugin-allure@0.1.40-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-analytics-module-ga@0.1.33-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-analytics-module-ga4@0.1.4-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-analytics-module-newrelic-browser@0.0.2-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + +## @backstage/plugin-apache-airflow@0.2.15-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + +## @backstage/plugin-api-docs@0.9.11-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/plugin-catalog@1.13.0-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-apollo-explorer@0.1.15-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/theme@0.4.1 + +## @backstage/plugin-app-backend@0.3.51-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.5.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/types@1.1.0 + - @backstage/plugin-app-node@0.1.3-next.2 + +## @backstage/plugin-app-node@0.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.2 + +## @backstage/plugin-auth-backend@0.18.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.1.0-next.2 + - @backstage/plugin-auth-backend-module-github-provider@0.1.0-next.2 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.0-next.1 + - @backstage/plugin-auth-backend-module-google-provider@0.1.0-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-auth-backend-module-github-provider@0.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + +## @backstage/plugin-auth-backend-module-google-provider@0.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + +## @backstage/plugin-auth-node@0.3.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-azure-devops@0.3.6-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-devops-backend@0.3.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-sites@0.1.13-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-azure-sites-backend@0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-badges@0.2.48-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-badges-backend@0.2.6-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-bazaar@0.2.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.13.0-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/cli@0.22.13-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-bazaar-backend@0.2.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/errors@1.2.1 + +## @backstage/plugin-bitbucket-cloud-common@0.2.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.2 + +## @backstage/plugin-bitrise@0.1.51-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-catalog-backend@1.13.0-next.2 + +### Patch Changes + +- acffa17027b6: Added some examples to the catalog OpenAPI definition +- 45947d3b2759: Fixes an issue where `order` was not a recognized parameter for the `/entities` endpoint. +- 814feeed7343: Update to handle invalid luxon values +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.7-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-events-node@0.2.12-next.2 + - @backstage/plugin-scaffolder-common@1.4.1-next.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-catalog-backend-module-aws@0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/integration-aws-node@0.1.6-next.1 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-kubernetes-common@0.6.6-next.1 + +## @backstage/plugin-catalog-backend-module-azure@0.1.22-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.12-next.2 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.1.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/plugin-bitbucket-cloud-common@0.2.12-next.2 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-events-node@0.2.12-next.2 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-kubernetes-common@0.6.6-next.1 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-catalog-backend-module-github@0.3.8-next.2 + +### Patch Changes + +- 96353bb7cb4a: Properly support custom `userTransformer` returning `undefined` in `GithubMultiOrgEntityProvider` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-events-node@0.2.12-next.2 + +## @backstage/plugin-catalog-backend-module-gitlab@0.2.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-events-node@0.2.12-next.2 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + +## @backstage/plugin-catalog-common@1.0.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-catalog-graph@0.2.36-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-graphql@0.3.23-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-catalog-import@0.9.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.19-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + +## @backstage/plugin-catalog-node@1.4.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + +## @backstage/plugin-catalog-react@1.8.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-permission-react@0.4.15-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-catalog-common@1.0.16-next.1 + +## @backstage/plugin-catalog-unprocessed-entities@0.1.3-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-cicd-statistics@0.1.26-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-cicd-statistics@0.1.26-next.2 + - @backstage/catalog-model@1.4.2-next.1 + +## @backstage/plugin-circleci@0.3.24-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-cloudbuild@0.3.24-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- 814feeed7343: Update to handle invalid luxon values +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-code-climate@0.1.24-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-code-coverage@0.2.17-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-code-coverage-backend@0.2.17-next.2 + +### Patch Changes + +- df005d9932fb: Add text body parser for LCOV reports + +- 91ccb56fb418: Replace `express-xml-bodyparser` with `body-parser-xml`. + + `express-xml-bodyparser` was last updated 8 years ago + and currently depends on a version of `xml2js` which + contains a vulnerability. + + This change will swap it out in favor of `body-parser-xml` + which is more maintained and depends on a more recent `xml2js` + version without the vulnerability. + +- 33e606a797ef: Include auth token when fetching entity + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-codescene@0.1.17-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- db92d1244897: Updated dependency `rc-progress` to `3.5.1`. +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-config-schema@0.1.45-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-cost-insights@0.12.13-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-cost-insights-common@0.1.1 + +## @backstage/plugin-devtools@0.1.4-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-permission-react@0.4.15-next.2 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-devtools-common@0.1.4-next.1 + +## @backstage/plugin-devtools-backend@0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.5.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-devtools-common@0.1.4-next.1 + +## @backstage/plugin-devtools-common@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-dynatrace@7.0.4-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-entity-feedback@0.2.7-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-entity-feedback-common@0.1.2 + +## @backstage/plugin-entity-feedback-backend@0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/plugin-entity-feedback-common@0.1.2 + +## @backstage/plugin-entity-validation@0.1.9-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + +## @backstage/plugin-events-backend@0.2.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-events-node@0.2.12-next.2 + +## @backstage/plugin-events-backend-module-aws-sqs@0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/types@1.1.0 + - @backstage/plugin-events-node@0.2.12-next.2 + +## @backstage/plugin-events-backend-module-azure@0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-events-node@0.2.12-next.2 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-events-node@0.2.12-next.2 + +## @backstage/plugin-events-backend-module-gerrit@0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-events-node@0.2.12-next.2 + +## @backstage/plugin-events-backend-module-github@0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-events-node@0.2.12-next.2 + +## @backstage/plugin-events-backend-module-gitlab@0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-events-node@0.2.12-next.2 + +## @backstage/plugin-events-backend-test-utils@0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.12-next.2 + +## @backstage/plugin-events-node@0.2.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.2 + +## @backstage/plugin-explore@0.4.10-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-explore-react@0.0.31-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/plugin-search-react@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-explore-backend@0.0.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.7-next.2 + - @backstage/types@1.1.0 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-explore-react@0.0.31-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-explore-common@0.0.1 + +## @backstage/plugin-firehydrant@0.2.8-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-fossa@0.2.56-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-gcalendar@0.3.18-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- 814feeed7343: Update to handle invalid luxon values +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-gcp-projects@0.3.41-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/theme@0.4.1 + +## @backstage/plugin-git-release-manager@0.3.37-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/theme@0.4.1 + +## @backstage/plugin-github-actions@0.6.5-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/integration-react@1.1.19-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-github-deployments@0.1.55-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/integration-react@1.1.19-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-github-issues@0.2.13-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-github-pull-requests-board@0.1.18-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-gitops-profiles@0.3.40-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-gocd@0.1.30-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-graphiql@0.2.54-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/theme@0.4.1 + +## @backstage/plugin-graphql-backend@0.1.41-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-graphql@0.3.23-next.1 + +## @backstage/plugin-graphql-voyager@0.1.7-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/theme@0.4.1 + +## @backstage/plugin-home@0.5.8-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-home-react@0.1.3-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-home-react@0.1.3-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + +## @backstage/plugin-ilert@0.2.13-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- 814feeed7343: Update to handle invalid luxon values +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-jenkins@0.8.6-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-jenkins-common@0.1.19-next.1 + +## @backstage/plugin-jenkins-backend@0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-jenkins-common@0.1.19-next.1 + +## @backstage/plugin-jenkins-common@0.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + +## @backstage/plugin-kafka@0.3.24-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-kafka-backend@0.2.44-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-kubernetes@0.10.3-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-kubernetes-common@0.6.6-next.1 + +## @backstage/plugin-kubernetes-backend@0.11.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/integration-aws-node@0.1.6-next.1 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-kubernetes-common@0.6.6-next.1 + +## @backstage/plugin-kubernetes-common@0.6.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + +## @backstage/plugin-lighthouse@0.4.9-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-lighthouse-common@0.1.3-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-lighthouse-backend@0.2.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-lighthouse-common@0.1.3-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-lighthouse-common@0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + +## @backstage/plugin-linguist@0.1.9-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-linguist-common@0.1.1 + +## @backstage/plugin-linguist-backend@0.4.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-linguist-common@0.1.1 + +## @backstage/plugin-microsoft-calendar@0.1.7-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- 814feeed7343: Update to handle invalid luxon values +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-newrelic@0.3.40-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/theme@0.4.1 + +## @backstage/plugin-newrelic-dashboard@0.2.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-nomad@0.1.5-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-nomad-backend@0.1.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/errors@1.2.1 + +## @backstage/plugin-octopus-deploy@0.2.6-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-opencost@0.2.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/theme@0.4.1 + +## @backstage/plugin-org@0.6.14-next.2 + +### Patch Changes + +- dd9329caea1c: Entity relations toggle should by default be aggregated for User entities +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-org-react@0.1.13-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-pagerduty@0.6.5-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-home-react@0.1.3-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-periskop@0.1.22-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-periskop-backend@0.1.22-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + +## @backstage/plugin-permission-backend@0.5.26-next.2 + +### Patch Changes + +- 84ad6fccd4d5: Moved `permissionModuleAllowAllPolicy` to `@backstage/plugin-permission-backend-module-allow-all-policy` +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-permission-common@0.7.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-permission-node@0.7.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-permission-react@0.4.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + +## @backstage/plugin-playlist@0.1.16-next.2 + +### Patch Changes + +- 79847cb73ed7: Updated latest playlist screen in readme document +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/plugin-search-react@1.7.0-next.2 + - @backstage/plugin-permission-react@0.4.15-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-playlist-common@0.1.10-next.1 + +## @backstage/plugin-playlist-backend@0.3.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-playlist-common@0.1.10-next.1 + +## @backstage/plugin-playlist-common@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.8-next.1 + +## @backstage/plugin-proxy-backend@0.3.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + +## @backstage/plugin-puppetdb@0.1.7-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-rollbar@0.4.24-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-rollbar-backend@0.1.48-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + +## @backstage/plugin-scaffolder@1.14.5-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/integration-react@1.1.19-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-scaffolder-react@1.5.5-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/plugin-permission-react@0.4.15-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-scaffolder-common@1.4.1-next.1 + +## @backstage/plugin-scaffolder-backend@1.16.6-next.2 + +### Patch Changes + +- ded27b83ead2: Add examples for `publish:bitbucket` scaffolder actions. +- 814feeed7343: Update to handle invalid luxon values +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.2 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-scaffolder-common@1.4.1-next.1 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/plugin-scaffolder-node@0.2.3-next.2 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/plugin-scaffolder-node@0.2.3-next.2 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/integration@1.7.0-next.2 + - @backstage/plugin-scaffolder-node@0.2.3-next.2 + - @backstage/errors@1.2.1 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/plugin-scaffolder-node@0.2.3-next.2 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.2 + - @backstage/errors@1.2.1 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.2 + - @backstage/types@1.1.0 + +## @backstage/plugin-scaffolder-common@1.4.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-scaffolder-node@0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.5-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-common@1.4.1-next.1 + +## @backstage/plugin-scaffolder-react@1.5.5-next.2 + +### Patch Changes + +- 27fef07f9229: Updated dependency `use-immer` to `^0.9.0`. +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-scaffolder-common@1.4.1-next.1 + +## @backstage/plugin-search@1.4.0-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/plugin-search-react@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-search-backend@1.4.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-search-backend-module-catalog@0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-search-backend-module-elasticsearch@1.3.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/integration-aws-node@0.1.6-next.1 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-search-backend-module-explore@0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-search-backend-module-pg@0.5.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-search-backend-module-techdocs@0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/plugin-techdocs-node@1.8.0-next.2 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-search-backend-node@1.2.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-search-common@1.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-search-react@1.7.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-sentry@0.5.9-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-shortcuts@0.3.14-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-sonarqube@0.7.5-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- db92d1244897: Updated dependency `rc-progress` to `3.5.1`. +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/plugin-sonarqube-react@0.1.8-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-sonarqube-backend@0.2.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/errors@1.2.1 + +## @backstage/plugin-sonarqube-react@0.1.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + +## @backstage/plugin-splunk-on-call@0.4.13-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-stack-overflow@0.1.20-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-home-react@0.1.3-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-search-react@1.7.0-next.2 + - @backstage/theme@0.4.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-stack-overflow-backend@0.2.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-stackstorm@0.1.6-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-tech-insights@0.3.16-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-tech-insights-common@0.2.11 + +## @backstage/plugin-tech-insights-backend@0.5.17-next.2 + +### Patch Changes + +- 814feeed7343: Update to handle invalid luxon values +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-tech-insights-node@0.4.9-next.2 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-tech-insights-common@0.2.11 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.35-next.2 + +### Patch Changes + +- 51b801f743b2: Handle extracting facts from 'not' conditions too +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-tech-insights-node@0.4.9-next.2 + - @backstage/errors@1.2.1 + - @backstage/plugin-tech-insights-common@0.2.11 + +## @backstage/plugin-tech-insights-node@0.4.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/types@1.1.0 + - @backstage/plugin-tech-insights-common@0.2.11 + +## @backstage/plugin-tech-radar@0.6.8-next.2 + +### Patch Changes + +- b45d0ac68191: Add names to blip entry links in the radar to improve accessibility +- 6d35e9ea2afa: Add description property for Rings which will be shown in the footer. +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/theme@0.4.1 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.21-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/plugin-techdocs@1.7.0-next.2 + - @backstage/plugin-catalog@1.13.0-next.2 + - @backstage/integration-react@1.1.19-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/core-app-api@1.10.0-next.2 + - @backstage/test-utils@1.4.3-next.2 + - @backstage/plugin-search-react@1.7.0-next.2 + - @backstage/plugin-techdocs-react@1.1.10-next.2 + - @backstage/theme@0.4.1 + +## @backstage/plugin-techdocs-backend@1.7.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-search-backend-module-techdocs@0.1.7-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-techdocs-node@1.8.0-next.2 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.0-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/integration-react@1.1.19-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-techdocs-react@1.1.10-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/theme@0.4.1 + +## @backstage/plugin-techdocs-node@1.8.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/integration-aws-node@0.1.6-next.1 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## @backstage/plugin-techdocs-react@1.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/version-bridge@1.0.4 + +## @backstage/plugin-todo@0.2.26-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-todo-backend@0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + +## @backstage/plugin-user-settings@0.7.9-next.2 + +### Patch Changes + +- e03f3ee2be94: changed auto theme tooltip title to camel case +- 8cec7664e146: Removed `@types/node` dependency +- 6e30769cc627: Introduced experimental support for internationalization. +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/core-app-api@1.10.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-user-settings-backend@0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## @backstage/plugin-vault@0.1.18-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## @backstage/plugin-vault-backend@0.3.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/errors@1.2.1 + +## @backstage/plugin-xcmetrics@0.2.42-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- 814feeed7343: Update to handle invalid luxon values +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## example-app@0.2.87-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-playlist@0.1.16-next.2 + - @backstage/plugin-tech-radar@0.6.8-next.2 + - @backstage/plugin-user-settings@0.7.9-next.2 + - @backstage/plugin-org@0.6.14-next.2 + - @backstage/plugin-techdocs@1.7.0-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.0-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.1.3-next.2 + - @backstage/integration-react@1.1.19-next.2 + - @backstage/plugin-microsoft-calendar@0.1.7-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-entity-feedback@0.2.7-next.2 + - @backstage/plugin-apache-airflow@0.2.15-next.2 + - @backstage/plugin-github-actions@0.6.5-next.2 + - @backstage/plugin-octopus-deploy@0.2.6-next.2 + - @backstage/plugin-stack-overflow@0.1.20-next.2 + - @backstage/app-defaults@1.4.3-next.2 + - @backstage/core-app-api@1.10.0-next.2 + - @backstage/plugin-code-coverage@0.2.17-next.2 + - @backstage/plugin-cost-insights@0.12.13-next.2 + - @backstage/plugin-tech-insights@0.3.16-next.2 + - @backstage/plugin-azure-devops@0.3.6-next.2 + - @backstage/plugin-gcp-projects@0.3.41-next.2 + - @backstage/plugin-azure-sites@0.1.13-next.2 + - @backstage/plugin-cloudbuild@0.3.24-next.2 + - @backstage/plugin-kubernetes@0.10.3-next.2 + - @backstage/plugin-lighthouse@0.4.9-next.2 + - @backstage/plugin-scaffolder@1.14.5-next.2 + - @backstage/plugin-stackstorm@0.1.6-next.2 + - @backstage/plugin-dynatrace@7.0.4-next.2 + - @backstage/plugin-gcalendar@0.3.18-next.2 + - @backstage/plugin-pagerduty@0.6.5-next.2 + - @backstage/plugin-shortcuts@0.3.14-next.2 + - @backstage/plugin-airbrake@0.3.24-next.2 + - @backstage/plugin-api-docs@0.9.11-next.2 + - @backstage/plugin-circleci@0.3.24-next.2 + - @backstage/plugin-devtools@0.1.4-next.2 + - @backstage/plugin-graphiql@0.2.54-next.2 + - @backstage/plugin-linguist@0.1.9-next.2 + - @backstage/plugin-newrelic@0.3.40-next.2 + - @backstage/plugin-puppetdb@0.1.7-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-explore@0.4.10-next.2 + - @backstage/plugin-jenkins@0.8.6-next.2 + - @backstage/plugin-rollbar@0.4.24-next.2 + - @backstage/plugin-badges@0.2.48-next.2 + - @backstage/plugin-search@1.4.0-next.2 + - @backstage/plugin-sentry@0.5.9-next.2 + - @backstage/plugin-kafka@0.3.24-next.2 + - @backstage/plugin-nomad@0.1.5-next.2 + - @backstage/cli@0.22.13-next.2 + - @backstage/plugin-gocd@0.1.30-next.2 + - @backstage/plugin-home@0.5.8-next.2 + - @backstage/plugin-todo@0.2.26-next.2 + - @backstage/plugin-adr@0.6.7-next.2 + - @backstage/plugin-scaffolder-react@1.5.5-next.2 + - @internal/plugin-catalog-customized@0.0.14-next.2 + - @backstage/plugin-catalog-graph@0.2.36-next.2 + - @backstage/plugin-catalog-import@0.9.14-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/plugin-newrelic-dashboard@0.2.17-next.2 + - @backstage/plugin-search-react@1.7.0-next.2 + - @backstage/plugin-techdocs-react@1.1.10-next.2 + - @backstage/plugin-permission-react@0.4.15-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## example-app-next@0.0.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-playlist@0.1.16-next.2 + - @backstage/plugin-tech-radar@0.6.8-next.2 + - @backstage/plugin-user-settings@0.7.9-next.2 + - @backstage/plugin-org@0.6.14-next.2 + - @backstage/plugin-techdocs@1.7.0-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.0-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.1.3-next.2 + - @backstage/integration-react@1.1.19-next.2 + - @backstage/plugin-microsoft-calendar@0.1.7-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-entity-feedback@0.2.7-next.2 + - @backstage/plugin-apache-airflow@0.2.15-next.2 + - @backstage/plugin-github-actions@0.6.5-next.2 + - @backstage/plugin-octopus-deploy@0.2.6-next.2 + - @backstage/plugin-stack-overflow@0.1.20-next.2 + - @backstage/app-defaults@1.4.3-next.2 + - @backstage/core-app-api@1.10.0-next.2 + - @backstage/plugin-code-coverage@0.2.17-next.2 + - @backstage/plugin-cost-insights@0.12.13-next.2 + - @backstage/plugin-tech-insights@0.3.16-next.2 + - @backstage/plugin-azure-devops@0.3.6-next.2 + - @backstage/plugin-gcp-projects@0.3.41-next.2 + - @backstage/plugin-azure-sites@0.1.13-next.2 + - @backstage/plugin-cloudbuild@0.3.24-next.2 + - @backstage/plugin-kubernetes@0.10.3-next.2 + - @backstage/plugin-lighthouse@0.4.9-next.2 + - @backstage/plugin-scaffolder@1.14.5-next.2 + - @backstage/plugin-stackstorm@0.1.6-next.2 + - @backstage/plugin-dynatrace@7.0.4-next.2 + - @backstage/plugin-gcalendar@0.3.18-next.2 + - @backstage/plugin-pagerduty@0.6.5-next.2 + - @backstage/plugin-shortcuts@0.3.14-next.2 + - @backstage/plugin-airbrake@0.3.24-next.2 + - @backstage/plugin-api-docs@0.9.11-next.2 + - @backstage/plugin-circleci@0.3.24-next.2 + - @backstage/plugin-devtools@0.1.4-next.2 + - @backstage/plugin-graphiql@0.2.54-next.2 + - @backstage/plugin-linguist@0.1.9-next.2 + - @backstage/plugin-newrelic@0.3.40-next.2 + - @backstage/plugin-puppetdb@0.1.7-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-explore@0.4.10-next.2 + - @backstage/plugin-jenkins@0.8.6-next.2 + - @backstage/plugin-rollbar@0.4.24-next.2 + - @backstage/plugin-badges@0.2.48-next.2 + - @backstage/plugin-search@1.4.0-next.2 + - @backstage/plugin-sentry@0.5.9-next.2 + - @backstage/plugin-kafka@0.3.24-next.2 + - @backstage/cli@0.22.13-next.2 + - @backstage/plugin-gocd@0.1.30-next.2 + - @backstage/plugin-home@0.5.8-next.2 + - @backstage/plugin-todo@0.2.26-next.2 + - @backstage/plugin-adr@0.6.7-next.2 + - @backstage/plugin-scaffolder-react@1.5.5-next.2 + - @internal/plugin-catalog-customized@0.0.14-next.2 + - @backstage/plugin-catalog-graph@0.2.36-next.2 + - @backstage/plugin-catalog-import@0.9.14-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/plugin-newrelic-dashboard@0.2.17-next.2 + - @backstage/plugin-search-react@1.7.0-next.2 + - @backstage/plugin-techdocs-react@1.1.10-next.2 + - @backstage/frontend-app-api@0.0.1-next.1 + - @backstage/frontend-plugin-api@0.0.1-next.0 + - @backstage/plugin-permission-react@0.4.15-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## example-backend@0.2.87-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.16.6-next.2 + - @backstage/plugin-code-coverage-backend@0.2.17-next.2 + - @backstage/plugin-permission-backend@0.5.26-next.2 + - @backstage/plugin-catalog-backend@1.13.0-next.2 + - @backstage/plugin-badges-backend@0.2.6-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.35-next.2 + - @backstage/plugin-tech-insights-backend@0.5.17-next.2 + - @backstage/backend-tasks@0.5.8-next.2 + - example-app@0.2.87-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-app-backend@0.3.51-next.2 + - @backstage/plugin-auth-backend@0.18.9-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/plugin-entity-feedback-backend@0.1.9-next.2 + - @backstage/plugin-events-backend@0.2.12-next.2 + - @backstage/plugin-kubernetes-backend@0.11.6-next.2 + - @backstage/plugin-linguist-backend@0.4.3-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/plugin-playlist-backend@0.3.7-next.2 + - @backstage/plugin-proxy-backend@0.3.3-next.2 + - @backstage/plugin-rollbar-backend@0.1.48-next.2 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.4-next.2 + - @backstage/plugin-search-backend@1.4.3-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.7-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.7-next.2 + - @backstage/plugin-search-backend-module-pg@0.5.12-next.2 + - @backstage/plugin-search-backend-module-techdocs@0.1.7-next.2 + - @backstage/plugin-techdocs-backend@1.7.0-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/plugin-devtools-backend@0.1.6-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-adr-backend@0.3.9-next.2 + - @backstage/plugin-azure-devops-backend@0.3.30-next.2 + - @backstage/plugin-azure-sites-backend@0.1.13-next.2 + - @backstage/plugin-explore-backend@0.0.13-next.2 + - @backstage/plugin-graphql-backend@0.1.41-next.2 + - @backstage/plugin-jenkins-backend@0.2.6-next.2 + - @backstage/plugin-kafka-backend@0.2.44-next.2 + - @backstage/plugin-lighthouse-backend@0.2.7-next.2 + - @backstage/plugin-nomad-backend@0.1.5-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.20-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.3.6-next.2 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/plugin-tech-insights-node@0.4.9-next.2 + - @backstage/plugin-todo-backend@0.2.3-next.2 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.3-next.2 + - @backstage/plugin-events-node@0.2.12-next.2 + - @backstage/plugin-search-common@1.2.6-next.1 + +## example-backend-next@0.0.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.16.6-next.2 + - @backstage/plugin-permission-backend@0.5.26-next.2 + - @backstage/plugin-catalog-backend@1.13.0-next.2 + - @backstage/plugin-badges-backend@0.2.6-next.2 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-defaults@0.2.3-next.2 + - @backstage/plugin-app-backend@0.3.51-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-entity-feedback-backend@0.1.9-next.2 + - @backstage/plugin-kubernetes-backend@0.11.6-next.2 + - @backstage/plugin-linguist-backend@0.4.3-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/plugin-proxy-backend@0.3.3-next.2 + - @backstage/plugin-search-backend@1.4.3-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.7-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.7-next.2 + - @backstage/plugin-search-backend-module-techdocs@0.1.7-next.2 + - @backstage/plugin-techdocs-backend@1.7.0-next.2 + - @backstage/plugin-devtools-backend@0.1.6-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-adr-backend@0.3.9-next.2 + - @backstage/plugin-azure-devops-backend@0.3.30-next.2 + - @backstage/plugin-lighthouse-backend@0.2.7-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/plugin-todo-backend@0.2.3-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.3-next.2 + +## @backstage/backend-plugin-manager@0.0.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-events-backend@0.2.12-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.2 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-events-node@0.2.12-next.2 + - @backstage/plugin-search-common@1.2.6-next.1 + +## e2e-test@0.2.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.5-next.2 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + +## @backstage/frontend-app-api@0.0.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-graphiql@0.2.54-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/frontend-plugin-api@0.0.1-next.0 + - @backstage/types@1.1.0 + +## @backstage/frontend-plugin-api@0.0.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/types@1.1.0 + +## techdocs-cli-embedded-app@0.2.86-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.7.0-next.2 + - @backstage/plugin-catalog@1.13.0-next.2 + - @backstage/integration-react@1.1.19-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/app-defaults@1.4.3-next.2 + - @backstage/core-app-api@1.10.0-next.2 + - @backstage/test-utils@1.4.3-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/cli@0.22.13-next.2 + - @backstage/plugin-techdocs-react@1.1.10-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## @internal/plugin-catalog-customized@0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.13.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + +## @internal/plugin-todo-list@1.0.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/theme@0.4.1 + +## @internal/plugin-todo-list-backend@1.0.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/errors@1.2.1 + +## @internal/plugin-todo-list-common@1.0.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.8-next.1 diff --git a/lighthouserc.js b/lighthouserc.js index 90a15abbd3..70136ee158 100644 --- a/lighthouserc.js +++ b/lighthouserc.js @@ -32,6 +32,17 @@ module.exports = { 'http://localhost:3000/create/templates/default/react-ssr-template', /** Search */ 'http://localhost:3000/search', + /** Miscellaneous */ + 'http://localhost:3000/settings', + /** plugin-devtools */ + 'http://localhost:3000/devtools', + 'http://localhost:3000/devtools/config', + /** plugin-explore */ + 'http://localhost:3000/explore', + 'http://localhost:3000/explore/groups', + 'http://localhost:3000/explore/tools', + /** plugin-tech-radar */ + 'http://localhost:3000/tech-radar', ], settings: { onlyCategories: ['accessibility'], diff --git a/microsite/blog/2023-08-17-expedia-proof-of-value-metrics-2.mdx b/microsite/blog/2023-08-17-expedia-proof-of-value-metrics-2.mdx new file mode 100644 index 0000000000..9c6e67fcf1 --- /dev/null +++ b/microsite/blog/2023-08-17-expedia-proof-of-value-metrics-2.mdx @@ -0,0 +1,206 @@ +--- +# prettier-ignore +title: "Expedia Group shares Backstage proof of value metrics 2.0" +author: Guillermo Manzo, Expedia Group +authorURL: https://www.linkedin.com/in/guillermomanzo +--- + +![backstage header](assets/2023-08-17/header.png) + +_Editor's Note: In this post, Expedia Group provides a follow-up to their initial Q4 2021 [post on how they measure Backstage proof of value](https://backstage.spotify.com/blog/measuring-backstage-proof-of-value-at-expedia/). +We're grateful to the Expedia Group team for sharing their processes, revised proof of value metrics, and adaptations to their approach over time in hopes of providing a successful framework for the rest of the Backstage community._ + +[Expedia Group's](https://expediagroup.com) mission is to power global travel for everyone, everywhere. +This vision is made possible through Expedia Group's massive platform, which connects 168 million loyalty members, 50 thousand B2B partners, 3 million properties and 500 airlines, car rentals and cruise lines across the globe. In short, the scale of our platform is enormous, which means that we need to scale our engineering capabilities as well so we can continue to provide seamless experiences to our travelers and partners. + +{/* truncate */} + +In August 2020, we adopted Backstage to enhance our foundational platform capabilities and reduce developer friction in the process. Its framework proved scalable for us and as EG's platform evolved, so did our application of Backstage. Since our [initial share about our Backstage deployment](https://backstage.spotify.com/blog/measuring-backstage-proof-of-value-at-expedia/), we've continued to follow our process of: + +- Syncing Expedia Group's internal Developer Journey phases by key DevOps processes and Software Delivery Life Cycle (SDLC) milestones (POC, Alpha, Beta, etc). +- Identifying Developer Jobs to be Done (JTBD) by our Internal Developer Journey phases +- Aligning developer JTBD's to proof of value (PoV) metrics. + +In the diagram below, you can see how we distilled these processes into Backstage PoV metrics. + +![backstage pov metrics](assets/2023-08-17/table.png) + +To ground ourselves in the evolving developer and Backstage landscape at Expedia Group, we constantly ask ourselves, "How can we continue to show the value of Backstage to our customers and the Developer Community?" To fully answer this question, we adapted our initial proof of value metrics to reinforce our alignment to the current landscape of the Expedia Group developer journey. + +## Expedia Group proof of value metrics 2.0 + +We are excited to share our PoV metrics because it affirms our investment in Backstage for Expedia Group's Backstage users and provides a compelling metrics-based answer on why more organizations should consider adopting it. + +As our Backstage application matured, we updated our PoV metrics and aligned our 2023 roadmap to reflect these changes. Below, we highlight key wins from 2022 and outline our long-term vision for each metric. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PoV MetricDescription2022 Key Win(s)Long-Term Vision
"Ship on Day One" / onboarding metrics +

+ A metric used to determine an engineer's readiness to develop at EG + during their onboarding process. +

+

+ It measures the time from first commit to deploying code in + production, using a "Hello World Template" +

+
+
    +
  • + 850+ engineers went through the “Ship on Day One” Bootcamp when + first joining Expedia Group +
  • +
  • + We had a success rate of ~95% of engineers completing bootcamp on + first try +
  • +
  • 7,000+ components created or onboarded
  • +
  • 4-min average per app creation/onboarding
  • +
+
+
    +
  • + Develop full onboarding curriculum coverage for all critical + capabilities offered within EG - Example: performance and quality + management, pipeline management, etc. +
  • +
  • + Create an end-to-end guided tour of the EG Developer Journey w/ + functional training +
  • +
+
+ Reduce context switching through integration with core EG capabilities + + Through plugins, tool integration, reduces context-switching as + engineers will not need to toggle between multiple tools + +
    +
  • + 15%+ of internal developer tools are integrated within Backstage, + reducing the amount of context-switching needed +
  • +
  • + 4000+ unique users using Backstage for ~20 min on average a day. +
  • +
+
+
    +
  • + End-to-end integration with EG paved road tools and Backstage to + support increased developer productivity +
  • +
  • + Drive broader adoption to have all EG employees using Backstage to + manage their SDLC and to build internal UIs using plugins +
  • +
+
Improve search results relevance + Intuitive search capabilities to surface relevant Backstage content like + TechDocs, entities, tools, etc. + +
    +
  • Enabled global search functionality throughout Backstage
  • +
  • +

    Contributed to the open source Backstage Search platform

    +
  • +
+
+
    +
  • Engine optimization/ranking improvement
  • +
  • Visualize key search metrics to support improvements
  • +
  • Chat + AI integration with chat-based interfaces
  • +
+
Technical documentation reach + Number of developers leveraging TechDocs for all services within EG + +
    +
  • 500+ services tied with TechDocs
  • +
  • 50,000+ average monthly TechDoc views
  • +
+
+
    +
  • + All Backstage entities leveraging TechDocs to enable a centralized + documentation portal integrated with the Software Catalog +
  • +
  • + Quality/governance and gamification to support users and + contributors +
  • +
  • + More TechDocs and mkdocs add-ons to compliment the core capabilities + and drive usability of documentation on Backstage +
  • +
+
Developer Experience community engagementEnhance Backstage through community engagement +
    +
  • + Diverse contributions to Backstage open source focused on search and + discovery - 10+ +
  • +
  • Community engagement through internal case studies, and blogs
  • +
+
+
    +
  • + Continuous contributions to open-source Backstage, focused on plugin + administration, driving documentation adoption, and improved UX + around core capabilities +
  • +
  • + Strategic community engagements to enhance the platform's usability + and long-term confidence in adopting Backstage +
  • +
  • + Event participation and storytelling around EG's Backstage journey +
  • +
+
+ +## YMMV: Your Metrics May Vary! + +Measuring the value of Backstage will vary depending on your organization's needs and priorities. As an early adopter of Backstage, we hope the Expedia Group PoV metrics provide a helpful framework for understanding how Backstage can be used to improve developer productivity, collaboration, and knowledge sharing. + +We look forward to sharing future updates with the community as we continue to embrace a continuous improvement approach to Backstage. If you missed it, you can read about where we started in our [initial proof of value metrics](https://backstage.spotify.com/blog/measuring-backstage-proof-of-value-at-expedia/) back in 2021. + +_Read more about Expedia's tech journey on [their Medium Blog](https://medium.com/expedia-group-tech)_ diff --git a/microsite/blog/assets/2023-08-17/header.png b/microsite/blog/assets/2023-08-17/header.png new file mode 100644 index 0000000000..bd75529f72 Binary files /dev/null and b/microsite/blog/assets/2023-08-17/header.png differ diff --git a/microsite/blog/assets/2023-08-17/table.png b/microsite/blog/assets/2023-08-17/table.png new file mode 100644 index 0000000000..c832740634 Binary files /dev/null and b/microsite/blog/assets/2023-08-17/table.png differ diff --git a/microsite/data/plugins/api-sdk-generator.yaml b/microsite/data/plugins/api-sdk-generator.yaml new file mode 100644 index 0000000000..5553089807 --- /dev/null +++ b/microsite/data/plugins/api-sdk-generator.yaml @@ -0,0 +1,10 @@ +--- +title: API SDK Generator +author: Konfig +authorUrl: https://konfigthis.com/ +category: Development +description: Generate SDKs for your REST API to accelerate integration +documentation: https://github.com/konfig-dev/backstage-plugin-konfig/tree/main/plugins/backstage-plugin-konfig +iconUrl: https://raw.githubusercontent.com/konfig-dev/backstage-plugin-konfig/main/plugins/backstage-plugin-konfig/docs/logo.png +npmPackageName: 'backstage-plugin-konfig' +addedDate: '2023-08-07' diff --git a/microsite/data/plugins/artifactory.yaml b/microsite/data/plugins/artifactory.yaml new file mode 100644 index 0000000000..119e09eb8e --- /dev/null +++ b/microsite/data/plugins/artifactory.yaml @@ -0,0 +1,12 @@ +--- +title: Artifacts Plugin for JFrog Artifactory +author: StageCentral +authorUrl: https://stagecentral.io +category: Artifacts +description: | + Connect Backstage entities to software artifacts stored in JFrog Artifactory. + Display information about artifacts, such as their version, size, and download links. +documentation: https://github.com/StageCentral/backstage-artifactory-plugin/tree/main#readme +iconUrl: https://raw.githubusercontent.com/StageCentral/backstage-artifactory-plugin/main/logo/logo.png +npmPackageName: '@stagecentral/plugin-artifactory' +addedDate: '2023-08-23' diff --git a/microsite/data/plugins/blockchain-radar.yaml b/microsite/data/plugins/blockchain-radar.yaml new file mode 100644 index 0000000000..0aacd19ab6 --- /dev/null +++ b/microsite/data/plugins/blockchain-radar.yaml @@ -0,0 +1,15 @@ +--- +title: Blockchain Radar +author: '@aurora-is-near' +authorUrl: https://github.com/aurora-is-near +category: Discovery +description: Discover and manage contracts, multisigs and their access control policies on various blockchains. +documentation: https://github.com/aurora-is-near/backstage-plugin-blockchainradar +iconUrl: https://raw.githubusercontent.com/aurora-is-near/backstage-plugin-blockchainradar/main/docs/imgs/signs/sign-green.svg +npmPackageName: '@aurora-is-near/backstage-plugin-blockchainradar-backend' +tags: + - security + - monitoring + - blockchain + - alerting +addedDate: '2023-08-31' diff --git a/microsite/data/plugins/flux.yaml b/microsite/data/plugins/flux.yaml new file mode 100644 index 0000000000..91156f7ed3 --- /dev/null +++ b/microsite/data/plugins/flux.yaml @@ -0,0 +1,10 @@ +--- +title: Flux Plugins +author: Weaveworks +authorUrl: https://weave.works +category: GitOps +description: View the status of fluxcd.io resources +documentation: https://github.com/weaveworks/weaveworks-backstage/tree/main/plugins/backstage-plugin-flux#readme +iconUrl: https://raw.githubusercontent.com/fluxcd/website/main/assets/icons/logo.svg +npmPackageName: '@weaveworksoss/backstage-plugin-flux' +addedDate: '2023-08-23' diff --git a/microsite/data/plugins/jfrog-artifactory-libs.yaml b/microsite/data/plugins/jfrog-artifactory-libs.yaml new file mode 100644 index 0000000000..22583cbfbd --- /dev/null +++ b/microsite/data/plugins/jfrog-artifactory-libs.yaml @@ -0,0 +1,13 @@ +--- +title: JFrog Artifactory Libs plugin +author: Vity +authorUrl: https://github.com/Vity01 +category: Discovery +description: + frontend plugin provides a simple way to display generated artifact (library) details like + - group, artifact, repository, what is the latest version, and it simply + allows to copy library definition for the package managers. +documentation: https://github.com/Vity01/backstage-jfrog-artifactory-libs +iconUrl: https://raw.githubusercontent.com/Vity01/backstage-jfrog-artifactory-libs/main/doc/artifactory.svg +npmPackageName: 'backstage-plugin-jfrog-artifactory-libs' +addedDate: '2023-08-14' diff --git a/microsite/data/plugins/kiali.yaml b/microsite/data/plugins/kiali.yaml new file mode 100644 index 0000000000..a85aa6444a --- /dev/null +++ b/microsite/data/plugins/kiali.yaml @@ -0,0 +1,10 @@ +--- +title: Kiali Service Mesh +author: Red Hat +authorUrl: https://redhat.com +category: Istio +description: Configure, visualize, validate and troubleshoot your mesh with Istio +documentation: https://janus-idp.io/plugins/kiali +iconUrl: https://janus-idp.io/images/plugins/kiali.svg +npmPackageName: '@janus-idp/backstage-plugin-kiali' +addedDate: '2023-07-25' diff --git a/microsite/data/plugins/rafay.yaml b/microsite/data/plugins/rafay.yaml new file mode 100644 index 0000000000..7aaf0d7681 --- /dev/null +++ b/microsite/data/plugins/rafay.yaml @@ -0,0 +1,10 @@ +--- +title: Rafay Kubernetes Operations Platform +author: Rafay Systems +authorUrl: https://rafay.co/ +category: Deployment +description: Deploy and view clusters, namespaces, workloads and more using Rafay plugin +documentation: https://docs.rafay.co/backstage/setup/ +iconUrl: https://avatars.githubusercontent.com/u/33210764?s=200&v=4 +npmPackageName: '@rafaysystems/backstage-plugin-rafay' +addedDate: '2023-08-24' diff --git a/microsite/data/plugins/scaffolder-backend-ms-teams.yaml b/microsite/data/plugins/scaffolder-backend-ms-teams.yaml new file mode 100644 index 0000000000..6d16b1300b --- /dev/null +++ b/microsite/data/plugins/scaffolder-backend-ms-teams.yaml @@ -0,0 +1,9 @@ +--- +title: Microsoft Teams Scaffolder Actions +author: Gaurav Pandey +authorUrl: https://github.com/grvpandey11 +category: Scaffolder +description: Interact with Microsoft Teams from Scaffolder templates +documentation: https://github.com/grvpandey11/backstage-plugin-scaffolder-backend-module-ms-teams/tree/main#readme +npmPackageName: '@grvpandey11/backstage-plugin-scaffolder-backend-module-ms-teams' +addedDate: '2023-08-11' diff --git a/microsite/data/plugins/scaffolder-backend-slack.yaml b/microsite/data/plugins/scaffolder-backend-slack.yaml new file mode 100644 index 0000000000..dce7b60eb7 --- /dev/null +++ b/microsite/data/plugins/scaffolder-backend-slack.yaml @@ -0,0 +1,10 @@ +--- +title: Slack Scaffolder Actions +author: Drew Hill +authorUrl: https://github.com/arhill05 +category: Scaffolder +description: Interact with Slack from scaffolder templates +documentation: https://github.com/arhill05/backstage-plugin-scaffolder-backend-module-slack#readme +iconUrl: img/Slack-mark-RGB.png +npmPackageName: '@mdude2314/backstage-plugin-scaffolder-backend-module-slack' +addedDate: '2023-08-04' diff --git a/microsite/data/plugins/solo-io-gloo-platform-portal.yaml b/microsite/data/plugins/solo-io-gloo-platform-portal.yaml index fe7f4d78da..2f17b84f17 100644 --- a/microsite/data/plugins/solo-io-gloo-platform-portal.yaml +++ b/microsite/data/plugins/solo-io-gloo-platform-portal.yaml @@ -4,7 +4,7 @@ author: Solo.io authorUrl: https://solo.io category: Infrastructure description: View, test, and manage your Gloo Platform Portal APIs through our Backstage plugin. -documentation: https://github.com/solo-io/dev-portal-backstage-public#readme +documentation: https://github.com/solo-io/backstage-plugins-overview#readme iconUrl: /img/solo-io-glooy-circle.png.webp npmPackageName: '@solo.io/dev-portal-backstage-plugin' addedDate: '2023-05-19' diff --git a/microsite/docusaurus.config.js b/microsite/docusaurus.config.js index 23438762cc..a55ad01a35 100644 --- a/microsite/docusaurus.config.js +++ b/microsite/docusaurus.config.js @@ -177,7 +177,7 @@ module.exports = { position: 'left', }, { - to: 'docs/releases/v1.16.0', + to: 'docs/releases/v1.17.0', label: 'Releases', position: 'left', }, diff --git a/microsite/sidebars.json b/microsite/sidebars.json index d9178396e8..0e73bd94f2 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -1,6 +1,7 @@ { "releases": { "Release Notes": [ + "releases/v1.17.0", "releases/v1.16.0", "releases/v1.15.0", "releases/v1.14.0", @@ -331,6 +332,7 @@ "deployment/k8s", "deployment/heroku", "deployment/koyeb", + "deployment/flightcontrol", { "type": "category", "label": "Using Backstage Deploy (alpha)", diff --git a/microsite/src/pages/home/_home.tsx b/microsite/src/pages/home/_home.tsx index 07e05068c6..ddcfed7d03 100644 --- a/microsite/src/pages/home/_home.tsx +++ b/microsite/src/pages/home/_home.tsx @@ -32,27 +32,6 @@ const HomePage = () => { -
-
- CFP is now open for BackstageCon 2023 🚀 -
- -
{!hiddenNewsletterBanner && (
=3.0.0 <4.0.0" immutable: ^4.0.0 source-map-js: ">=0.6.2 <2.0.0" bin: sass: sass.js - checksum: 06e163c37af466ec194cf2ed8dab616372afeb19322d356947d48ea664fc38398ae77c4d91bea9cb0ace954ce289d5518d0f8555ecc49f6bf2539a8ef52fc580 + checksum: 74fc11d0fcd5e16c5331b57dd59865705a299c64e89f2b99646869caeb011dc8d0b6144a6c74a90c264e9ef70654207dbf44fc9b7e3393f8bd14809b904c8a52 languageName: node linkType: hard diff --git a/package.json b/package.json index abaaec252c..6711d06f40 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "clean": "backstage-cli repo clean", "test": "backstage-cli repo test", "test:all": "backstage-cli repo test --coverage", + "fix": "backstage-cli repo fix", "lint": "backstage-cli repo lint --since origin/master", "lint:docs": "node ./scripts/check-docs-quality", "lint:all": "backstage-cli repo lint", @@ -47,7 +48,7 @@ "@types/react": "^17", "@types/react-dom": "^17" }, - "version": "1.17.0-next.1", + "version": "1.18.0-next.2", "dependencies": { "@backstage/errors": "workspace:^", "@manypkg/get-packages": "^1.1.3" @@ -65,7 +66,7 @@ "@types/node": "^16.11.26", "@types/webpack": "^5.28.0", "command-exists": "^1.2.9", - "concurrently": "^7.0.0", + "concurrently": "^8.0.0", "cross-env": "^7.0.0", "e2e-test": "workspace:*", "eslint": "^8.6.0", diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index 342ee7dde3..5b267a3105 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/app-defaults +## 1.4.3-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/core-app-api@1.10.0-next.2 + - @backstage/plugin-permission-react@0.4.15-next.2 + - @backstage/theme@0.4.1 + +## 1.4.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-app-api@1.10.0-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-permission-react@0.4.15-next.1 + - @backstage/theme@0.4.1 + +## 1.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-permission-react@0.4.15-next.0 + +## 1.4.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.9.1 + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/theme@0.4.1 + - @backstage/plugin-permission-react@0.4.14 + ## 1.4.2-next.0 ### Patch Changes diff --git a/packages/app-defaults/catalog-info.yaml b/packages/app-defaults/catalog-info.yaml new file mode 100644 index 0000000000..e8450be1ec --- /dev/null +++ b/packages/app-defaults/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-app-defaults + title: '@backstage/app-defaults' + description: Provides the default wiring of a Backstage App +spec: + lifecycle: production + type: backstage-web-library + owner: maintainers diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index a5e31372af..12cc333e9f 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/app-defaults", "description": "Provides the default wiring of a Backstage App", - "version": "1.4.2-next.0", + "version": "1.4.3-next.2", "publishConfig": { "access": "public", "main": "dist/index.esm.js", @@ -22,6 +22,7 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", @@ -50,7 +51,6 @@ "@backstage/test-utils": "workspace:^", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", - "@types/node": "^16.11.26", "@types/react": "^16.13.1 || ^17.0.0" }, "files": [ diff --git a/packages/app-next-example-plugin/.eslintrc.js b/packages/app-next-example-plugin/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/app-next-example-plugin/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/app-next-example-plugin/api-report.md b/packages/app-next-example-plugin/api-report.md new file mode 100644 index 0000000000..68a0471b6b --- /dev/null +++ b/packages/app-next-example-plugin/api-report.md @@ -0,0 +1,17 @@ +## API Report File for "app-next-example-plugin" + +> 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 { default as React_2 } from 'react'; + +// @public (undocumented) +const examplePlugin: BackstagePlugin; +export default examplePlugin; + +// @public (undocumented) +export const ExampleSidebarItem: () => React_2.JSX.Element; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/app-next-example-plugin/package.json b/packages/app-next-example-plugin/package.json new file mode 100644 index 0000000000..9e5a0d0ee6 --- /dev/null +++ b/packages/app-next-example-plugin/package.json @@ -0,0 +1,54 @@ +{ + "name": "app-next-example-plugin", + "description": "Backstage internal example plugin", + "version": "0.0.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "private": true, + "backstage": { + "role": "frontend-plugin" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/app-next-example-plugin" + }, + "keywords": [ + "backstage" + ], + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "sideEffects": false, + "scripts": { + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" + }, + "dependencies": { + "@backstage/core-components": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@material-ui/icons": "^4.9.1" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0", + "react-dom": "^16.13.1 || ^17.0.0", + "react-router-dom": "6.0.0-beta.0 || ^6.3.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "cross-fetch": "^3.1.5", + "msw": "^1.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/packages/app-next-example-plugin/src/Component.tsx b/packages/app-next-example-plugin/src/Component.tsx new file mode 100644 index 0000000000..e90be162ed --- /dev/null +++ b/packages/app-next-example-plugin/src/Component.tsx @@ -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. + */ + +import React from 'react'; + +export const Component = () => ( +
+

SHIP IT!

+
+); diff --git a/packages/app-next-example-plugin/src/ExampleSidebarItem.tsx b/packages/app-next-example-plugin/src/ExampleSidebarItem.tsx new file mode 100644 index 0000000000..20e84f1933 --- /dev/null +++ b/packages/app-next-example-plugin/src/ExampleSidebarItem.tsx @@ -0,0 +1,24 @@ +/* + * 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 React from 'react'; +import { SidebarItem } from '@backstage/core-components'; +import SaveIcon from '@material-ui/icons/Save'; + +/** @public */ +export const ExampleSidebarItem = () => ( + +); diff --git a/packages/app-next-example-plugin/src/index.ts b/packages/app-next-example-plugin/src/index.ts new file mode 100644 index 0000000000..89ce4180fd --- /dev/null +++ b/packages/app-next-example-plugin/src/index.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. + */ + +export { examplePlugin as default } from './plugin'; + +// TODO: This should be an extension created & exported in the `plugin.tsx` +export { ExampleSidebarItem } from './ExampleSidebarItem'; diff --git a/packages/app-next-example-plugin/src/plugin.tsx b/packages/app-next-example-plugin/src/plugin.tsx new file mode 100644 index 0000000000..5b5adeab2f --- /dev/null +++ b/packages/app-next-example-plugin/src/plugin.tsx @@ -0,0 +1,33 @@ +/* + * 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 React from 'react'; +import { + createPageExtension, + createPlugin, +} from '@backstage/frontend-plugin-api'; + +export const ExamplePage = createPageExtension({ + id: 'example.page', + defaultPath: '/example', + component: () => import('./Component').then(m => ), +}); + +/** @public */ +export const examplePlugin = createPlugin({ + id: 'example', + extensions: [ExamplePage], +}); diff --git a/packages/app-next/.eslintrc.js b/packages/app-next/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/app-next/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/app-next/CHANGELOG.md b/packages/app-next/CHANGELOG.md new file mode 100644 index 0000000000..8845db91f2 --- /dev/null +++ b/packages/app-next/CHANGELOG.md @@ -0,0 +1,153 @@ +# example-app-next + +## 0.0.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-playlist@0.1.16-next.2 + - @backstage/plugin-tech-radar@0.6.8-next.2 + - @backstage/plugin-user-settings@0.7.9-next.2 + - @backstage/plugin-org@0.6.14-next.2 + - @backstage/plugin-techdocs@1.7.0-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.0-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.1.3-next.2 + - @backstage/integration-react@1.1.19-next.2 + - @backstage/plugin-microsoft-calendar@0.1.7-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-entity-feedback@0.2.7-next.2 + - @backstage/plugin-apache-airflow@0.2.15-next.2 + - @backstage/plugin-github-actions@0.6.5-next.2 + - @backstage/plugin-octopus-deploy@0.2.6-next.2 + - @backstage/plugin-stack-overflow@0.1.20-next.2 + - @backstage/app-defaults@1.4.3-next.2 + - @backstage/core-app-api@1.10.0-next.2 + - @backstage/plugin-code-coverage@0.2.17-next.2 + - @backstage/plugin-cost-insights@0.12.13-next.2 + - @backstage/plugin-tech-insights@0.3.16-next.2 + - @backstage/plugin-azure-devops@0.3.6-next.2 + - @backstage/plugin-gcp-projects@0.3.41-next.2 + - @backstage/plugin-azure-sites@0.1.13-next.2 + - @backstage/plugin-cloudbuild@0.3.24-next.2 + - @backstage/plugin-kubernetes@0.10.3-next.2 + - @backstage/plugin-lighthouse@0.4.9-next.2 + - @backstage/plugin-scaffolder@1.14.5-next.2 + - @backstage/plugin-stackstorm@0.1.6-next.2 + - @backstage/plugin-dynatrace@7.0.4-next.2 + - @backstage/plugin-gcalendar@0.3.18-next.2 + - @backstage/plugin-pagerduty@0.6.5-next.2 + - @backstage/plugin-shortcuts@0.3.14-next.2 + - @backstage/plugin-airbrake@0.3.24-next.2 + - @backstage/plugin-api-docs@0.9.11-next.2 + - @backstage/plugin-circleci@0.3.24-next.2 + - @backstage/plugin-devtools@0.1.4-next.2 + - @backstage/plugin-graphiql@0.2.54-next.2 + - @backstage/plugin-linguist@0.1.9-next.2 + - @backstage/plugin-newrelic@0.3.40-next.2 + - @backstage/plugin-puppetdb@0.1.7-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-explore@0.4.10-next.2 + - @backstage/plugin-jenkins@0.8.6-next.2 + - @backstage/plugin-rollbar@0.4.24-next.2 + - @backstage/plugin-badges@0.2.48-next.2 + - @backstage/plugin-search@1.4.0-next.2 + - @backstage/plugin-sentry@0.5.9-next.2 + - @backstage/plugin-kafka@0.3.24-next.2 + - @backstage/cli@0.22.13-next.2 + - @backstage/plugin-gocd@0.1.30-next.2 + - @backstage/plugin-home@0.5.8-next.2 + - @backstage/plugin-todo@0.2.26-next.2 + - @backstage/plugin-adr@0.6.7-next.2 + - @backstage/plugin-scaffolder-react@1.5.5-next.2 + - @internal/plugin-catalog-customized@0.0.14-next.2 + - @backstage/plugin-catalog-graph@0.2.36-next.2 + - @backstage/plugin-catalog-import@0.9.14-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/plugin-newrelic-dashboard@0.2.17-next.2 + - @backstage/plugin-search-react@1.7.0-next.2 + - @backstage/plugin-techdocs-react@1.1.10-next.2 + - @backstage/frontend-app-api@0.0.1-next.1 + - @backstage/frontend-plugin-api@0.0.1-next.0 + - @backstage/plugin-permission-react@0.4.15-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## 0.0.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-playlist@0.1.16-next.1 + - @backstage/plugin-tech-radar@0.6.8-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-org@0.6.14-next.1 + - @backstage/plugin-home@0.5.8-next.1 + - @backstage/plugin-devtools@0.1.4-next.1 + - @backstage/plugin-user-settings@0.7.9-next.1 + - @backstage/plugin-search-react@1.7.0-next.1 + - @backstage/plugin-search@1.4.0-next.1 + - @backstage/plugin-kubernetes@0.10.3-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.0-next.1 + - @backstage/plugin-adr@0.6.7-next.1 + - @backstage/plugin-airbrake@0.3.24-next.1 + - @backstage/plugin-api-docs@0.9.11-next.1 + - @backstage/plugin-azure-devops@0.3.6-next.1 + - @backstage/plugin-azure-sites@0.1.13-next.1 + - @backstage/plugin-badges@0.2.48-next.1 + - @internal/plugin-catalog-customized@0.0.14-next.1 + - @backstage/plugin-catalog-graph@0.2.36-next.1 + - @backstage/plugin-catalog-import@0.9.14-next.1 + - @backstage/plugin-circleci@0.3.24-next.1 + - @backstage/plugin-cloudbuild@0.3.24-next.1 + - @backstage/plugin-code-coverage@0.2.17-next.1 + - @backstage/plugin-cost-insights@0.12.13-next.1 + - @backstage/plugin-dynatrace@7.0.4-next.1 + - @backstage/plugin-entity-feedback@0.2.7-next.1 + - @backstage/plugin-explore@0.4.10-next.1 + - @backstage/plugin-github-actions@0.6.5-next.1 + - @backstage/plugin-gocd@0.1.30-next.1 + - @backstage/plugin-jenkins@0.8.6-next.1 + - @backstage/plugin-kafka@0.3.24-next.1 + - @backstage/plugin-lighthouse@0.4.9-next.1 + - @backstage/plugin-linguist@0.1.9-next.1 + - @backstage/plugin-newrelic-dashboard@0.2.17-next.1 + - @backstage/plugin-octopus-deploy@0.2.6-next.1 + - @backstage/plugin-pagerduty@0.6.5-next.1 + - @backstage/plugin-puppetdb@0.1.7-next.1 + - @backstage/plugin-rollbar@0.4.24-next.1 + - @backstage/plugin-scaffolder@1.14.5-next.1 + - @backstage/plugin-scaffolder-react@1.5.5-next.1 + - @backstage/plugin-sentry@0.5.9-next.1 + - @backstage/plugin-tech-insights@0.3.16-next.1 + - @backstage/plugin-techdocs@1.6.9-next.1 + - @backstage/plugin-todo@0.2.26-next.1 + - @backstage/app-defaults@1.4.3-next.1 + - @backstage/cli@0.22.13-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/plugin-apache-airflow@0.2.15-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.1.3-next.1 + - @backstage/plugin-gcalendar@0.3.18-next.1 + - @backstage/plugin-gcp-projects@0.3.41-next.1 + - @backstage/plugin-graphiql@0.2.54-next.1 + - @backstage/plugin-microsoft-calendar@0.1.7-next.1 + - @backstage/plugin-newrelic@0.3.40-next.1 + - @backstage/plugin-shortcuts@0.3.14-next.1 + - @backstage/plugin-stack-overflow@0.1.20-next.1 + - @backstage/plugin-stackstorm@0.1.6-next.1 + - @backstage/plugin-techdocs-react@1.1.10-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-app-api@1.10.0-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/frontend-app-api@0.0.1-next.0 + - @backstage/plugin-permission-react@0.4.15-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/frontend-plugin-api@0.0.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/plugin-search-common@1.2.6-next.0 diff --git a/packages/app-next/README.md b/packages/app-next/README.md new file mode 100644 index 0000000000..21645609a9 --- /dev/null +++ b/packages/app-next/README.md @@ -0,0 +1,3 @@ +# example-app-next + +This package is an example of a Backstage application. diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml new file mode 100644 index 0000000000..c94d900b21 --- /dev/null +++ b/packages/app-next/app-config.yaml @@ -0,0 +1,147 @@ +app: + experimental: + packages: 'all' # ✨ + + + # scmAuthExtension: >- + # createScmAuthExtension({ + # id: 'apis.scmAuth.addons.ghe', + # factory({ bind }) { + # bind.scmAuthAddon({ + # baseUrl: 'https://github.spotify.net', + # api: githubAuthApiRef, + # }) + # } + # }) + + # externalRouteRefs: + # bind(catalogPlugin.externalRoutes, { + # createComponent: scaffolderPlugin.routes.root, + # viewTechDoc: techdocsPlugin.routes.docRoot, + # createFromTemplate: scaffolderPlugin.routes.selectedTemplate, + # }); + # bind(apiDocsPlugin.externalRoutes, { + # registerApi: catalogImportPlugin.routes.importPage, + # }); + # bind(scaffolderPlugin.externalRoutes, { + # registerComponent: catalogImportPlugin.routes.importPage, + # viewTechDoc: techdocsPlugin.routes.docRoot, + # }); + # bind(orgPlugin.externalRoutes, { + # catalogIndex: catalogPlugin.routes.catalogIndex, + # }); + + # extensions: + # - plugin.catalog: + # config: + # externalRoutes: + # createComponent: plugin.scaffolder.page + # viewTechDoc: plugin.techdocs.docRootPage + # createFromTemplate: plugin.scaffolder.templatePage + # - graphiql.page: + # config: + # path: / + # - apis.auth.providers.github: + # config: + # provider: ghe + + # - core.signInPage: + # props: + # provider: + # id: google + # title: Google + # message: Sign In using Google + # apiRef: googleAuthApiRef # ??? + + # - core.nav: + # config: + # logo: assets/logo.png + # layout: + # - label: Search + # icon: search + # to: /search + # items: + # - point: search + # - type: divider + # - label: Menu + # icon: menu + # items: + # - label: Home + # icon: home + # to: /catalog + # - label: Create... + # icon: create + # to: /create + # - type: divider + # - type: scroll-wrapper + # items: + # - label: Tech Radar + # icon: map + # to: /tech-radar + # - label: GraphiQL + # icon: graphiql + # to: /graphiql + # - type: divider + # - point: shortcuts + # - type: space + # - type: divider + # - label: Settings + # icon: avatar + # to: /settings + # items: + # - point: settings + # - core.nav/search: '@backstage/plugin-search#SidebarSearchModal' + # - core.nav/shortcuts: + # use: '@backstage/plugin-shortcuts#Shortcuts' + # config: + # allowExternalLinks: true + # - core.nav/settings: '@backstage/plugin-user-settings#SidebarSettings' + + # - core.pages.index: + # at: + # point: core.routes + # config: + # path: / + # use: 'react-router-dom#Navigate' + # config: + # to: /catalog + + # - scaffolder.page: + # config: + # groups: + # - title: Recommended + # filter: + # entity.metadata.tags: recommended + # - scaffolder.page/fields: '@backstage/plugin-scaffolder#LowerCaseValuePickerFieldExtension' + # - scaffolder.page/layout: '@backstage/plugin-scaffolder#TwoColumnLayout' + + # - search.page/content: 'app#customSearchPage' # custom search page from somewhere + + # - user-settings.page.routes.advanced: + # at: + # point: user-settings.page/routes + # config: + # title: Advanced + # path: /advanced + # use: '@backstage/plugin-user-settings#AdvancedSettings' + + # - entity.card.orphanWarning + # - entity.card.processingErrors + # - entity.card.about + # - entity.card.catalogGraph + # - entity.card.pagerDuty + # - entity.card.links + # - entity.card.labels + # - entity.card.githubInsightsLanguages + # - entity.card.githubInsightsReleases + # - entity.card.githubInsightsReadme: + # config: + # maxHeight: 350 # Throwing this config in to have an example, but in practice rely on default + # - entity.card.subcomponentsCard + # - entity.card.userProfile + # - entity.card.ownership + # - entity.card.likeDislikeRatings + # - entity.content.dependsOnComponents + # - entity.content.codeInsights + # - entity.content.todo + # - entity.content.feedbackResponse diff --git a/packages/app-next/architecture.drawio.svg b/packages/app-next/architecture.drawio.svg new file mode 100644 index 0000000000..b5ed4de71a --- /dev/null +++ b/packages/app-next/architecture.drawio.svg @@ -0,0 +1,614 @@ + + + + + + + + + + + + + + + + + + +
+
+
+ Extension Instance +
+
+
+
+ + Extension Ins... + +
+
+ + + + + + + +
+
+
+ Extension +
+
+
+
+ + Extension + +
+
+ + + + +
+
+
+ Output +
+
+
+
+ + Output + +
+
+ + + + + +
+
+
+ Input 1 +
+
+
+
+ + Input 1 + +
+
+ + + + + +
+
+
+ Input 2 +
+
+
+
+ + Input 2 + +
+
+ + + + + +
+
+
+ React Component +
+
+
+
+ + React Component + +
+
+ + + + + +
+
+
+ Route Path +
+
+
+
+ + Route Path + +
+
+ + + + + +
+
+
+ Display Title +
+
+
+
+ + Display Title + +
+
+ + + + + + +
+
+
+ Configuration +
+
+
+
+ + Configuration + +
+
+ + + + + + +
+
+
+ Parent +
+
+
+
+ + Parent + +
+
+ + + + + +
+
+
+ + Configuration Schema + +
+
+
+
+ + Configuration Sc... + +
+
+ + + + +
+
+
+ ID +
+
+
+
+ + ID + +
+
+ + + + + +
+
+
+ Multi Input Attachments +
+
+
+
+ + Multi Input Attachments + +
+
+ + + + +
+
+
+ Child +
+
+
+
+ + Child + +
+
+ + + + + + +
+
+
+ Child +
+
+
+
+ + Child + +
+
+ + + + + + + + + + + + + + +
+
+
+ Single Input Attachment +
+
+
+
+ + Single Input Attachm... + +
+
+ + + + +
+
+
+ Child +
+
+
+
+ + Child + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Other +
+
+
+
+ + Other + +
+
+ + + + + + + + + + + + + + + +
+
+
+ Output Data +
+
+
+
+ + Output Data + +
+
+ + + + + +
+
+
+ Input Data +
+
+
+
+ + Input Data + +
+
+ + + + + + + + + +
+
+
+ Root +
+
+
+
+ + Root + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Example Extension Data Types + +
+
+
+
+ + Example Extension Data Types + +
+
+ + + + +
+
+
+ Extension Architecture +
+
+
+
+ + Extension Architecture + +
+
+ + + + +
+
+
+ App Example +
+
+
+
+ + App Example + +
+
+ + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/packages/app-next/catalog-info.yaml b/packages/app-next/catalog-info.yaml new file mode 100644 index 0000000000..85e3f0d70b --- /dev/null +++ b/packages/app-next/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: example-app-next + title: example-app-next +spec: + lifecycle: experimental + type: backstage-frontend + owner: maintainers diff --git a/packages/app-next/package.json b/packages/app-next/package.json new file mode 100644 index 0000000000..2775fe8208 --- /dev/null +++ b/packages/app-next/package.json @@ -0,0 +1,143 @@ +{ + "name": "example-app-next", + "version": "0.0.1-next.1", + "private": true, + "backstage": { + "role": "frontend" + }, + "bundled": true, + "dependencies": { + "@backstage/app-defaults": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-app-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/integration-react": "workspace:^", + "@backstage/plugin-adr": "workspace:^", + "@backstage/plugin-airbrake": "workspace:^", + "@backstage/plugin-apache-airflow": "workspace:^", + "@backstage/plugin-api-docs": "workspace:^", + "@backstage/plugin-azure-devops": "workspace:^", + "@backstage/plugin-azure-sites": "workspace:^", + "@backstage/plugin-badges": "workspace:^", + "@backstage/plugin-catalog-common": "workspace:^", + "@backstage/plugin-catalog-graph": "workspace:^", + "@backstage/plugin-catalog-import": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-catalog-unprocessed-entities": "workspace:^", + "@backstage/plugin-circleci": "workspace:^", + "@backstage/plugin-cloudbuild": "workspace:^", + "@backstage/plugin-code-coverage": "workspace:^", + "@backstage/plugin-cost-insights": "workspace:^", + "@backstage/plugin-devtools": "workspace:^", + "@backstage/plugin-dynatrace": "workspace:^", + "@backstage/plugin-entity-feedback": "workspace:^", + "@backstage/plugin-explore": "workspace:^", + "@backstage/plugin-gcalendar": "workspace:^", + "@backstage/plugin-gcp-projects": "workspace:^", + "@backstage/plugin-github-actions": "workspace:^", + "@backstage/plugin-gocd": "workspace:^", + "@backstage/plugin-graphiql": "workspace:^", + "@backstage/plugin-home": "workspace:^", + "@backstage/plugin-jenkins": "workspace:^", + "@backstage/plugin-kafka": "workspace:^", + "@backstage/plugin-kubernetes": "workspace:^", + "@backstage/plugin-lighthouse": "workspace:^", + "@backstage/plugin-linguist": "workspace:^", + "@backstage/plugin-linguist-common": "workspace:^", + "@backstage/plugin-microsoft-calendar": "workspace:^", + "@backstage/plugin-newrelic": "workspace:^", + "@backstage/plugin-newrelic-dashboard": "workspace:^", + "@backstage/plugin-octopus-deploy": "workspace:^", + "@backstage/plugin-org": "workspace:^", + "@backstage/plugin-pagerduty": "workspace:^", + "@backstage/plugin-permission-react": "workspace:^", + "@backstage/plugin-playlist": "workspace:^", + "@backstage/plugin-puppetdb": "workspace:^", + "@backstage/plugin-rollbar": "workspace:^", + "@backstage/plugin-scaffolder": "workspace:^", + "@backstage/plugin-scaffolder-react": "workspace:^", + "@backstage/plugin-search": "workspace:^", + "@backstage/plugin-search-common": "workspace:^", + "@backstage/plugin-search-react": "workspace:^", + "@backstage/plugin-sentry": "workspace:^", + "@backstage/plugin-shortcuts": "workspace:^", + "@backstage/plugin-stack-overflow": "workspace:^", + "@backstage/plugin-stackstorm": "workspace:^", + "@backstage/plugin-tech-insights": "workspace:^", + "@backstage/plugin-tech-radar": "workspace:^", + "@backstage/plugin-techdocs": "workspace:^", + "@backstage/plugin-techdocs-module-addons-contrib": "workspace:^", + "@backstage/plugin-techdocs-react": "workspace:^", + "@backstage/plugin-todo": "workspace:^", + "@backstage/plugin-user-settings": "workspace:^", + "@backstage/theme": "workspace:^", + "@internal/plugin-catalog-customized": "workspace:^", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.61", + "@octokit/rest": "^19.0.3", + "@oriflame/backstage-plugin-score-card": "^0.7.0", + "@roadiehq/backstage-plugin-buildkite": "^2.0.8", + "@roadiehq/backstage-plugin-github-insights": "^2.0.5", + "@roadiehq/backstage-plugin-github-pull-requests": "^2.2.7", + "@roadiehq/backstage-plugin-travis-ci": "^2.0.5", + "app-next-example-plugin": "workspace:^", + "history": "^5.0.0", + "lodash": "^4.17.21", + "prop-types": "^15.7.2", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-router": "^6.3.0", + "react-router-dom": "^6.3.0", + "react-use": "^17.2.4", + "zen-observable": "^0.10.0" + }, + "devDependencies": { + "@backstage/test-utils": "workspace:^", + "@testing-library/cypress": "^9.0.0", + "@testing-library/dom": "^8.0.0", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/jquery": "^3.3.34", + "@types/react": "*", + "@types/react-dom": "*", + "@types/zen-observable": "^0.8.0", + "cross-env": "^7.0.0", + "cypress": "^10.0.0", + "eslint-plugin-cypress": "^2.10.3", + "start-server-and-test": "^1.10.11" + }, + "scripts": { + "start": "backstage-cli package start --config ../../app-config.yaml --config app-config.yaml", + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "test": "backstage-cli package test", + "lint": "backstage-cli package lint", + "test:e2e": "start-server-and-test start http://localhost:3000 cy:dev", + "test:e2e:ci": "start-server-and-test start http://localhost:3000 cy:run", + "cy:dev": "cypress open", + "cy:run": "cypress run" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "files": [ + "dist" + ], + "license": "Apache-2.0" +} diff --git a/packages/app-next/public/android-chrome-192x192.png b/packages/app-next/public/android-chrome-192x192.png new file mode 100644 index 0000000000..eec0ae25b9 Binary files /dev/null and b/packages/app-next/public/android-chrome-192x192.png differ diff --git a/packages/app-next/public/android-chrome-512x512.png b/packages/app-next/public/android-chrome-512x512.png new file mode 100644 index 0000000000..ac0578133b Binary files /dev/null and b/packages/app-next/public/android-chrome-512x512.png differ diff --git a/packages/app-next/public/apple-touch-icon.png b/packages/app-next/public/apple-touch-icon.png new file mode 100644 index 0000000000..6a45afb38f Binary files /dev/null and b/packages/app-next/public/apple-touch-icon.png differ diff --git a/packages/app-next/public/favicon-16x16.png b/packages/app-next/public/favicon-16x16.png new file mode 100644 index 0000000000..58cf61a35e Binary files /dev/null and b/packages/app-next/public/favicon-16x16.png differ diff --git a/packages/app-next/public/favicon-32x32.png b/packages/app-next/public/favicon-32x32.png new file mode 100644 index 0000000000..c0915ece75 Binary files /dev/null and b/packages/app-next/public/favicon-32x32.png differ diff --git a/packages/app-next/public/favicon.ico b/packages/app-next/public/favicon.ico new file mode 100644 index 0000000000..5e45e5dfbd Binary files /dev/null and b/packages/app-next/public/favicon.ico differ diff --git a/packages/app-next/public/favicon.svg b/packages/app-next/public/favicon.svg new file mode 100644 index 0000000000..7f2d375cd8 --- /dev/null +++ b/packages/app-next/public/favicon.svg @@ -0,0 +1 @@ +Backstage favicon \ No newline at end of file diff --git a/packages/app-next/public/index.html b/packages/app-next/public/index.html new file mode 100644 index 0000000000..0710a0d69d --- /dev/null +++ b/packages/app-next/public/index.html @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + <%= config.getString('app.title') %> + + <% if (config.has('app.datadogRum')) { %> + + <% } %> + + + + +
+ + + diff --git a/packages/app-next/public/manifest.json b/packages/app-next/public/manifest.json new file mode 100644 index 0000000000..4a7c1b4ec4 --- /dev/null +++ b/packages/app-next/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "Backstage", + "name": "Backstage", + "icons": [ + { + "src": "favicon.ico", + "sizes": "48x48", + "type": "image/png" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/packages/app-next/public/robots.txt b/packages/app-next/public/robots.txt new file mode 100644 index 0000000000..01b0f9a107 --- /dev/null +++ b/packages/app-next/public/robots.txt @@ -0,0 +1,2 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * diff --git a/packages/app-next/public/safari-pinned-tab.svg b/packages/app-next/public/safari-pinned-tab.svg new file mode 100644 index 0000000000..0f500b3002 --- /dev/null +++ b/packages/app-next/public/safari-pinned-tab.svg @@ -0,0 +1 @@ +Created by potrace 1.11, written by Peter Selinger 2001-2013 \ No newline at end of file diff --git a/packages/app-next/src/App.test.tsx b/packages/app-next/src/App.test.tsx new file mode 100644 index 0000000000..047c9821b5 --- /dev/null +++ b/packages/app-next/src/App.test.tsx @@ -0,0 +1,48 @@ +/* + * 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 { renderWithEffects } from '@backstage/test-utils'; + +describe('App', () => { + it('should render', async () => { + process.env = { + NODE_ENV: 'test', + APP_CONFIG: [ + { + data: { + app: { + title: 'Test', + support: { url: 'http://localhost:7007/support' }, + }, + backend: { baseUrl: 'http://localhost:7007' }, + lighthouse: { + baseUrl: 'http://localhost:3003', + }, + techdocs: { + storageUrl: 'http://localhost:7007/api/techdocs/static/docs', + }, + }, + context: 'test', + }, + ] as any, + }; + + const { default: App } = await import('./App'); + const rendered = await renderWithEffects(); + expect(rendered.baseElement).toBeInTheDocument(); + }); +}); diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx new file mode 100644 index 0000000000..0452cc3709 --- /dev/null +++ b/packages/app-next/src/App.tsx @@ -0,0 +1,114 @@ +/* + * 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 { graphiqlPlugin as legacyGraphiqlPlugin } from '@backstage/plugin-graphiql'; +import { createApp as createLegacyApp } from '@backstage/app-defaults'; +import { createApp } from '@backstage/frontend-app-api'; +import { pagesPlugin } from './examples/pagesPlugin'; +import graphiqlPlugin from '@backstage/plugin-graphiql/alpha'; + +/* + +# Notes + +TODO: + - proper createApp + - connect extensions and plugins, provide method? + - higher level API for creating standard extensions + higher order framework API for creating those? + - extension config schema + validation + - figure out how to resolve configured extension ref to runtime value, e.g. '@backstage/plugin-graphiql#GraphiqlPage' + - make sure all shorthands work + tests + - figure out package structure / how to ship, frontend-plugin-api/frontend-app-api + - figure out routing, useRouteRef in the new system + - Legacy plugins / interop + - dynamic updates, runtime API + +*/ + +/* core */ + +// const discoverPackages = async () => { +// // stub for now, deferring package discovery til later +// return ['@backstage/plugin-graphiql']; +// }; + +/* graphiql package */ + +/* app.tsx */ + +const app = createApp({ + plugins: [graphiqlPlugin, pagesPlugin], + // bindRoutes({ bind }) { + // bind(catalogPlugin.externalRoutes, { + // createComponent: scaffolderPlugin.routes.root, + // }); + // bind(scaffolderPlugin.externalRoutes, { + // registerComponent: catalogImportPlugin.routes.importPage, + // }); + // }, +}); + +const legacyApp = createLegacyApp({ plugins: [legacyGraphiqlPlugin] }); + +export default legacyApp.createRoot(app.createRoot()); + +// const routes = ( +// +// {/* } /> +// } /> +// } +// > +// +// +// +// +// +// + +// +// +// +// +// + +// +// +// +// +// +// +// } +// /> */} +// {/* } +// /> */} +// } /> +// +// ); + +// export default app.createRoot( +// <> +// {/* +// */} +// {routes} +// , +// ); diff --git a/packages/app-next/src/examples/pagesPlugin.tsx b/packages/app-next/src/examples/pagesPlugin.tsx new file mode 100644 index 0000000000..6616c88c4b --- /dev/null +++ b/packages/app-next/src/examples/pagesPlugin.tsx @@ -0,0 +1,100 @@ +/* + * 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 React from 'react'; +import { Link } from '@backstage/core-components'; +import { + createPageExtension, + createPlugin, + useRouteRef, +} from '@backstage/frontend-plugin-api'; +import { createRouteRef } from '@backstage/core-plugin-api'; +import { Route, Routes } from 'react-router-dom'; + +const indexRouteRef = createRouteRef({ id: 'index' }); +const page1RouteRef = createRouteRef({ id: 'page1' }); +// const page2RouteRef = createSubRouteRef({ +// id: 'page2', +// parent: page1RouteRef, +// path: '/page2', +// }); + +const IndexPage = createPageExtension({ + id: 'index', + defaultPath: '/', + routeRef: indexRouteRef, + component: async () => { + const Component = () => { + const page1Link = useRouteRef(page1RouteRef); + return ( +
+ op +
+ Page 1 +
+
+ GraphiQL +
+
+ ); + }; + return ; + }, +}); + +const Page1 = createPageExtension({ + id: 'page1', + defaultPath: '/page1', + routeRef: page1RouteRef, + component: async () => { + const Component = () => { + const indexLink = useRouteRef(indexRouteRef); + // const page2Link = useRouteRef(page2RouteRef); + + return ( +
+

This is page 1

+ Go back + Page 2 + {/* Page 2 */} + +
+ Sub-page content: +
+ + This is also page 1} /> + This is page 2} /> + +
+
+
+ ); + }; + return ; + }, +}); + +export const pagesPlugin = createPlugin({ + id: 'pages', + // routes: { + // index: indexRouteRef, + // // reference in config: + // // 'plugin.pages.routes.index' + // // OR + // // 'page1' + // }, + extensions: [IndexPage, Page1], +}); diff --git a/packages/app-next/src/index.tsx b/packages/app-next/src/index.tsx new file mode 100644 index 0000000000..b15bc4c102 --- /dev/null +++ b/packages/app-next/src/index.tsx @@ -0,0 +1,22 @@ +/* + * 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 '@backstage/cli/asset-types'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +ReactDOM.render(, document.getElementById('root')); diff --git a/packages/app-next/src/setupTests.ts b/packages/app-next/src/setupTests.ts new file mode 100644 index 0000000000..23fcbe9676 --- /dev/null +++ b/packages/app-next/src/setupTests.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index c1886c92d5..6ed7929e6d 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,368 @@ # example-app +## 0.2.87-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-playlist@0.1.16-next.2 + - @backstage/plugin-tech-radar@0.6.8-next.2 + - @backstage/plugin-user-settings@0.7.9-next.2 + - @backstage/plugin-org@0.6.14-next.2 + - @backstage/plugin-techdocs@1.7.0-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.0-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.1.3-next.2 + - @backstage/integration-react@1.1.19-next.2 + - @backstage/plugin-microsoft-calendar@0.1.7-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-entity-feedback@0.2.7-next.2 + - @backstage/plugin-apache-airflow@0.2.15-next.2 + - @backstage/plugin-github-actions@0.6.5-next.2 + - @backstage/plugin-octopus-deploy@0.2.6-next.2 + - @backstage/plugin-stack-overflow@0.1.20-next.2 + - @backstage/app-defaults@1.4.3-next.2 + - @backstage/core-app-api@1.10.0-next.2 + - @backstage/plugin-code-coverage@0.2.17-next.2 + - @backstage/plugin-cost-insights@0.12.13-next.2 + - @backstage/plugin-tech-insights@0.3.16-next.2 + - @backstage/plugin-azure-devops@0.3.6-next.2 + - @backstage/plugin-gcp-projects@0.3.41-next.2 + - @backstage/plugin-azure-sites@0.1.13-next.2 + - @backstage/plugin-cloudbuild@0.3.24-next.2 + - @backstage/plugin-kubernetes@0.10.3-next.2 + - @backstage/plugin-lighthouse@0.4.9-next.2 + - @backstage/plugin-scaffolder@1.14.5-next.2 + - @backstage/plugin-stackstorm@0.1.6-next.2 + - @backstage/plugin-dynatrace@7.0.4-next.2 + - @backstage/plugin-gcalendar@0.3.18-next.2 + - @backstage/plugin-pagerduty@0.6.5-next.2 + - @backstage/plugin-shortcuts@0.3.14-next.2 + - @backstage/plugin-airbrake@0.3.24-next.2 + - @backstage/plugin-api-docs@0.9.11-next.2 + - @backstage/plugin-circleci@0.3.24-next.2 + - @backstage/plugin-devtools@0.1.4-next.2 + - @backstage/plugin-graphiql@0.2.54-next.2 + - @backstage/plugin-linguist@0.1.9-next.2 + - @backstage/plugin-newrelic@0.3.40-next.2 + - @backstage/plugin-puppetdb@0.1.7-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-explore@0.4.10-next.2 + - @backstage/plugin-jenkins@0.8.6-next.2 + - @backstage/plugin-rollbar@0.4.24-next.2 + - @backstage/plugin-badges@0.2.48-next.2 + - @backstage/plugin-search@1.4.0-next.2 + - @backstage/plugin-sentry@0.5.9-next.2 + - @backstage/plugin-kafka@0.3.24-next.2 + - @backstage/plugin-nomad@0.1.5-next.2 + - @backstage/cli@0.22.13-next.2 + - @backstage/plugin-gocd@0.1.30-next.2 + - @backstage/plugin-home@0.5.8-next.2 + - @backstage/plugin-todo@0.2.26-next.2 + - @backstage/plugin-adr@0.6.7-next.2 + - @backstage/plugin-scaffolder-react@1.5.5-next.2 + - @internal/plugin-catalog-customized@0.0.14-next.2 + - @backstage/plugin-catalog-graph@0.2.36-next.2 + - @backstage/plugin-catalog-import@0.9.14-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/plugin-newrelic-dashboard@0.2.17-next.2 + - @backstage/plugin-search-react@1.7.0-next.2 + - @backstage/plugin-techdocs-react@1.1.10-next.2 + - @backstage/plugin-permission-react@0.4.15-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## 0.2.87-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-playlist@0.1.16-next.1 + - @backstage/plugin-tech-radar@0.6.8-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-org@0.6.14-next.1 + - @backstage/plugin-home@0.5.8-next.1 + - @backstage/plugin-devtools@0.1.4-next.1 + - @backstage/plugin-user-settings@0.7.9-next.1 + - @backstage/plugin-search-react@1.7.0-next.1 + - @backstage/plugin-search@1.4.0-next.1 + - @backstage/plugin-kubernetes@0.10.3-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.0-next.1 + - @backstage/plugin-adr@0.6.7-next.1 + - @backstage/plugin-airbrake@0.3.24-next.1 + - @backstage/plugin-api-docs@0.9.11-next.1 + - @backstage/plugin-azure-devops@0.3.6-next.1 + - @backstage/plugin-azure-sites@0.1.13-next.1 + - @backstage/plugin-badges@0.2.48-next.1 + - @internal/plugin-catalog-customized@0.0.14-next.1 + - @backstage/plugin-catalog-graph@0.2.36-next.1 + - @backstage/plugin-catalog-import@0.9.14-next.1 + - @backstage/plugin-circleci@0.3.24-next.1 + - @backstage/plugin-cloudbuild@0.3.24-next.1 + - @backstage/plugin-code-coverage@0.2.17-next.1 + - @backstage/plugin-cost-insights@0.12.13-next.1 + - @backstage/plugin-dynatrace@7.0.4-next.1 + - @backstage/plugin-entity-feedback@0.2.7-next.1 + - @backstage/plugin-explore@0.4.10-next.1 + - @backstage/plugin-github-actions@0.6.5-next.1 + - @backstage/plugin-gocd@0.1.30-next.1 + - @backstage/plugin-jenkins@0.8.6-next.1 + - @backstage/plugin-kafka@0.3.24-next.1 + - @backstage/plugin-lighthouse@0.4.9-next.1 + - @backstage/plugin-linguist@0.1.9-next.1 + - @backstage/plugin-newrelic-dashboard@0.2.17-next.1 + - @backstage/plugin-nomad@0.1.5-next.1 + - @backstage/plugin-octopus-deploy@0.2.6-next.1 + - @backstage/plugin-pagerduty@0.6.5-next.1 + - @backstage/plugin-puppetdb@0.1.7-next.1 + - @backstage/plugin-rollbar@0.4.24-next.1 + - @backstage/plugin-scaffolder@1.14.5-next.1 + - @backstage/plugin-scaffolder-react@1.5.5-next.1 + - @backstage/plugin-sentry@0.5.9-next.1 + - @backstage/plugin-tech-insights@0.3.16-next.1 + - @backstage/plugin-techdocs@1.6.9-next.1 + - @backstage/plugin-todo@0.2.26-next.1 + - @backstage/app-defaults@1.4.3-next.1 + - @backstage/cli@0.22.13-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/plugin-apache-airflow@0.2.15-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.1.3-next.1 + - @backstage/plugin-gcalendar@0.3.18-next.1 + - @backstage/plugin-gcp-projects@0.3.41-next.1 + - @backstage/plugin-graphiql@0.2.54-next.1 + - @backstage/plugin-microsoft-calendar@0.1.7-next.1 + - @backstage/plugin-newrelic@0.3.40-next.1 + - @backstage/plugin-shortcuts@0.3.14-next.1 + - @backstage/plugin-stack-overflow@0.1.20-next.1 + - @backstage/plugin-stackstorm@0.1.6-next.1 + - @backstage/plugin-techdocs-react@1.1.10-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-app-api@1.10.0-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-permission-react@0.4.15-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## 0.2.87-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-unprocessed-entities@0.1.3-next.0 + - @backstage/integration-react@1.1.18-next.0 + - @backstage/plugin-microsoft-calendar@0.1.7-next.0 + - @backstage/plugin-newrelic-dashboard@0.2.16-next.0 + - @backstage/plugin-apache-airflow@0.2.15-next.0 + - @backstage/plugin-github-actions@0.6.4-next.0 + - @backstage/plugin-octopus-deploy@0.2.5-next.0 + - @backstage/plugin-azure-devops@0.3.5-next.0 + - @backstage/plugin-gcp-projects@0.3.41-next.0 + - @backstage/plugin-azure-sites@0.1.12-next.0 + - @backstage/plugin-cloudbuild@0.3.23-next.0 + - @backstage/plugin-lighthouse@0.4.8-next.0 + - @backstage/plugin-tech-radar@0.6.8-next.0 + - @backstage/plugin-dynatrace@7.0.3-next.0 + - @backstage/plugin-gcalendar@0.3.18-next.0 + - @backstage/plugin-shortcuts@0.3.14-next.0 + - @backstage/plugin-airbrake@0.3.23-next.0 + - @backstage/plugin-circleci@0.3.23-next.0 + - @backstage/plugin-graphiql@0.2.54-next.0 + - @backstage/plugin-linguist@0.1.8-next.0 + - @backstage/plugin-newrelic@0.3.40-next.0 + - @backstage/plugin-playlist@0.1.15-next.0 + - @backstage/plugin-puppetdb@0.1.6-next.0 + - @backstage/plugin-jenkins@0.8.5-next.0 + - @backstage/plugin-rollbar@0.4.23-next.0 + - @backstage/plugin-badges@0.2.47-next.0 + - @backstage/plugin-sentry@0.5.8-next.0 + - @backstage/plugin-kafka@0.3.23-next.0 + - @backstage/plugin-nomad@0.1.4-next.0 + - @backstage/plugin-todo@0.2.25-next.0 + - @backstage/plugin-org@0.6.13-next.0 + - @backstage/cli@0.22.12-next.0 + - @backstage/plugin-techdocs@1.6.8-next.0 + - @backstage/plugin-entity-feedback@0.2.6-next.0 + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/plugin-code-coverage@0.2.16-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/plugin-cost-insights@0.12.12-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/plugin-adr@0.6.6-next.0 + - @backstage/plugin-catalog-import@0.9.13-next.0 + - @backstage/plugin-scaffolder@1.14.4-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.18-next.0 + - @backstage/plugin-home@0.5.7-next.0 + - @backstage/plugin-pagerduty@0.6.4-next.0 + - @backstage/plugin-stack-overflow@0.1.20-next.0 + - @backstage/plugin-api-docs@0.9.10-next.0 + - @internal/plugin-catalog-customized@0.0.14-next.0 + - @backstage/plugin-catalog-graph@0.2.35-next.0 + - @backstage/plugin-explore@0.4.9-next.0 + - @backstage/plugin-scaffolder-react@1.5.4-next.0 + - @backstage/app-defaults@1.4.3-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-devtools@0.1.4-next.0 + - @backstage/plugin-gocd@0.1.29-next.0 + - @backstage/plugin-kubernetes@0.10.2-next.0 + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/plugin-permission-react@0.4.15-next.0 + - @backstage/plugin-search@1.3.6-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + - @backstage/plugin-stackstorm@0.1.6-next.0 + - @backstage/plugin-tech-insights@0.3.15-next.0 + - @backstage/plugin-techdocs-react@1.1.10-next.0 + - @backstage/plugin-user-settings@0.7.8-next.0 + +## 0.2.86 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.9.1 + - @backstage/plugin-catalog-graph@0.2.33 + - @backstage/plugin-playlist@0.1.13 + - @backstage/plugin-pagerduty@0.6.2 + - @backstage/plugin-scaffolder@1.14.2 + - @backstage/plugin-explore@0.4.7 + - @backstage/plugin-org@0.6.11 + - @backstage/integration-react@1.1.16 + - @backstage/plugin-microsoft-calendar@0.1.6 + - @backstage/plugin-newrelic-dashboard@0.2.14 + - @backstage/plugin-entity-feedback@0.2.4 + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/plugin-apache-airflow@0.2.14 + - @backstage/plugin-octopus-deploy@0.2.3 + - @backstage/plugin-stack-overflow@0.1.19 + - @backstage/plugin-code-coverage@0.2.14 + - @backstage/plugin-tech-insights@0.3.13 + - @backstage/plugin-dynatrace@7.0.1 + - @backstage/plugin-gcalendar@0.3.17 + - @backstage/plugin-shortcuts@0.3.13 + - @backstage/plugin-airbrake@0.3.21 + - @backstage/plugin-kafka@0.3.21 + - @backstage/plugin-nomad@0.1.2 + - @backstage/plugin-adr@0.6.4 + - @backstage/plugin-newrelic@0.3.39 + - @backstage/cli@0.22.10 + - @backstage/plugin-kubernetes@0.10.0 + - @backstage/core-components@0.13.4 + - @backstage/plugin-home@0.5.5 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/plugin-scaffolder-react@1.5.2 + - @backstage/plugin-devtools@0.1.3 + - @backstage/app-defaults@1.4.2 + - @backstage/core-plugin-api@1.5.3 + - @backstage/plugin-api-docs@0.9.7 + - @backstage/plugin-azure-devops@0.3.3 + - @backstage/plugin-azure-sites@0.1.10 + - @backstage/plugin-badges@0.2.45 + - @backstage/plugin-catalog-import@0.9.11 + - @backstage/plugin-catalog-unprocessed-entities@0.1.2 + - @backstage/plugin-circleci@0.3.21 + - @backstage/plugin-cloudbuild@0.3.21 + - @backstage/plugin-cost-insights@0.12.10 + - @backstage/plugin-gcp-projects@0.3.40 + - @backstage/plugin-github-actions@0.6.2 + - @backstage/plugin-gocd@0.1.27 + - @backstage/plugin-graphiql@0.2.53 + - @backstage/plugin-jenkins@0.8.3 + - @backstage/plugin-lighthouse@0.4.6 + - @backstage/plugin-linguist@0.1.6 + - @backstage/plugin-puppetdb@0.1.4 + - @backstage/plugin-rollbar@0.4.21 + - @backstage/plugin-search@1.3.4 + - @backstage/plugin-search-react@1.6.4 + - @backstage/plugin-sentry@0.5.6 + - @backstage/plugin-stackstorm@0.1.5 + - @backstage/plugin-tech-radar@0.6.7 + - @backstage/plugin-techdocs@1.6.6 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.16 + - @backstage/plugin-todo@0.2.23 + - @backstage/plugin-user-settings@0.7.6 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-react@0.4.14 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-techdocs-react@1.1.9 + - @internal/plugin-catalog-customized@0.0.13 + +## 0.2.86-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-graph@0.2.33-next.2 + - @backstage/plugin-linguist-common@0.1.1-next.1 + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/plugin-linguist@0.1.6-next.2 + - @backstage/plugin-adr@0.6.4-next.2 + - @backstage/plugin-airbrake@0.3.21-next.2 + - @backstage/plugin-api-docs@0.9.7-next.2 + - @backstage/plugin-azure-devops@0.3.3-next.1 + - @backstage/plugin-azure-sites@0.1.10-next.1 + - @backstage/plugin-badges@0.2.45-next.1 + - @internal/plugin-catalog-customized@0.0.13-next.2 + - @backstage/plugin-catalog-import@0.9.11-next.2 + - @backstage/plugin-circleci@0.3.21-next.1 + - @backstage/plugin-cloudbuild@0.3.21-next.1 + - @backstage/plugin-code-coverage@0.2.14-next.2 + - @backstage/plugin-cost-insights@0.12.10-next.1 + - @backstage/plugin-dynatrace@7.0.1-next.2 + - @backstage/plugin-entity-feedback@0.2.4-next.2 + - @backstage/plugin-explore@0.4.7-next.1 + - @backstage/plugin-github-actions@0.6.2-next.2 + - @backstage/plugin-gocd@0.1.27-next.1 + - @backstage/plugin-home@0.5.5-next.1 + - @backstage/plugin-jenkins@0.8.3-next.2 + - @backstage/plugin-kafka@0.3.21-next.2 + - @backstage/plugin-kubernetes@0.9.4-next.1 + - @backstage/plugin-lighthouse@0.4.6-next.1 + - @backstage/plugin-newrelic-dashboard@0.2.14-next.2 + - @backstage/plugin-nomad@0.1.2-next.2 + - @backstage/plugin-octopus-deploy@0.2.3-next.2 + - @backstage/plugin-org@0.6.11-next.2 + - @backstage/plugin-pagerduty@0.6.2-next.1 + - @backstage/plugin-playlist@0.1.13-next.2 + - @backstage/plugin-puppetdb@0.1.4-next.1 + - @backstage/plugin-rollbar@0.4.21-next.1 + - @backstage/plugin-scaffolder@1.14.2-next.2 + - @backstage/plugin-scaffolder-react@1.5.2-next.1 + - @backstage/plugin-search@1.3.4-next.1 + - @backstage/plugin-sentry@0.5.6-next.1 + - @backstage/plugin-tech-insights@0.3.13-next.2 + - @backstage/plugin-techdocs@1.6.6-next.2 + - @backstage/plugin-todo@0.2.23-next.1 + - @backstage/plugin-user-settings@0.7.6-next.1 + - @backstage/cli@0.22.10-next.1 + - @backstage/integration-react@1.1.16-next.1 + - @backstage/plugin-apache-airflow@0.2.14-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.1.2-next.0 + - @backstage/plugin-devtools@0.1.3-next.1 + - @backstage/plugin-gcalendar@0.3.17-next.1 + - @backstage/plugin-gcp-projects@0.3.40-next.0 + - @backstage/plugin-graphiql@0.2.53-next.0 + - @backstage/plugin-microsoft-calendar@0.1.6-next.1 + - @backstage/plugin-newrelic@0.3.39-next.0 + - @backstage/plugin-shortcuts@0.3.13-next.1 + - @backstage/plugin-stack-overflow@0.1.19-next.1 + - @backstage/plugin-stackstorm@0.1.5-next.0 + - @backstage/plugin-tech-radar@0.6.7-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.16-next.1 + ## 0.2.86-next.1 ### Patch Changes diff --git a/packages/app/catalog-info.yaml b/packages/app/catalog-info.yaml new file mode 100644 index 0000000000..e0808307cc --- /dev/null +++ b/packages/app/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: example-app + title: example-app +spec: + lifecycle: experimental + type: backstage-frontend + owner: maintainers diff --git a/packages/app/package.json b/packages/app/package.json index f33a5645b9..52e7a295a1 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.86-next.1", + "version": "0.2.87-next.2", "private": true, "backstage": { "role": "frontend" @@ -108,7 +108,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/jquery": "^3.3.34", - "@types/node": "^16.11.26", "@types/react": "*", "@types/react-dom": "*", "@types/zen-observable": "^0.8.0", diff --git a/packages/app/src/components/home/HomePage.tsx b/packages/app/src/components/home/HomePage.tsx index aa3739728e..a8ddc0e0e2 100644 --- a/packages/app/src/components/home/HomePage.tsx +++ b/packages/app/src/components/home/HomePage.tsx @@ -15,14 +15,14 @@ */ import { - HomePageRandomJoke, - WelcomeTitle, - HeaderWorldClock, ClockConfig, - HomePageStarredEntities, CustomHomepageGrid, - HomePageToolkit, + HeaderWorldClock, HomePageCompanyLogo, + HomePageRandomJoke, + HomePageStarredEntities, + HomePageToolkit, + WelcomeTitle, } from '@backstage/plugin-home'; import { Content, Header, Page } from '@backstage/core-components'; import { HomePageSearchBar } from '@backstage/plugin-search'; @@ -64,6 +64,9 @@ const defaultConfig = [ y: 0, width: 12, height: 1, + movable: false, + resizable: false, + deletable: false, }, { component: 'WelcomeTitle', @@ -77,7 +80,7 @@ const defaultConfig = [ x: 0, y: 2, width: 12, - height: 1, + height: 2, }, ]; diff --git a/packages/app/src/setupTests.ts b/packages/app/src/setupTests.ts index 23fcbe9676..d6dc983b17 100644 --- a/packages/app/src/setupTests.ts +++ b/packages/app/src/setupTests.ts @@ -19,3 +19,9 @@ // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; + +// Do not remove, patching jsdom environment to support TextEncoder, refer to https://github.com/jsdom/jsdom/issues/2524 +// eslint-disable-next-line no-restricted-imports +import { TextEncoder } from 'util'; + +global.TextEncoder = TextEncoder; diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md index cbfbfef468..e53e5d273d 100644 --- a/packages/backend-app-api/CHANGELOG.md +++ b/packages/backend-app-api/CHANGELOG.md @@ -1,5 +1,111 @@ # @backstage/backend-app-api +## 0.5.3-next.2 + +### Patch Changes + +- 37a20c7f14aa: Adds include and exclude configuration to feature discovery of backend packages + Adds alpha modules to feature discovery +- 3fc64b9e2f8f: Extension points are now tracked via their ID rather than reference, in order to support package duplication. +- b219d097b3f4: Backend startup will now fail if any circular service dependencies are detected. +- Updated dependencies + - @backstage/config-loader@1.5.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/config-loader@1.5.0-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.5.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-node@0.7.13-next.0 + +## 0.5.0 + +### Minor Changes + +- b9c57a4f857e: **BREAKING**: Renamed `configServiceFactory` to `rootConfigServiceFactory`. +- a6d7983f349c: **BREAKING**: Removed the `services` option from `createBackend`. Service factories are now `BackendFeature`s and should be installed with `backend.add(...)` instead. The following should be migrated: + + ```ts + const backend = createBackend({ services: [myCustomServiceFactory] }); + ``` + + To instead pass the service factory via `backend.add(...)`: + + ```ts + const backend = createBackend(); + backend.add(customRootLoggerServiceFactory); + ``` + +### Patch Changes + +- e65c4896f755: Do not throw in backend.stop, if start failed +- c7aa4ff1793c: Allow modules to register extension points. +- 57a10c6c69cc: Add validation to make sure that extension points do not cross plugin boundaries. +- cc9256a33bcc: Added new experimental `featureDiscoveryServiceFactory`, available as an `/alpha` export. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config-loader@1.4.0 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/cli-node@0.1.3 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/backend-tasks@0.5.5 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.5.0-next.2 + +### Patch Changes + +- e65c4896f755: Do not throw in backend.stop, if start failed +- cc9256a33bcc: Added new experimental `featureDiscoveryServiceFactory`, available as an `/alpha` export. +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + - @backstage/config-loader@1.4.0-next.1 + ## 0.5.0-next.1 ### Minor Changes diff --git a/packages/backend-app-api/alpha-api-report.md b/packages/backend-app-api/alpha-api-report.md new file mode 100644 index 0000000000..488feedf2c --- /dev/null +++ b/packages/backend-app-api/alpha-api-report.md @@ -0,0 +1,16 @@ +## API Report File for "@backstage/backend-app-api" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; + +// @alpha (undocumented) +export const featureDiscoveryServiceFactory: () => ServiceFactory< + FeatureDiscoveryService, + 'root' +>; + +// (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 675a7755fc..3bddcc48d5 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -43,7 +43,14 @@ import { UrlReader } from '@backstage/backend-common'; // @public (undocumented) export interface Backend { // (undocumented) - add(feature: BackendFeature): void; + add( + feature: + | BackendFeature + | (() => BackendFeature) + | Promise<{ + default: BackendFeature | (() => BackendFeature); + }>, + ): void; // (undocumented) start(): Promise; // (undocumented) @@ -81,7 +88,7 @@ export function createSpecializedBackend( // @public (undocumented) export interface CreateSpecializedBackendOptions { // (undocumented) - services: ServiceFactoryOrFunction[]; + defaultServiceFactories: ServiceFactoryOrFunction[]; } // @public (undocumented) diff --git a/packages/backend-app-api/catalog-info.yaml b/packages/backend-app-api/catalog-info.yaml new file mode 100644 index 0000000000..051e7ab5c2 --- /dev/null +++ b/packages/backend-app-api/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-backend-app-api + title: '@backstage/backend-app-api' + description: Core API used by Backstage backend apps +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index d1aa511794..3bd12759f2 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -1,17 +1,30 @@ { "name": "@backstage/backend-app-api", "description": "Core API used by Backstage backend apps", - "version": "0.5.0-next.1", + "version": "0.5.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" }, "backstage": { "role": "node-library" }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } + }, "homepage": "https://backstage.io", "repository": { "type": "git", @@ -36,6 +49,7 @@ "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", "@backstage/config": "workspace:^", "@backstage/config-loader": "workspace:^", "@backstage/errors": "workspace:^", @@ -73,6 +87,7 @@ "@types/node-forge": "^1.3.0", "@types/stoppable": "^1.1.0", "http-errors": "^2.0.0", + "mock-fs": "^5.2.0", "supertest": "^6.1.3" }, "files": [ diff --git a/packages/backend-app-api/src/alpha.ts b/packages/backend-app-api/src/alpha.ts new file mode 100644 index 0000000000..a334214738 --- /dev/null +++ b/packages/backend-app-api/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * 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 { featureDiscoveryServiceFactory } from './alpha/featureDiscoveryServiceFactory'; diff --git a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts new file mode 100644 index 0000000000..448f1a8ff7 --- /dev/null +++ b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts @@ -0,0 +1,329 @@ +/* + * 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 mockFs from 'mock-fs'; +import { resolve as resolvePath, dirname } from 'path'; +import { startTestBackend, mockServices } from '@backstage/backend-test-utils'; +import { featureDiscoveryServiceFactory } from './featureDiscoveryServiceFactory'; + +const rootDir = dirname(process.argv[1]); + +describe('featureDiscoveryServiceFactory', () => { + beforeEach(() => { + mockFs({ + [rootDir]: { + 'package.json': JSON.stringify({ + name: 'example-app', + dependencies: { + 'detected-plugin': '0.0.0', + 'detected-module': '0.0.0', + 'detected-plugin-with-alpha': '0.0.0', + 'detected-library': '0.0.0', + }, + }), + }, + [resolvePath(rootDir, 'node_modules/detected-plugin')]: { + 'package.json': JSON.stringify({ + name: 'detected-plugin', + main: 'index.js', + backstage: { + role: 'backend-plugin', + }, + }), + 'index.js': ` + const { createBackendPlugin, coreServices } = require('@backstage/backend-plugin-api'); + exports.default = createBackendPlugin({ + pluginId: 'detected', + register(env) { + env.registerInit({ + deps: { logger: coreServices.rootLogger }, + async init({ logger }) { + logger.warn('detected-plugin'); + }, + }); + }, + }); + `, + }, + [resolvePath(rootDir, 'node_modules/detected-module')]: { + 'package.json': JSON.stringify({ + name: 'detected-module', + main: 'index.js', + backstage: { + role: 'backend-plugin-module', + }, + }), + 'index.js': ` + const { createBackendModule, coreServices } = require('@backstage/backend-plugin-api'); + exports.default = createBackendModule({ + pluginId: 'detected', + moduleId: 'derp', + register(env) { + env.registerInit({ + deps: { logger: coreServices.rootLogger }, + async init({ logger }) { + logger.warn('detected-module'); + }, + }); + }, + }); + `, + }, + [resolvePath(rootDir, 'node_modules/detected-plugin-with-alpha')]: { + 'package.json': JSON.stringify({ + name: 'detected-plugin-with-alpha', + main: 'index.js', + exports: { + '.': { + default: 'index.js', + }, + './alpha': { + default: 'alpha.js', + }, + './package.json': './package.json', + }, + backstage: { + role: 'backend-plugin', + }, + }), + 'index.js': `exports.default = undefined;`, + 'alpha.js': ` + const { createBackendPlugin, coreServices } = require('@backstage/backend-plugin-api'); + exports.default = createBackendPlugin({ + pluginId: 'detected-alpha', + register(env) { + env.registerInit({ + deps: { logger: coreServices.rootLogger }, + async init({ logger }) { + logger.warn('detected-plugin-with-alpha'); + }, + }); + }, + }); + `, + }, + [resolvePath(rootDir, 'node_modules/detected-library')]: { + 'package.json': JSON.stringify({ + name: 'detected-library', + main: 'index.js', + backstage: { + role: 'node-library', + }, + }), + 'index.js': ` + const { createServiceFactory, createServiceRef, coreServices } = require('@backstage/backend-plugin-api'); + exports.default = createServiceFactory({ + service: createServiceRef({ id: 'test', scope: 'root' }), + deps: { logger: coreServices.rootLogger }, + factory({ logger }) { + logger.warn('detected-library'); + return {}; + }, + }); + `, + }, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should detect plugin and module packages when "all" is specified', async () => { + const mock = mockServices.rootLogger.mock({ child: () => mock }); + + await startTestBackend({ + features: [ + mock.factory, + featureDiscoveryServiceFactory(), + mockServices.rootConfig.factory({ + data: { backend: { packages: 'all' } }, + }), + ], + }); + + expect(mock.warn).toHaveBeenCalledWith('detected-plugin'); + expect(mock.warn).toHaveBeenCalledWith('detected-module'); + expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); + expect(mock.warn).toHaveBeenCalledWith('detected-library'); + }); + + it('detects only the packages that are listed as included', async () => { + const mock = mockServices.rootLogger.mock({ child: () => mock }); + + await startTestBackend({ + features: [ + mock.factory, + featureDiscoveryServiceFactory(), + mockServices.rootConfig.factory({ + data: { + backend: { + packages: { + include: [ + 'detected-plugin', + 'detected-plugin-with-alpha', + 'detected-library', + ], + }, + }, + }, + }), + ], + }); + + expect(mock.warn).toHaveBeenCalledWith('detected-plugin'); + expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); + expect(mock.warn).toHaveBeenCalledWith('detected-library'); + expect(mock.warn).not.toHaveBeenCalledWith('detected-module'); + }); + + it('does not detect packages when included is an empty list', async () => { + const mock = mockServices.rootLogger.mock({ child: () => mock }); + + await startTestBackend({ + features: [ + mock.factory, + featureDiscoveryServiceFactory(), + mockServices.rootConfig.factory({ + data: { + backend: { + packages: { + include: [], + }, + }, + }, + }), + ], + }); + + expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin'); + expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin-with-alpha'); + expect(mock.warn).not.toHaveBeenCalledWith('detected-module'); + expect(mock.warn).not.toHaveBeenCalledWith('detected-library'); + }); + + it('does not detect an excluded packages', async () => { + const mock = mockServices.rootLogger.mock({ child: () => mock }); + + await startTestBackend({ + features: [ + mock.factory, + featureDiscoveryServiceFactory(), + mockServices.rootConfig.factory({ + data: { + backend: { + packages: { + exclude: ['detected-plugin', 'detected-module'], + }, + }, + }, + }), + ], + }); + + expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin'); + expect(mock.warn).not.toHaveBeenCalledWith('detected-module'); + expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); + expect(mock.warn).toHaveBeenCalledWith('detected-library'); + }); + + it('does not excluded packages when it is an empty list', async () => { + const mock = mockServices.rootLogger.mock({ child: () => mock }); + + await startTestBackend({ + features: [ + mock.factory, + featureDiscoveryServiceFactory(), + mockServices.rootConfig.factory({ + data: { + backend: { + packages: { + exclude: [], + }, + }, + }, + }), + ], + }); + + expect(mock.warn).toHaveBeenCalledWith('detected-plugin'); + expect(mock.warn).toHaveBeenCalledWith('detected-module'); + expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); + expect(mock.warn).toHaveBeenCalledWith('detected-library'); + }); + + it('does not detect packages that are included and excluded', async () => { + const mock = mockServices.rootLogger.mock({ child: () => mock }); + + await startTestBackend({ + features: [ + mock.factory, + featureDiscoveryServiceFactory(), + mockServices.rootConfig.factory({ + data: { + backend: { + packages: { + include: [ + 'detected-plugin', + 'detected-module', + 'detected-plugin-with-alpha', + ], + exclude: ['detected-plugin'], + }, + }, + }, + }), + ], + }); + + expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin'); + expect(mock.warn).not.toHaveBeenCalledWith('detected-library'); + expect(mock.warn).toHaveBeenCalledWith('detected-module'); + expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha'); + }); + + it('does not detect any packages when "packages" is empty', async () => { + const mock = mockServices.rootLogger.mock({ child: () => mock }); + + await startTestBackend({ + features: [ + mock.factory, + featureDiscoveryServiceFactory(), + mockServices.rootConfig.factory({ + data: { backend: { packages: {} } }, + }), + ], + }); + + expect(mock.warn).not.toHaveBeenCalled(); + }); + + it('does not detect any packages when "packages" is not present', async () => { + const mock = mockServices.rootLogger.mock({ child: () => mock }); + + await startTestBackend({ + features: [ + mock.factory, + featureDiscoveryServiceFactory(), + mockServices.rootConfig.factory({ + data: { backend: {} }, + }), + ], + }); + + expect(mock.warn).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts new file mode 100644 index 0000000000..b68475a77b --- /dev/null +++ b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.ts @@ -0,0 +1,174 @@ +/* + * 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 { + BackendFeature, + RootConfigService, + RootLoggerService, + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { + featureDiscoveryServiceRef, + FeatureDiscoveryService, +} from '@backstage/backend-plugin-api/alpha'; +import { resolve as resolvePath, dirname } from 'path'; +import fs from 'fs-extra'; +import { BackstagePackageJson } from '@backstage/cli-node'; + +/** @internal */ +async function findClosestPackageDir( + searchDir: string, +): Promise { + let path = searchDir; + + // Some confidence check to avoid infinite loop + for (let i = 0; i < 1000; i++) { + const packagePath = resolvePath(path, 'package.json'); + const exists = await fs.pathExists(packagePath); + if (exists) { + return path; + } + + const newPath = dirname(path); + if (newPath === path) { + return undefined; + } + path = newPath; + } + + throw new Error( + `Iteration limit reached when searching for root package.json at ${searchDir}`, + ); +} + +/** @internal */ +class PackageDiscoveryService implements FeatureDiscoveryService { + constructor( + private readonly config: RootConfigService, + private readonly logger: RootLoggerService, + ) {} + + getDependencyNames(path: string) { + const { dependencies } = require(path) as BackstagePackageJson; + const packagesConfig = this.config.getOptional('backend.packages'); + + const dependencyNames = Object.keys(dependencies || {}); + + if (packagesConfig === 'all') { + return dependencyNames; + } + + const includedPackagesConfig = this.config.getOptionalStringArray( + 'backend.packages.include', + ); + + const includedPackages = includedPackagesConfig + ? new Set(includedPackagesConfig) + : dependencyNames; + const excludedPackagesSet = new Set( + this.config.getOptionalStringArray('backend.packages.exclude'), + ); + + return [...includedPackages].filter(name => !excludedPackagesSet.has(name)); + } + + async getBackendFeatures(): Promise<{ features: Array }> { + const packagesConfig = this.config.getOptional('backend.packages'); + if (!packagesConfig || Object.keys(packagesConfig).length === 0) { + return { features: [] }; + } + + const packageDir = await findClosestPackageDir(process.argv[1]); + if (!packageDir) { + throw new Error('Package discovery failed to find package.json'); + } + const dependencyNames = this.getDependencyNames( + resolvePath(packageDir, 'package.json'), + ); + + const features: BackendFeature[] = []; + + for (const name of dependencyNames) { + const depPkg = require(require.resolve(`${name}/package.json`, { + paths: [packageDir], + })) as BackstagePackageJson; + if (!depPkg?.backstage || depPkg?.backstage?.role === 'cli') { + continue; // Not a backstage package, ignore + } + + const exportedModulePaths = [ + require.resolve(name, { + paths: [packageDir], + }), + ]; + + // Find modules exported as alpha + try { + exportedModulePaths.push( + require.resolve(`${name}/alpha`, { paths: [packageDir] }), + ); + } catch { + /* ignore */ + } + + for (const modulePath of exportedModulePaths) { + const mod = require(modulePath); + + if (isBackendFeature(mod.default)) { + this.logger.info(`Detected: ${name}`); + features.push(mod.default); + } + if (isBackendFeatureFactory(mod.default)) { + this.logger.info(`Detected: ${name}`); + features.push(mod.default()); + } + } + } + + return { features }; + } +} + +/** @alpha */ +export const featureDiscoveryServiceFactory = createServiceFactory({ + service: featureDiscoveryServiceRef, + deps: { + config: coreServices.rootConfig, + logger: coreServices.rootLogger, + }, + factory({ config, logger }) { + return new PackageDiscoveryService(config, logger); + }, +}); + +function isBackendFeature(value: unknown): value is BackendFeature { + return ( + !!value && + typeof value === 'object' && + (value as BackendFeature).$$type === '@backstage/BackendFeature' + ); +} + +function isBackendFeatureFactory( + value: unknown, +): value is () => BackendFeature { + return ( + !!value && + typeof value === 'function' && + (value as any).$$type === '@backstage/BackendFeatureFactory' + ); +} diff --git a/packages/backend-app-api/src/lib/DependencyGraph.test.ts b/packages/backend-app-api/src/lib/DependencyGraph.test.ts new file mode 100644 index 0000000000..394359e486 --- /dev/null +++ b/packages/backend-app-api/src/lib/DependencyGraph.test.ts @@ -0,0 +1,518 @@ +/* + * 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 { DependencyGraph } from './DependencyGraph'; + +describe('DependencyGraph', () => { + it('should be empty', async () => { + const empty = DependencyGraph.fromMap({}); + expect(empty.findUnsatisfiedDeps()).toEqual([]); + expect(empty.detectCircularDependency()).toBeUndefined(); + await expect( + empty.parallelTopologicalTraversal(async id => id), + ).resolves.toEqual([]); + }); + + describe('detectCircularDependency', () => { + it('should return undefined with no deps', () => { + expect( + DependencyGraph.fromMap({ + 1: {}, + 2: {}, + 3: {}, + 4: {}, + }).detectCircularDependency(), + ).toBeUndefined(); + }); + + it('should return undefined with no circular deps', () => { + expect( + DependencyGraph.fromMap({ + 1: { provides: ['a'] }, + 2: { consumes: ['a'], provides: ['b', 'c'] }, + 3: { consumes: ['b'] }, + 4: { consumes: ['c'] }, + }).detectCircularDependency(), + ).toBeUndefined(); + }); + + it('should detect an immediate circular dependency', () => { + expect( + DependencyGraph.fromMap({ + 1: { provides: ['a'], consumes: ['a'] }, + }).detectCircularDependency(), + ).toEqual(['1', '1']); + }); + + it('should detect a small circular dependency', () => { + expect( + DependencyGraph.fromMap({ + 1: { provides: ['a'], consumes: ['b'] }, + 2: { provides: ['b'], consumes: ['a'] }, + }).detectCircularDependency(), + ).toEqual(['1', '2', '1']); + }); + + it('should detect a larger distance circular dependency', () => { + expect( + DependencyGraph.fromMap({ + 1: { provides: ['a'] }, + 2: { provides: ['b'], consumes: ['a', 'e'] }, + 3: { provides: ['c'], consumes: ['b'] }, + 4: { provides: ['d', 'e'], consumes: ['c', 'a'] }, + }).detectCircularDependency(), + ).toEqual(['2', '4', '3', '2']); + }); + + it('should detect a circular dependency starting from the first node', () => { + expect( + DependencyGraph.fromMap({ + 1: { provides: ['a'], consumes: ['b'] }, + 2: { provides: ['b'], consumes: ['c'] }, + 3: { provides: ['c'], consumes: ['d'] }, + 4: { provides: ['d'], consumes: ['a'] }, + }).detectCircularDependency(), + ).toEqual(['1', '2', '3', '4', '1']); + }); + + it('should detect all independent circular dependency cycles', () => { + expect( + Array.from( + DependencyGraph.fromMap({ + // Cycle 1 + 1: { provides: ['a'], consumes: ['b'] }, + 2: { provides: ['b'], consumes: ['c'] }, + 3: { provides: ['c'], consumes: ['a'] }, + + // Cycle 2 + 4: { provides: ['d'], consumes: ['e'] }, + 5: { provides: ['e'], consumes: ['f'] }, + 6: { provides: ['f'], consumes: ['d'] }, + + // Cycle 3 + 7: { provides: ['g'], consumes: ['h'] }, + 8: { provides: ['h'], consumes: ['i'] }, + 9: { provides: ['i'], consumes: ['g'] }, + }).detectCircularDependencies(), + ), + ).toEqual([ + ['1', '2', '3', '1'], + ['4', '5', '6', '4'], + ['7', '8', '9', '7'], + ]); + }); + + it('should only detect unique circular dependency cycles', () => { + expect( + Array.from( + DependencyGraph.fromMap({ + 1: { provides: ['a'], consumes: ['b'] }, + 2: { provides: ['b'], consumes: ['c', 'a'] }, + 3: { provides: ['c'], consumes: ['d', 'a'] }, + 4: { provides: ['d'], consumes: ['e', 'a'] }, + 5: { provides: ['e'], consumes: ['f', 'a'] }, + 6: { provides: ['f'], consumes: ['h', 'a'] }, + 7: { provides: ['h'], consumes: ['a'] }, + }).detectCircularDependencies(), + ), + ).toEqual([ + ['1', '2', '1'], + ['1', '2', '3', '1'], + ['1', '2', '3', '4', '1'], + ['1', '2', '3', '4', '5', '1'], + ['1', '2', '3', '4', '5', '6', '1'], + ['1', '2', '3', '4', '5', '6', '7', '1'], + ]); + }); + + it('should detect circular dependency cycles in order when fromIterable', () => { + expect( + Array.from( + DependencyGraph.fromIterable([ + { value: 'a', provides: ['a'], consumes: ['b'] }, + { value: 'b', provides: ['b'], consumes: ['c', 'a'] }, + { value: 'c', provides: ['c'], consumes: ['d', 'a'] }, + { value: '4', provides: ['d'], consumes: ['e', 'a'] }, + { value: '5', provides: ['e'], consumes: ['f', 'a'] }, + { value: '6', provides: ['f'], consumes: ['h', 'a'] }, + ]).detectCircularDependencies(), + ), + ).toEqual([ + ['a', 'b', 'a'], + ['a', 'b', 'c', 'a'], + ['a', 'b', 'c', '4', 'a'], + ['a', 'b', 'c', '4', '5', 'a'], + ['a', 'b', 'c', '4', '5', '6', 'a'], + ]); + }); + + it('should detect circular dependency cycles in order by key when fromMap', () => { + expect( + Array.from( + DependencyGraph.fromMap({ + 1: { provides: ['a'], consumes: ['b'] }, + 2: { provides: ['b'], consumes: ['c', 'a'] }, + 3: { provides: ['c'], consumes: ['d', 'a'] }, + 4: { provides: ['d'], consumes: ['e', 'a'] }, + 5: { provides: ['e'], consumes: ['f', 'a'] }, + 6: { provides: ['f'], consumes: ['h', 'a'] }, + }).detectCircularDependencies(), + ), + ).toEqual([ + ['1', '2', '1'], + ['1', '2', '3', '1'], + ['1', '2', '3', '4', '1'], + ['1', '2', '3', '4', '5', '1'], + ['1', '2', '3', '4', '5', '6', '1'], + ]); + }); + + it('should detect circular dependency cycles in order by key when fromMap 2', () => { + expect( + Array.from( + DependencyGraph.fromMap({ + a: { provides: ['a'], consumes: ['b'] }, + b: { provides: ['b'], consumes: ['c', 'a'] }, + c: { provides: ['c'], consumes: ['d', 'a'] }, + 4: { provides: ['d'], consumes: ['e', 'a'] }, + 5: { provides: ['e'], consumes: ['f', 'a'] }, + 6: { provides: ['f'], consumes: ['h', 'a'] }, + }).detectCircularDependencies(), + ), + ).toEqual([ + ['4', 'a', 'b', 'c', '4'], + ['5', 'a', 'b', 'c', '4', '5'], + ['6', 'a', 'b', 'c', '4', '5', '6'], + ['a', 'b', 'a'], + ['a', 'b', 'c', 'a'], + ]); + }); + + it('should detect circular dependency cycles in order by key when fromMap 3', () => { + expect( + Array.from( + DependencyGraph.fromMap({ + a: { provides: ['a'], consumes: ['b'] }, + b: { provides: ['b'], consumes: ['c', 'a'] }, + c: { provides: ['c'], consumes: ['d', 'a'] }, + d: { provides: ['d'], consumes: ['e', 'a'] }, + e: { provides: ['e'], consumes: ['f', 'a'] }, + f: { provides: ['f'], consumes: ['h', 'a'] }, + }).detectCircularDependencies(), + ), + ).toEqual([ + ['a', 'b', 'a'], + ['a', 'b', 'c', 'a'], + ['a', 'b', 'c', 'd', 'a'], + ['a', 'b', 'c', 'd', 'e', 'a'], + ['a', 'b', 'c', 'd', 'e', 'f', 'a'], + ]); + }); + + it('should detect circular dependency cycles with duplicate keys when fromIterable', () => { + expect( + Array.from( + DependencyGraph.fromIterable([ + { value: 'a', provides: ['a'], consumes: ['b'] }, + { value: 'a', provides: ['b'], consumes: ['c', 'a'] }, + { value: 'a', provides: ['c'], consumes: ['d', 'a'] }, + { value: 'a', provides: ['d'], consumes: ['e', 'a'] }, + { value: 'a', provides: ['e'], consumes: ['f', 'a'] }, + { value: 'a', provides: ['f'], consumes: ['h', 'a'] }, + ]).detectCircularDependencies(), + ), + ).toEqual([ + ['a', 'a', 'a'], + ['a', 'a', 'a', 'a'], + ['a', 'a', 'a', 'a', 'a'], + ['a', 'a', 'a', 'a', 'a', 'a'], + ['a', 'a', 'a', 'a', 'a', 'a', 'a'], + ]); + }); + + it('should detect circular dependency cycles with object values when fromIterable', () => { + expect( + Array.from( + DependencyGraph.fromIterable([ + { value: { key: 1 }, provides: ['a'], consumes: ['b'] }, + { value: { key: 2 }, provides: ['b'], consumes: ['c', 'a'] }, + { value: { key: 3 }, provides: ['c'], consumes: ['d', 'a'] }, + { value: { key: 4 }, provides: ['d'], consumes: ['e', 'a'] }, + { value: { key: 5 }, provides: ['e'], consumes: ['f', 'a'] }, + { value: { key: 6 }, provides: ['f'], consumes: ['h', 'a'] }, + ]).detectCircularDependencies(), + ), + ).toEqual([ + [{ key: 1 }, { key: 2 }, { key: 1 }], + [{ key: 1 }, { key: 2 }, { key: 3 }, { key: 1 }], + [{ key: 1 }, { key: 2 }, { key: 3 }, { key: 4 }, { key: 1 }], + [ + { key: 1 }, + { key: 2 }, + { key: 3 }, + { key: 4 }, + { key: 5 }, + { key: 1 }, + ], + [ + { key: 1 }, + { key: 2 }, + { key: 3 }, + { key: 4 }, + { key: 5 }, + { key: 6 }, + { key: 1 }, + ], + ]); + }); + + it('should detect circular dependency cycles with array values when fromIterable', () => { + expect( + Array.from( + DependencyGraph.fromIterable([ + { value: [1], provides: ['a'], consumes: ['b'] }, + { value: [2], provides: ['b'], consumes: ['c', 'a'] }, + { value: [3], provides: ['c'], consumes: ['d', 'a'] }, + { value: [4], provides: ['d'], consumes: ['e', 'a'] }, + { value: [5], provides: ['e'], consumes: ['f', 'a'] }, + { value: [6], provides: ['f'], consumes: ['h', 'a'] }, + ]).detectCircularDependencies(), + ), + ).toEqual([ + [[1], [2], [1]], + [[1], [2], [3], [1]], + [[1], [2], [3], [4], [1]], + [[1], [2], [3], [4], [5], [1]], + [[1], [2], [3], [4], [5], [6], [1]], + ]); + }); + + it('should detect circular dependency cycles by reference with symbol values when fromIterable', () => { + const symbol1 = Symbol(1); + const symbol2 = Symbol(2); + const symbol3 = Symbol(3); + const symbol4 = Symbol(4); + const symbol5 = Symbol(5); + const symbol6 = Symbol(6); + + expect( + Array.from( + DependencyGraph.fromIterable([ + { value: symbol1, provides: ['a'], consumes: ['b'] }, + { value: symbol2, provides: ['b'], consumes: ['c', 'a'] }, + { value: symbol3, provides: ['c'], consumes: ['d', 'a'] }, + { value: symbol4, provides: ['d'], consumes: ['e', 'a'] }, + { value: symbol5, provides: ['e'], consumes: ['f', 'a'] }, + { value: symbol6, provides: ['f'], consumes: ['h', 'a'] }, + ]).detectCircularDependencies(), + ), + ).toEqual([ + [symbol1, symbol2, symbol1], + [symbol1, symbol2, symbol3, symbol1], + [symbol1, symbol2, symbol3, symbol4, symbol1], + [symbol1, symbol2, symbol3, symbol4, symbol5, symbol1], + [symbol1, symbol2, symbol3, symbol4, symbol5, symbol6, symbol1], + ]); + }); + + it('should ignore circular dependency cycles by reference with symbol values when fromMap', () => { + const symbol1 = Symbol('1'); + const symbol2 = Symbol('2'); + const symbol3 = Symbol('3'); + const symbol4 = Symbol('4'); + const symbol5 = Symbol('5'); + const symbol6 = Symbol('6'); + + expect( + Array.from( + DependencyGraph.fromMap({ + [symbol1]: { provides: ['a'], consumes: ['b'] }, + [symbol2]: { provides: ['b'], consumes: ['c', 'a'] }, + [symbol3]: { provides: ['c'], consumes: ['d', 'a'] }, + [symbol4]: { provides: ['d'], consumes: ['e', 'a'] }, + [symbol5]: { provides: ['e'], consumes: ['f', 'a'] }, + [symbol6]: { provides: ['f'], consumes: ['h', 'a'] }, + }).detectCircularDependencies(), + ), + ).toEqual([]); + }); + }); + + describe('findUnsatisfiedDeps', () => { + it('should return nothing with no deps', () => { + expect( + DependencyGraph.fromMap({ + 1: {}, + 2: {}, + 3: {}, + 4: {}, + }).findUnsatisfiedDeps(), + ).toEqual([]); + }); + + it('should return nothing when all deps are satisfied', async () => { + expect( + DependencyGraph.fromMap({ + 1: { provides: ['a'] }, + 2: { consumes: ['a'], provides: ['b', 'c'] }, + 3: { consumes: ['b'] }, + 4: { consumes: ['c'] }, + }).findUnsatisfiedDeps(), + ).toEqual([]); + }); + + it('should find a single unsatisfied dep', async () => { + expect( + DependencyGraph.fromMap({ + 1: { consumes: ['a'] }, + }).findUnsatisfiedDeps(), + ).toEqual([{ value: '1', unsatisfied: ['a'] }]); + }); + + it('should handle circular dependencies', async () => { + expect( + DependencyGraph.fromMap({ + 1: { consumes: ['a'], provides: ['a'] }, + }).findUnsatisfiedDeps(), + ).toEqual([]); + + expect( + DependencyGraph.fromMap({ + 1: { consumes: ['a'], provides: ['b'] }, + 2: { consumes: ['b'], provides: ['a'] }, + }).findUnsatisfiedDeps(), + ).toEqual([]); + + expect( + DependencyGraph.fromMap({ + 1: { consumes: ['a'] }, + 2: { consumes: ['b'], provides: ['c'] }, + 3: { consumes: ['c'], provides: ['a', 'b'] }, + }).findUnsatisfiedDeps(), + ).toEqual([]); + }); + + it('should find multiple unsatisfied deps for one node', async () => { + expect( + DependencyGraph.fromMap({ + 1: { provides: ['a'], consumes: ['b'] }, + 2: { provides: ['b'], consumes: ['a', 'd', 'e'] }, + }).findUnsatisfiedDeps(), + ).toEqual([{ value: '2', unsatisfied: ['d', 'e'] }]); + }); + + it('should find multiple unsatisfied deps for multiple nodes', async () => { + expect( + DependencyGraph.fromMap({ + 1: { provides: ['a'] }, + 2: { provides: ['b'], consumes: ['a', 'd', 'e'] }, + 3: { provides: [], consumes: ['b'] }, + 4: { provides: [], consumes: ['c', 'a'] }, + }).findUnsatisfiedDeps(), + ).toEqual([ + { value: '2', unsatisfied: ['d', 'e'] }, + { value: '4', unsatisfied: ['c'] }, + ]); + }); + }); + + describe('parallelTopologicalTraversal', () => { + it('should traverse with no deps', async () => { + await expect( + DependencyGraph.fromMap({ + 1: {}, + 2: {}, + 3: {}, + 4: {}, + }).parallelTopologicalTraversal(async id => id), + ).resolves.toEqual(['1', '2', '3', '4']); + }); + + it('should traverse with a few deps', async () => { + await expect( + DependencyGraph.fromMap({ + 1: { provides: ['a'] }, + 2: { consumes: ['a'], provides: ['b', 'c'] }, + 3: { consumes: ['b'] }, + 4: { consumes: ['c'] }, + }).parallelTopologicalTraversal(async id => id), + ).resolves.toEqual(['1', '2', '3', '4']); + }); + + it('should traverse in reverse', async () => { + await expect( + DependencyGraph.fromMap({ + 1: { consumes: ['c'] }, + 2: { provides: ['c'], consumes: ['b'] }, + 3: { provides: ['b'], consumes: ['a'] }, + 4: { provides: ['a'] }, + }).parallelTopologicalTraversal(async id => id), + ).resolves.toEqual(['4', '3', '2', '1']); + }); + + it('should execute in parallel', async () => { + await expect( + DependencyGraph.fromMap({ + 1: { provides: ['a'] }, + 2: { provides: ['b'], consumes: ['a'] }, + 3: { provides: ['c'], consumes: ['a'] }, + 4: { consumes: ['b'] }, + 5: { consumes: ['c'] }, + }).parallelTopologicalTraversal(async id => id), + ).resolves.toEqual(['1', '2', '3', '4', '5']); + + // Same as above, but with 2 being delayed + await expect( + DependencyGraph.fromMap({ + 1: { provides: ['a'] }, + 2: { provides: ['b'], consumes: ['a'] }, + 3: { provides: ['c'], consumes: ['a'] }, + 4: { consumes: ['b'] }, + 5: { consumes: ['c'] }, + }).parallelTopologicalTraversal(async id => { + // When delaying 2 we expect 3 and 5 to complete before 2 and 4 + if (id === '2') { + await new Promise(resolve => setTimeout(resolve, 100)); + } + return id; + }), + ).resolves.toEqual(['1', '3', '5', '2', '4']); + }); + + it('should detect circular dependencies', async () => { + await expect( + DependencyGraph.fromMap({ + 1: { provides: ['a'], consumes: ['a'] }, + }).parallelTopologicalTraversal(async id => id), + ).rejects.toThrow('Circular dependency detected'); + + await expect( + DependencyGraph.fromMap({ + 1: { provides: ['a'], consumes: ['b'] }, + 2: { provides: ['b'], consumes: ['a'] }, + }).parallelTopologicalTraversal(async id => id), + ).rejects.toThrow('Circular dependency detected'); + + await expect( + DependencyGraph.fromMap({ + 1: { provides: ['a'] }, + 2: { provides: ['c'], consumes: ['a', 'b'] }, + 3: { provides: ['b'], consumes: ['a', 'c'] }, + }).parallelTopologicalTraversal(async id => id), + ).rejects.toThrow('Circular dependency detected'); + }); + }); +}); diff --git a/packages/backend-app-api/src/lib/DependencyGraph.ts b/packages/backend-app-api/src/lib/DependencyGraph.ts new file mode 100644 index 0000000000..86fad3fbf2 --- /dev/null +++ b/packages/backend-app-api/src/lib/DependencyGraph.ts @@ -0,0 +1,246 @@ +/* + * 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. + */ + +interface NodeInput { + value: T; + consumes?: Iterable; + provides?: Iterable; +} + +/** @internal */ +class Node { + static from(input: NodeInput) { + return new Node( + input.value, + input.consumes ? new Set(input.consumes) : new Set(), + input.provides ? new Set(input.provides) : new Set(), + ); + } + + private constructor( + readonly value: T, + readonly consumes: Set, + readonly provides: Set, + ) {} +} + +/** @internal */ +class CycleKeySet { + static from(nodes: Array>) { + return new CycleKeySet(nodes); + } + + #nodeIds: Map; + #cycleKeys: Set; + + private constructor(nodes: Array>) { + this.#nodeIds = new Map(nodes.map((n, i) => [n.value, i])); + this.#cycleKeys = new Set(); + } + + tryAdd(path: T[]): boolean { + const cycleKey = this.#getCycleKey(path); + if (this.#cycleKeys.has(cycleKey)) { + return false; + } + this.#cycleKeys.add(cycleKey); + return true; + } + + #getCycleKey(path: T[]): string { + return path + .map(n => this.#nodeIds.get(n)!) + .sort() + .join(','); + } +} + +/** + * Internal helper to help validate and traverse a dependency graph. + * @internal + */ +export class DependencyGraph { + static fromMap( + nodes: Record, 'value'>>, + ): DependencyGraph { + return this.fromIterable( + Object.entries(nodes).map(([key, node]) => ({ + value: String(key), + ...node, + })), + ); + } + + static fromIterable( + nodeInputs: Iterable>, + ): DependencyGraph { + const nodes = new Array>(); + for (const nodeInput of nodeInputs) { + nodes.push(Node.from(nodeInput)); + } + + return new DependencyGraph(nodes); + } + + #nodes: Array>; + #allProvided: Set; + + private constructor(nodes: Array>) { + this.#nodes = nodes; + this.#allProvided = new Set(); + + for (const node of this.#nodes.values()) { + for (const produced of node.provides) { + this.#allProvided.add(produced); + } + } + } + + /** + * Find all nodes that consume dependencies that are not provided by any other node. + */ + findUnsatisfiedDeps(): Array<{ value: T; unsatisfied: string[] }> { + const unsatisfiedDependencies = []; + for (const node of this.#nodes.values()) { + const unsatisfied = Array.from(node.consumes).filter( + id => !this.#allProvided.has(id), + ); + if (unsatisfied.length > 0) { + unsatisfiedDependencies.push({ value: node.value, unsatisfied }); + } + } + return unsatisfiedDependencies; + } + + /** + * Detect the first circular dependency within the graph, returning the path of nodes that + * form a cycle, with the same node as the first and last element of the array. + */ + detectCircularDependency(): T[] | undefined { + return this.detectCircularDependencies().next().value; + } + + /** + * Detect circular dependencies within the graph, returning the path of nodes that + * form a cycle, with the same node as the first and last element of the array. + */ + *detectCircularDependencies(): Generator { + const cycleKeys = CycleKeySet.from(this.#nodes); + + for (const startNode of this.#nodes) { + const visited = new Set>(); + const stack = new Array<[node: Node, path: T[]]>([ + startNode, + [startNode.value], + ]); + + while (stack.length > 0) { + const [node, path] = stack.pop()!; + if (visited.has(node)) { + continue; + } + visited.add(node); + for (const consumed of node.consumes) { + const providerNodes = this.#nodes.filter(other => + other.provides.has(consumed), + ); + for (const provider of providerNodes) { + if (provider === startNode) { + if (cycleKeys.tryAdd(path)) { + yield [...path, startNode.value]; + } + + break; + } + if (!visited.has(provider)) { + stack.push([provider, [...path, provider.value]]); + } + } + } + } + } + return undefined; + } + + /** + * Traverses the dependency graph in topological order, calling the provided + * function for each node and waiting for it to resolve. + * + * The nodes are traversed in parallel, but in such a way that no node is + * visited before all of its dependencies. + * + * Dependencies of nodes that are not produced by any other nodes will be ignored. + */ + async parallelTopologicalTraversal( + fn: (value: T) => Promise, + ): Promise { + const allProvided = this.#allProvided; + const producedSoFar = new Set(); + const waiting = new Set(this.#nodes.values()); + const visited = new Set>(); + const results = new Array(); + let inFlight = 0; // Keep track of how many callbacks are in flight, so that we know if we got stuck + + // Find all nodes that have no dependencies that have not already been produced by visited nodes + async function processMoreNodes() { + if (waiting.size === 0) { + return; + } + const nodesToProcess = []; + for (const node of waiting) { + let ready = true; + for (const consumed of node.consumes) { + if (allProvided.has(consumed) && !producedSoFar.has(consumed)) { + ready = false; + continue; + } + } + if (ready) { + nodesToProcess.push(node); + } + } + + for (const node of nodesToProcess) { + waiting.delete(node); + } + + if (nodesToProcess.length === 0 && inFlight === 0) { + // We expect the caller to check for circular dependencies before + // traversal, so this error should never happen + throw new Error('Circular dependency detected'); + } + + await Promise.all(nodesToProcess.map(processNode)); + } + + // Process an individual node, and then add its produced dependencies to the set of available products + async function processNode(node: Node) { + visited.add(node); + inFlight += 1; + + const result = await fn(node.value); + results.push(result); + + node.provides.forEach(produced => producedSoFar.add(produced)); + inFlight -= 1; + await processMoreNodes(); + } + + await processMoreNodes(); + + return results; + } +} diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts index d0b40df33b..a6c586d806 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts @@ -14,39 +14,29 @@ * limitations under the License. */ +import { + ServiceFactoryTester, + mockServices, +} from '@backstage/backend-test-utils'; import { httpRouterServiceFactory } from './httpRouterServiceFactory'; describe('httpRouterFactory', () => { it('should register plugin paths', async () => { - const rootHttpRouter = { use: jest.fn() }; - const factory = httpRouterServiceFactory() as any; + const rootHttpRouter = mockServices.rootHttpRouter.mock(); + const tester = ServiceFactoryTester.from(httpRouterServiceFactory, { + dependencies: [rootHttpRouter.factory], + }); - const handler1 = () => {}; - const router1 = await factory.factory( - { - rootHttpRouter, - plugin: { getId: () => 'test1' }, - lifecycle: { addStartupHook() {}, addShutdownHook() {} }, - }, - undefined, - ); - router1.use(handler1); + const router1 = await tester.get('test1'); + router1.use(() => {}); expect(rootHttpRouter.use).toHaveBeenCalledTimes(1); expect(rootHttpRouter.use).toHaveBeenCalledWith( '/api/test1', expect.any(Function), ); - const handler2 = () => {}; - const router2 = await factory.factory( - { - rootHttpRouter, - plugin: { getId: () => 'test2' }, - lifecycle: { addStartupHook() {}, addShutdownHook() {} }, - }, - undefined, - ); - router2.use(handler2); + const router2 = await tester.get('test2'); + router2.use(() => {}); expect(rootHttpRouter.use).toHaveBeenCalledTimes(2); expect(rootHttpRouter.use).toHaveBeenCalledWith( '/api/test2', @@ -55,37 +45,24 @@ describe('httpRouterFactory', () => { }); it('should use custom path generator', async () => { - const rootHttpRouter = { use: jest.fn() }; - const factory = httpRouterServiceFactory({ - getPath: id => `/some/${id}/path`, - }) as any; - - const handler1 = () => {}; - const router1 = await factory.factory( - { - rootHttpRouter, - plugin: { getId: () => 'test1' }, - lifecycle: { addStartupHook() {}, addShutdownHook() {} }, - }, - undefined, + const rootHttpRouter = mockServices.rootHttpRouter.mock(); + const tester = ServiceFactoryTester.from( + httpRouterServiceFactory({ + getPath: id => `/some/${id}/path`, + }), + { dependencies: [rootHttpRouter.factory] }, ); - router1.use(handler1); + + const router1 = await tester.get('test1'); + router1.use(() => {}); expect(rootHttpRouter.use).toHaveBeenCalledTimes(1); expect(rootHttpRouter.use).toHaveBeenCalledWith( '/some/test1/path', expect.any(Function), ); - const handler2 = () => {}; - const router2 = await factory.factory( - { - rootHttpRouter, - plugin: { getId: () => 'test2' }, - lifecycle: { addStartupHook() {}, addShutdownHook() {} }, - }, - undefined, - ); - router2.use(handler2); + const router2 = await tester.get('test2'); + router2.use(() => {}); expect(rootHttpRouter.use).toHaveBeenCalledTimes(2); expect(rootHttpRouter.use).toHaveBeenCalledWith( '/some/test2/path', 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 7af9de44da..7b889b3a0e 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 @@ -14,55 +14,33 @@ * limitations under the License. */ -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { startTestBackend } from '@backstage/backend-test-utils'; +import { coreServices } from '@backstage/backend-plugin-api'; +import { ServiceFactoryTester } from '@backstage/backend-test-utils'; import { schedulerServiceFactory } from './schedulerServiceFactory'; describe('schedulerFactory', () => { it('creates sidecar database features', async () => { - expect.assertions(3); + const tester = ServiceFactoryTester.from(schedulerServiceFactory()); - const subject = schedulerServiceFactory(); - - const plugin = createBackendPlugin({ - pluginId: 'example', - register(reg) { - reg.registerInit({ - deps: { - scheduler: subject.service, - database: coreServices.database, - }, - init: async ({ scheduler, database }) => { - await scheduler.scheduleTask({ - id: 'task1', - timeout: { seconds: 1 }, - frequency: { seconds: 1 }, - fn: async () => {}, - }); - - const client = await database.getClient(); - await expect( - client.from('backstage_backend_tasks__tasks').count(), - ).resolves.toEqual([{ 'count(*)': 1 }]); - await expect( - client.from('backstage_backend_tasks__knex_migrations').count(), - ).resolves.toEqual([{ 'count(*)': expect.any(Number) }]); - await expect( - client - .from('backstage_backend_tasks__knex_migrations_lock') - .count(), - ).resolves.toEqual([{ 'count(*)': expect.any(Number) }]); - }, - }); - }, + const scheduler = await tester.get(); + await scheduler.scheduleTask({ + id: 'task1', + timeout: { seconds: 1 }, + frequency: { seconds: 1 }, + fn: async () => {}, }); - await startTestBackend({ - features: [plugin()], - services: [subject], - }); + const database = await tester.getService(coreServices.database); + + const client = await database.getClient(); + await expect( + client.from('backstage_backend_tasks__tasks').count(), + ).resolves.toEqual([{ 'count(*)': 1 }]); + await expect( + client.from('backstage_backend_tasks__knex_migrations').count(), + ).resolves.toEqual([{ 'count(*)': expect.any(Number) }]); + await expect( + client.from('backstage_backend_tasks__knex_migrations_lock').count(), + ).resolves.toEqual([{ 'count(*)': expect.any(Number) }]); }); }); diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index b05194f594..791b9b1c3a 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -20,10 +20,15 @@ import { coreServices, createBackendPlugin, createBackendModule, + createExtensionPoint, } from '@backstage/backend-plugin-api'; import { BackendInitializer } from './BackendInitializer'; -import { ServiceRegistry } from './ServiceRegistry'; -import { rootLifecycleServiceFactory } from '../services/implementations'; + +import { + lifecycleServiceFactory, + loggerServiceFactory, + rootLifecycleServiceFactory, +} from '../services/implementations'; const rootRef = createServiceRef<{ x: number }>({ id: '1', @@ -44,12 +49,23 @@ class MockLogger { } } +const baseFactories = [ + lifecycleServiceFactory(), + rootLifecycleServiceFactory(), + createServiceFactory({ + service: coreServices.rootLogger, + deps: {}, + factory: () => new MockLogger(), + })(), + loggerServiceFactory(), +]; + describe('BackendInitializer', () => { it('should initialize root scoped services', async () => { const rootFactory = jest.fn(); const pluginFactory = jest.fn(); - const registry = new ServiceRegistry([ + const services = [ createServiceFactory({ service: rootRef, deps: {}, @@ -66,17 +82,76 @@ describe('BackendInitializer', () => { deps: {}, factory: () => new MockLogger(), })(), - ]); + ]; - const init = new BackendInitializer(registry); + const init = new BackendInitializer(services); await init.start(); expect(rootFactory).toHaveBeenCalled(); expect(pluginFactory).not.toHaveBeenCalled(); }); + it('should initialize modules with extension points', async () => { + expect.assertions(3); + + const extensionPoint = createExtensionPoint<{ values: string[] }>({ + id: 'a', + }); + const init = new BackendInitializer(baseFactories); + + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'modA', + register(reg) { + reg.registerInit({ + deps: { extension: extensionPoint }, + async init({ extension }) { + expect(extension.values).toEqual(['b']); + extension.values.push('a'); + }, + }); + }, + })(), + ); + + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'modB', + register(reg) { + const values = ['b']; + reg.registerExtensionPoint(extensionPoint, { values }); + reg.registerInit({ + deps: {}, + async init() { + expect(values).toEqual(['b', 'a', 'c']); + }, + }); + }, + })(), + ); + + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'modC', + register(reg) { + reg.registerInit({ + deps: { extension: extensionPoint }, + async init({ extension }) { + expect(extension.values).toEqual(['b', 'a']); + extension.values.push('c'); + }, + }); + }, + })(), + ); + await init.start(); + }); + it('should forward errors when plugins fail to start', async () => { - const init = new BackendInitializer(new ServiceRegistry([])); + const init = new BackendInitializer([]); init.add( createBackendPlugin({ pluginId: 'test', @@ -96,7 +171,7 @@ describe('BackendInitializer', () => { }); it('should forward errors when modules fail to start', async () => { - const init = new BackendInitializer(new ServiceRegistry([])); + const init = new BackendInitializer([]); init.add( createBackendModule({ pluginId: 'test', @@ -117,7 +192,7 @@ describe('BackendInitializer', () => { }); it('should reject duplicate plugins', async () => { - const init = new BackendInitializer(new ServiceRegistry([])); + const init = new BackendInitializer([]); init.add( createBackendPlugin({ pluginId: 'test', @@ -146,7 +221,7 @@ describe('BackendInitializer', () => { }); it('should reject duplicate modules', async () => { - const init = new BackendInitializer(new ServiceRegistry([])); + const init = new BackendInitializer([]); init.add( createBackendModule({ pluginId: 'test', @@ -175,4 +250,78 @@ describe('BackendInitializer', () => { "Module 'mod' for plugin 'test' is already registered", ); }); + + it('should reject modules with circular dependencies', async () => { + const extA = createExtensionPoint({ id: 'a' }); + const extB = createExtensionPoint({ id: 'b' }); + const init = new BackendInitializer([ + rootLifecycleServiceFactory(), + createServiceFactory({ + service: coreServices.rootLogger, + deps: {}, + factory: () => new MockLogger(), + })(), + ]); + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'modA', + register(reg) { + reg.registerExtensionPoint(extA, 'a'); + reg.registerInit({ + deps: { ext: extB }, + async init() {}, + }); + }, + })(), + ); + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'modB', + register(reg) { + reg.registerExtensionPoint(extB, 'b'); + reg.registerInit({ + deps: { ext: extA }, + async init() {}, + }); + }, + })(), + ); + await expect(init.start()).rejects.toThrow( + "Circular dependency detected for modules of plugin 'test', 'modA' -> 'modB' -> 'modA'", + ); + }); + + it('should reject modules that depend on extension points other plugins', async () => { + const init = new BackendInitializer(baseFactories); + const extA = createExtensionPoint({ id: 'a' }); + init.add( + createBackendPlugin({ + pluginId: 'testA', + register(reg) { + reg.registerExtensionPoint(extA, 'a'); + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + })(), + ); + init.add( + createBackendModule({ + pluginId: 'testB', + moduleId: 'mod', + register(reg) { + reg.registerInit({ + deps: { ext: extA }, + async init() {}, + }); + }, + })(), + ); + await expect(init.start()).rejects.toThrow( + "Extension point registered for plugin 'testA' may not be used by module for plugin 'testB'", + ); + }); }); diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index ec4b2a79ac..4969f82176 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -19,14 +19,18 @@ import { ExtensionPoint, coreServices, ServiceRef, + ServiceFactory, } from '@backstage/backend-plugin-api'; import { BackendLifecycleImpl } from '../services/implementations/rootLifecycle/rootLifecycleServiceFactory'; import { BackendPluginLifecycleImpl } from '../services/implementations/lifecycle/lifecycleServiceFactory'; -import { EnumerableServiceHolder, ServiceOrExtensionPoint } from './types'; +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'; -import { ForwardedError } from '@backstage/errors'; +import { ForwardedError, ConflictError } from '@backstage/errors'; +import { featureDiscoveryServiceRef } from '@backstage/backend-plugin-api/alpha'; +import { DependencyGraph } from '../lib/DependencyGraph'; +import { ServiceRegistry } from './ServiceRegistry'; export interface BackendRegisterInit { consumes: Set; @@ -40,11 +44,12 @@ export interface BackendRegisterInit { export class BackendInitializer { #startPromise?: Promise; #features = new Array(); - #extensionPoints = new Map, unknown>(); - #serviceHolder: EnumerableServiceHolder; + #extensionPoints = new Map(); + #serviceRegistry: ServiceRegistry; + #registeredFeatures = new Array>(); - constructor(serviceHolder: EnumerableServiceHolder) { - this.#serviceHolder = serviceHolder; + constructor(defaultApiFactories: ServiceFactory[]) { + this.#serviceRegistry = ServiceRegistry.create([...defaultApiFactories]); } async #getInitDeps( @@ -55,13 +60,16 @@ export class BackendInitializer { const missingRefs = new Set(); for (const [name, ref] of Object.entries(deps)) { - const extensionPoint = this.#extensionPoints.get( - ref as ExtensionPoint, - ); - if (extensionPoint) { - result.set(name, extensionPoint); + const ep = this.#extensionPoints.get(ref.id); + if (ep) { + if (ep.pluginId !== pluginId) { + throw new Error( + `Extension point registered for plugin '${ep.pluginId}' may not be used by module for plugin '${pluginId}'`, + ); + } + result.set(name, ep.impl); } else { - const impl = await this.#serviceHolder.get( + const impl = await this.#serviceRegistry.get( ref as ServiceRef, pluginId, ); @@ -83,22 +91,34 @@ export class BackendInitializer { return Object.fromEntries(result); } - add(feature: BackendFeature) { + add(feature: BackendFeature | Promise) { if (this.#startPromise) { throw new Error('feature can not be added after the backend has started'); } + this.#registeredFeatures.push(Promise.resolve(feature)); + } + + #addFeature(feature: BackendFeature) { if (feature.$$type !== '@backstage/BackendFeature') { throw new Error( `Failed to add feature, invalid type '${feature.$$type}'`, ); } - const internalFeature = feature as InternalBackendFeature; - if (internalFeature.version !== 'v1') { + + 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 { throw new Error( - `Failed to add feature, invalid version '${internalFeature.version}'`, + `Failed to add feature, invalid feature ${JSON.stringify(feature)}`, ); } - this.#features.push(internalFeature); } async start(): Promise { @@ -129,10 +149,29 @@ export class BackendInitializer { } async #doStart(): Promise { + this.#serviceRegistry.checkForCircularDeps(); + + for (const feature of this.#registeredFeatures) { + this.#addFeature(await feature); + } + + const featureDiscovery = await this.#serviceRegistry.get( + featureDiscoveryServiceRef, + 'root', + ); + + if (featureDiscovery) { + const { features } = await featureDiscovery.getBackendFeatures(); + for (const feature of features) { + this.#addFeature(feature); + } + this.#serviceRegistry.checkForCircularDeps(); + } + // Initialize all root scoped services - for (const ref of this.#serviceHolder.getServiceRefs()) { + for (const ref of this.#serviceRegistry.getServiceRefs()) { if (ref.scope === 'root') { - await this.#serviceHolder.get(ref, 'root'); + await this.#serviceRegistry.get(ref, 'root'); } } @@ -144,14 +183,17 @@ export class BackendInitializer { for (const r of feature.getRegistrations()) { const provides = new Set>(); - if (r.type === 'plugin') { + if (r.type === 'plugin' || r.type === 'module') { for (const [extRef, extImpl] of r.extensionPoints) { - if (this.#extensionPoints.has(extRef)) { + if (this.#extensionPoints.has(extRef.id)) { throw new Error( `ExtensionPoint with ID '${extRef.id}' is already registered`, ); } - this.#extensionPoints.set(extRef, extImpl); + this.#extensionPoints.set(extRef.id, { + impl: extImpl, + pluginId: r.pluginId, + }); provides.add(extRef); } } @@ -193,21 +235,41 @@ export class BackendInitializer { await Promise.all( allPluginIds.map(async pluginId => { // Modules are initialized before plugins, so that they can provide extension to the plugin - const modules = moduleInits.get(pluginId) ?? []; - await Promise.all( - Array.from(modules).map(async ([moduleId, moduleInit]) => { - const moduleDeps = await this.#getInitDeps( - moduleInit.init.deps, - pluginId, + const modules = moduleInits.get(pluginId); + if (modules) { + const tree = DependencyGraph.fromIterable( + Array.from(modules).map(([moduleId, moduleInit]) => ({ + value: { moduleId, moduleInit }, + // Relationships are reversed at this point since we're only interested in the extension points. + // If a modules provides extension point A we want it to be initialized AFTER all modules + // that depend on extension point A, so that they can provide their extensions. + consumes: Array.from(moduleInit.provides).map(p => p.id), + provides: Array.from(moduleInit.consumes).map(c => c.id), + })), + ); + const circular = tree.detectCircularDependency(); + if (circular) { + throw new ConflictError( + `Circular dependency detected for modules of plugin '${pluginId}', ${circular + .map(({ moduleId }) => `'${moduleId}'`) + .join(' -> ')}`, ); - await moduleInit.init.func(moduleDeps).catch(error => { - throw new ForwardedError( - `Module '${moduleId}' for plugin '${pluginId}' startup failed`, - error, + } + await tree.parallelTopologicalTraversal( + async ({ moduleId, moduleInit }) => { + const moduleDeps = await this.#getInitDeps( + moduleInit.init.deps, + pluginId, ); - }); - }), - ); + await moduleInit.init.func(moduleDeps).catch(error => { + throw new ForwardedError( + `Module '${moduleId}' for plugin '${pluginId}' startup failed`, + error, + ); + }); + }, + ); + } // Once all modules have been initialized, we can initialize the plugin itself const pluginInit = pluginInits.get(pluginId); @@ -238,7 +300,7 @@ export class BackendInitializer { // Once the backend is started, any uncaught errors or unhandled rejections are caught // and logged, in order to avoid crashing the entire backend on local failures. if (process.env.NODE_ENV !== 'test') { - const rootLogger = await this.#serviceHolder.get( + const rootLogger = await this.#serviceRegistry.get( coreServices.rootLogger, 'root', ); @@ -272,7 +334,7 @@ export class BackendInitializer { // Bit of a hacky way to grab the lifecycle services, potentially find a nicer way to do this async #getRootLifecycleImpl(): Promise { - const lifecycleService = await this.#serviceHolder.get( + const lifecycleService = await this.#serviceRegistry.get( coreServices.rootLifecycle, 'root', ); @@ -285,7 +347,7 @@ export class BackendInitializer { async #getPluginLifecycleImpl( pluginId: string, ): Promise { - const lifecycleService = await this.#serviceHolder.get( + const lifecycleService = await this.#serviceRegistry.get( coreServices.lifecycle, pluginId, ); @@ -295,3 +357,15 @@ export class BackendInitializer { throw new Error('Unexpected plugin lifecycle service implementation'); } } + +function isServiceFactory(feature: BackendFeature): feature is ServiceFactory { + return !!(feature as ServiceFactory).service; +} + +function isInternalBackendFeature( + feature: BackendFeature, +): feature is InternalBackendFeature { + return ( + typeof (feature as InternalBackendFeature).getRegistrations === 'function' + ); +} diff --git a/packages/backend-app-api/src/wiring/BackstageBackend.ts b/packages/backend-app-api/src/wiring/BackstageBackend.ts index c53f47b685..2b916908de 100644 --- a/packages/backend-app-api/src/wiring/BackstageBackend.ts +++ b/packages/backend-app-api/src/wiring/BackstageBackend.ts @@ -14,22 +14,28 @@ * limitations under the License. */ -import { ServiceFactory, BackendFeature } from '@backstage/backend-plugin-api'; +import { BackendFeature, ServiceFactory } from '@backstage/backend-plugin-api'; import { BackendInitializer } from './BackendInitializer'; -import { ServiceRegistry } from './ServiceRegistry'; import { Backend } from './types'; export class BackstageBackend implements Backend { - #services: ServiceRegistry; #initializer: BackendInitializer; - constructor(apiFactories: ServiceFactory[]) { - this.#services = new ServiceRegistry(apiFactories); - this.#initializer = new BackendInitializer(this.#services); + constructor(defaultServiceFactories: ServiceFactory[]) { + this.#initializer = new BackendInitializer(defaultServiceFactories); } - add(feature: BackendFeature): void { - this.#initializer.add(feature); + add( + feature: + | BackendFeature + | (() => BackendFeature) + | Promise<{ default: BackendFeature | (() => BackendFeature) }>, + ): void { + if (isPromise(feature)) { + this.#initializer.add(feature.then(f => unwrapFeature(f.default))); + } else { + this.#initializer.add(unwrapFeature(feature)); + } } async start(): Promise { @@ -40,3 +46,18 @@ export class BackstageBackend implements Backend { await this.#initializer.stop(); } } + +function isPromise(value: unknown | Promise): value is Promise { + return ( + typeof value === 'object' && + value !== null && + 'then' in value && + typeof value.then === 'function' + ); +} + +function unwrapFeature( + feature: BackendFeature | (() => BackendFeature), +): BackendFeature { + return typeof feature === 'function' ? feature() : feature; +} diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts index e698ade348..32807ca826 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts @@ -31,7 +31,7 @@ const sf1 = createServiceFactory({ async factory() { return { x: 1 }; }, -})(); +}); const ref2 = createServiceRef<{ x: number }>({ scope: 'root', @@ -43,14 +43,14 @@ const sf2 = createServiceFactory({ async factory() { return { x: 2 }; }, -})(); +}); const sf2b = createServiceFactory({ service: ref2, deps: {}, async factory() { return { x: 22 }; }, -})(); +}); const refDefault1 = createServiceRef<{ x: number }>({ id: '1', @@ -61,7 +61,7 @@ const refDefault1 = createServiceRef<{ x: number }>({ async factory() { return { x: 10 }; }, - })(), + }), }); const refDefault2a = createServiceRef<{ x: number }>({ @@ -90,12 +90,12 @@ const refDefault2b = createServiceRef<{ x: number }>({ describe('ServiceRegistry', () => { it('should return undefined if there is no factory defined', async () => { - const registry = new ServiceRegistry([]); + const registry = ServiceRegistry.create([]); expect(registry.get(ref1, 'catalog')).toBe(undefined); }); it('should return an implementation for a registered ref', async () => { - const registry = new ServiceRegistry([sf1]); + const registry = ServiceRegistry.create([sf1()]); await expect(registry.get(ref1, 'catalog')).resolves.toEqual({ x: 1 }); await expect(registry.get(ref1, 'scaffolder')).resolves.toEqual({ x: 1 }); expect(await registry.get(ref1, 'catalog')).toBe( @@ -110,7 +110,7 @@ describe('ServiceRegistry', () => { }); it('should handle multiple factories with different serviceRefs', async () => { - const registry = new ServiceRegistry([sf1, sf2]); + const registry = ServiceRegistry.create([sf1(), sf2()]); await expect(registry.get(ref1, 'catalog')).resolves.toEqual({ x: 1, @@ -125,13 +125,14 @@ describe('ServiceRegistry', () => { it('should not be possible for root scoped services to depend on plugin scoped services', async () => { const factory = createServiceFactory({ + // @ts-expect-error service: ref2, deps: { pluginDep: ref1 }, async factory() { return { x: 2 }; }, }); - const registry = new ServiceRegistry([factory(), sf1]); + const registry = ServiceRegistry.create([factory(), sf1()]); await expect(registry.get(ref2, 'catalog')).rejects.toThrow( "Failed to instantiate 'root' scoped service '2' because it depends on 'plugin' scoped service '1'.", ); @@ -145,7 +146,7 @@ describe('ServiceRegistry', () => { return { x: rootDep.x }; }, }); - const registry = new ServiceRegistry([factory(), sf2]); + const registry = ServiceRegistry.create([factory(), sf2()]); await expect(registry.get(ref1, 'catalog')).resolves.toEqual({ x: 2, }); @@ -160,7 +161,7 @@ describe('ServiceRegistry', () => { return { x: rootDep.x }; }, }); - const registry = new ServiceRegistry([factory(), sf2]); + const registry = ServiceRegistry.create([factory(), sf2()]); await expect(registry.get(ref, 'catalog')).resolves.toEqual({ x: 2, }); @@ -175,35 +176,61 @@ describe('ServiceRegistry', () => { return { pluginId: meta.getId() }; }, }); - const registry = new ServiceRegistry([factory()]); + const registry = ServiceRegistry.create([factory()]); await expect(registry.get(ref, 'catalog')).resolves.toEqual({ pluginId: 'catalog', }); }); it('should use the last factory for each ref', async () => { - const registry = new ServiceRegistry([sf2, sf2b]); + const registry = ServiceRegistry.create([sf2(), sf2b()]); await expect(registry.get(ref2, 'catalog')).resolves.toEqual({ x: 22, }); }); + it('should use added service factories for each ref', async () => { + const registry = ServiceRegistry.create([sf2()]); + registry.add(sf2b()); + await expect(registry.get(ref2, 'catalog')).resolves.toEqual({ + x: 22, + }); + }); + + it('should not allow factories to be added after instantiation', async () => { + const registry = ServiceRegistry.create([sf2()]); + await expect(registry.get(ref2, 'catalog')).resolves.toEqual({ + x: 2, + }); + expect(() => registry.add(sf2b())).toThrow( + 'Unable to set service factory with id 2, service has already been instantiated', + ); + }); + + it('should not allow the same factory to be added twice', async () => { + const registry = ServiceRegistry.create([sf2()]); + registry.add(sf2b()); + expect(() => registry.add(sf2b())).toThrow( + 'Duplicate service implementations provided for 2', + ); + }); + it('should use the defaultFactory from the ref if not provided to the registry', async () => { - const registry = new ServiceRegistry([]); + const registry = ServiceRegistry.create([]); await expect(registry.get(refDefault1, 'catalog')).resolves.toEqual({ x: 10, }); }); it('should not use the defaultFactory from the ref if provided to the registry', async () => { - const registry = new ServiceRegistry([sf1]); + const registry = ServiceRegistry.create([sf1()]); await expect(registry.get(refDefault1, 'catalog')).resolves.toEqual({ x: 1, }); }); it('should handle duplicate defaultFactories by duplicating the implementations', async () => { - const registry = new ServiceRegistry([]); + const registry = ServiceRegistry.create([]); await expect(registry.get(refDefault2a, 'catalog')).resolves.toEqual({ x: 20, }); @@ -234,7 +261,7 @@ describe('ServiceRegistry', () => { defaultFactory: factoryLoader, }); - const registry = new ServiceRegistry([]); + const registry = ServiceRegistry.create([]); await Promise.all([ expect(registry.get(ref, 'catalog')).resolves.toBeUndefined(), expect(registry.get(ref, 'catalog')).resolves.toBeUndefined(), @@ -252,7 +279,7 @@ describe('ServiceRegistry', () => { factory, }); - const registry = new ServiceRegistry([myFactory()]); + const registry = ServiceRegistry.create([myFactory()]); await Promise.all([ registry.get(ref1, 'catalog')!, @@ -274,7 +301,7 @@ describe('ServiceRegistry', () => { factory, }); - const registry = new ServiceRegistry([myFactory()]); + const registry = ServiceRegistry.create([myFactory()]); await Promise.all([ registry.get(ref1, 'catalog')!, @@ -296,7 +323,7 @@ describe('ServiceRegistry', () => { }, }); - const registry = new ServiceRegistry([myFactory()]); + const registry = ServiceRegistry.create([myFactory()]); await expect(registry.get(ref1, 'catalog')).rejects.toThrow( "Failed to instantiate service '1' for 'catalog' because the following dependent services are missing: '2'", @@ -323,13 +350,217 @@ describe('ServiceRegistry', () => { }, }); - const registry = new ServiceRegistry([factoryA(), factoryB()]); + const registry = ServiceRegistry.create([factoryA(), factoryB()]); await expect(registry.get(refA, 'catalog')).rejects.toThrow( "Failed to instantiate service 'a' for 'catalog' because the factory function threw an error, Error: Failed to instantiate service 'b' for 'catalog' because the following dependent services are missing: 'c', 'd'", ); }); + describe('checkForCircularDeps', () => { + it('should throw if there are shallow circular dependencies', async () => { + const refA = createServiceRef({ id: 'a' }); + const refB = createServiceRef({ id: 'b' }); + + const factoryA = createServiceFactory({ + service: refA, + deps: { b: refB }, + factory: async ({ b }) => b, + }); + + const factoryB = createServiceFactory({ + service: refB, + deps: { a: refA }, + factory: async ({ a }) => a, + }); + + expect(() => ServiceRegistry.create([factoryA(), factoryB()])).toThrow( + `Circular dependencies detected: + 'a' -> 'b' -> 'a'`, + ); + }); + + it('should throw if there are multiple circular dependency cycles', async () => { + const refA = createServiceRef({ id: 'a' }); + const refB = createServiceRef({ id: 'b' }); + const refC = createServiceRef({ id: 'c' }); + const refD = createServiceRef({ id: 'd' }); + + const factoryA = createServiceFactory({ + service: refA, + deps: { b: refB }, + factory: async ({ b }) => b, + }); + + const factoryB = createServiceFactory({ + service: refB, + deps: { a: refA }, + factory: async ({ a }) => a, + }); + + const factoryC = createServiceFactory({ + service: refC, + deps: { d: refD }, + factory: async ({ d }) => d, + }); + + const factoryD = createServiceFactory({ + service: refD, + deps: { c: refC }, + factory: async ({ c }) => c, + }); + + expect(() => + ServiceRegistry.create([ + factoryA(), + factoryB(), + factoryC(), + factoryD(), + ]), + ).toThrow( + `Circular dependencies detected: + 'a' -> 'b' -> 'a' + 'c' -> 'd' -> 'c'`, + ); + }); + + it('should throw if there are deep circular dependencies', async () => { + const refA = createServiceRef({ id: 'a' }); + const refB = createServiceRef({ id: 'b' }); + const refC = createServiceRef({ id: 'c' }); + + const factoryA = createServiceFactory({ + service: refA, + deps: { b: refB }, + factory: async ({ b }) => b, + }); + + const factoryB = createServiceFactory({ + service: refB, + deps: { c: refC }, + factory: async ({ c }) => c, + }); + + const factoryC = createServiceFactory({ + service: refC, + deps: { a: refA }, + factory: async ({ a }) => a, + }); + + expect(() => + ServiceRegistry.create([factoryA(), factoryB(), factoryC()]), + ).toThrow( + `Circular dependencies detected: + 'a' -> 'b' -> 'c' -> 'a'`, + ); + }); + + it('should throw if there are deep circular dependencies 2', async () => { + const refA = createServiceRef({ id: 'a' }); + const refB = createServiceRef({ id: 'b' }); + const refC = createServiceRef({ id: 'c' }); + const refD = createServiceRef({ id: 'd' }); + + const factoryA = createServiceFactory({ + service: refA, + deps: { b: refB }, + factory: async ({ b }) => b, + }); + + const factoryB = createServiceFactory({ + service: refB, + deps: { c: refC, d: refD }, + factory: async ({ c, d }) => c + d, + }); + + const factoryC = createServiceFactory({ + service: refC, + deps: { a: refA }, + factory: async ({ a }) => a, + }); + + const factoryD = createServiceFactory({ + service: refD, + deps: {}, + factory: async () => 'd', + }); + + expect(() => + ServiceRegistry.create([ + factoryA(), + factoryB(), + factoryC(), + factoryD(), + ]), + ).toThrow( + `Circular dependencies detected: + 'a' -> 'b' -> 'c' -> 'a'`, + ); + }); + + it('should throw if there are circular dependencies', async () => { + const refA = createServiceRef({ id: 'a' }); + const refB = createServiceRef({ id: 'b' }); + const refC = createServiceRef({ id: 'c' }); + + const factoryA = createServiceFactory({ + service: refA, + deps: { b: refB, c: refC }, + factory: async ({ b, c }) => b + c, + }); + + const factoryB = createServiceFactory({ + service: refB, + deps: {}, + factory: async () => 'b', + }); + + const factoryC = createServiceFactory({ + service: refC, + deps: { a: refA }, + factory: async ({ a }) => a, + }); + + expect(() => + ServiceRegistry.create([factoryA(), factoryB(), factoryC()]), + ).toThrow( + `Circular dependencies detected: + 'a' -> 'c' -> 'a'`, + ); + }); + + it('should not infinitely loop if there are circular dependencies where not all nodes are in the cycle', async () => { + const refA = createServiceRef({ id: 'a' }); + const refB = createServiceRef({ id: 'b' }); + const refC = createServiceRef({ id: 'c' }); + + const factoryA = createServiceFactory({ + service: refA, + deps: { b: refB }, + factory: async ({ b }) => b, + }); + + const factoryB = createServiceFactory({ + service: refB, + deps: { c: refC }, + factory: async ({ c }) => c, + }); + + const factoryC = createServiceFactory({ + service: refC, + deps: { b: refB }, + factory: async ({ b }) => b, + }); + + expect(() => + ServiceRegistry.create([factoryA(), factoryB(), factoryC()]), + ).toThrow( + `Circular dependencies detected: + 'b' -> 'c' -> 'b'`, + ); + }); + }); + it('should decorate error messages thrown by the top-level factory function', async () => { const myFactory = createServiceFactory({ service: ref1, @@ -342,7 +573,7 @@ describe('ServiceRegistry', () => { }, }); - const registry = new ServiceRegistry([myFactory()]); + const registry = ServiceRegistry.create([myFactory()]); await expect(registry.get(ref1, 'catalog')).rejects.toThrow( "Failed to instantiate service '1' because createRootContext threw an error, Error: top-level error", @@ -358,7 +589,7 @@ describe('ServiceRegistry', () => { }, }); - const registry = new ServiceRegistry([myFactory()]); + const registry = ServiceRegistry.create([myFactory()]); await expect(registry.get(ref1, 'catalog')).rejects.toThrow( "Failed to instantiate service '1' for 'catalog' because the factory function threw an error, Error: error in plugin", @@ -373,7 +604,7 @@ describe('ServiceRegistry', () => { }, }); - const registry = new ServiceRegistry([]); + const registry = ServiceRegistry.create([]); await expect(registry.get(ref, 'catalog')).rejects.toThrow( "Failed to instantiate service '1' because the default factory loader threw an error, Error: default factory error", diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index b5b55f67cf..ad52e645bb 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -20,11 +20,11 @@ import { coreServices, createServiceFactory, } from '@backstage/backend-plugin-api'; -import { stringifyError } from '@backstage/errors'; -import { EnumerableServiceHolder } from './types'; +import { ConflictError, stringifyError } from '@backstage/errors'; // Direct internal import to avoid duplication // eslint-disable-next-line @backstage/no-forbidden-package-imports import { InternalServiceFactory } from '@backstage/backend-plugin-api/src/services/system/types'; +import { DependencyGraph } from '../lib/DependencyGraph'; /** * Keep in sync with `@backstage/backend-plugin-api/src/services/system/types.ts` * @internal @@ -39,7 +39,7 @@ function toInternalServiceFactory( factory: ServiceFactory, ): InternalServiceFactory { const f = factory as InternalServiceFactory; - if (f.$$type !== '@backstage/ServiceFactory') { + if (f.$$type !== '@backstage/BackendFeature') { throw new Error(`Invalid service factory, bad type '${f.$$type}'`); } if (f.version !== 'v1') { @@ -49,14 +49,20 @@ function toInternalServiceFactory( } const pluginMetadataServiceFactory = createServiceFactory( - (options: { pluginId: string }) => ({ + (options?: { pluginId: string }) => ({ service: coreServices.pluginMetadata, deps: {}, - factory: async () => ({ getId: () => options.pluginId }), + factory: async () => ({ getId: () => options?.pluginId! }), }), ); -export class ServiceRegistry implements EnumerableServiceHolder { +export class ServiceRegistry { + static create(factories: Array): ServiceRegistry { + const registry = new ServiceRegistry(factories); + registry.checkForCircularDeps(); + return registry; + } + readonly #providedFactories: Map; readonly #loadedDefaultFactories: Map< Function, @@ -73,8 +79,10 @@ export class ServiceRegistry implements EnumerableServiceHolder { InternalServiceFactory, Promise >(); + readonly #addedFactoryIds = new Set(); + readonly #instantiatedFactories = new Set(); - constructor(factories: Array) { + private constructor(factories: Array) { this.#providedFactories = new Map( factories.map(sf => [sf.service.id, toInternalServiceFactory(sf)]), ); @@ -146,11 +154,58 @@ export class ServiceRegistry implements EnumerableServiceHolder { } } + 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), + }), + ), + ); + const circularDependencies = Array.from(graph.detectCircularDependencies()); + + if (circularDependencies.length) { + const cycles = circularDependencies + .map(c => c.map(id => `'${id}'`).join(' -> ')) + .join('\n '); + + throw new ConflictError(`Circular dependencies detected:\n ${cycles}`); + } + } + + add(factory: ServiceFactory) { + const factoryId = factory.service.id; + if (factoryId === coreServices.pluginMetadata.id) { + throw new Error( + `The ${coreServices.pluginMetadata.id} service cannot be overridden`, + ); + } + + 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)); + } + getServiceRefs(): ServiceRef[] { return Array.from(this.#providedFactories.values()).map(f => f.service); } 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); diff --git a/packages/backend-app-api/src/wiring/createSpecializedBackend.test.ts b/packages/backend-app-api/src/wiring/createSpecializedBackend.test.ts index dc9a0cc085..1fd7fcbbec 100644 --- a/packages/backend-app-api/src/wiring/createSpecializedBackend.test.ts +++ b/packages/backend-app-api/src/wiring/createSpecializedBackend.test.ts @@ -22,13 +22,15 @@ import { createSpecializedBackend } from './createSpecializedBackend'; describe('createSpecializedBackend', () => { it('should create a backend without services', () => { - expect(() => createSpecializedBackend({ services: [] })).not.toThrow(); + expect(() => + createSpecializedBackend({ defaultServiceFactories: [] }), + ).not.toThrow(); }); it('should throw on duplicate service implementations', () => { expect(() => createSpecializedBackend({ - services: [ + defaultServiceFactories: [ createServiceFactory({ service: coreServices.rootLifecycle, deps: {}, @@ -55,7 +57,7 @@ describe('createSpecializedBackend', () => { it('should throw when providing a plugin metadata service implementation', () => { expect(() => createSpecializedBackend({ - services: [ + defaultServiceFactories: [ createServiceFactory({ service: coreServices.pluginMetadata, deps: {}, diff --git a/packages/backend-app-api/src/wiring/createSpecializedBackend.ts b/packages/backend-app-api/src/wiring/createSpecializedBackend.ts index 15e51f7c4c..f5c4d8b152 100644 --- a/packages/backend-app-api/src/wiring/createSpecializedBackend.ts +++ b/packages/backend-app-api/src/wiring/createSpecializedBackend.ts @@ -24,7 +24,7 @@ import { Backend, CreateSpecializedBackendOptions } from './types'; export function createSpecializedBackend( options: CreateSpecializedBackendOptions, ): Backend { - const services = options.services.map(sf => + const services = options.defaultServiceFactories.map(sf => typeof sf === 'function' ? sf() : sf, ); diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index ffedb40324..027b2bf254 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -25,7 +25,12 @@ import { * @public */ export interface Backend { - add(feature: BackendFeature): void; + add( + feature: + | BackendFeature + | (() => BackendFeature) + | Promise<{ default: BackendFeature | (() => BackendFeature) }>, + ): void; start(): Promise; stop(): Promise; } @@ -34,18 +39,7 @@ export interface Backend { * @public */ export interface CreateSpecializedBackendOptions { - services: ServiceFactoryOrFunction[]; -} - -export interface ServiceHolder { - get(api: ServiceRef, pluginId: string): Promise | undefined; -} - -/** - * @internal - */ -export interface EnumerableServiceHolder extends ServiceHolder { - getServiceRefs(): ServiceRef[]; + defaultServiceFactories: ServiceFactoryOrFunction[]; } /** diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index f0c6d75478..2d4c3cd4d9 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,88 @@ # @backstage/backend-common +## 0.19.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.5.3-next.2 + - @backstage/config-loader@1.5.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/integration@1.7.0-next.2 + - @backstage/integration-aws-node@0.1.6-next.1 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/backend-dev-utils@0.1.1 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.19.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-app-api@0.5.3-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/config-loader@1.5.0-next.1 + - @backstage/integration-aws-node@0.1.6-next.0 + - @backstage/backend-dev-utils@0.1.1 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.19.4-next.0 + +### Patch Changes + +- 5f1a92b9f19f: Use `DefaultAzureDevOpsCredentialsProvider` to retrieve credentials for Azure DevOps. +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/backend-app-api@0.5.2-next.0 + - @backstage/backend-dev-utils@0.1.1 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/integration-aws-node@0.1.5 + - @backstage/types@1.1.0 + +## 0.19.2 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 443afcf7f567: To improve performance, `GerritUrlReader.readTree()` now uses Gitiles to fetch an archive instead of cloning the repository. + If `gitilesBaseUrl` is not configured, `readTree` still uses Git to clone the repository. + + Added `stripFirstDirectory` option to `ReadTreeResponseFactory.fromTarArchive()`, allowing to disable stripping first directory + for `tar` archives. + +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-app-api@0.5.0 + - @backstage/config-loader@1.4.0 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/integration@1.6.0 + - @backstage/integration-aws-node@0.1.5 + - @backstage/backend-dev-utils@0.1.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.19.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.5.0-next.2 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/config-loader@1.4.0-next.1 + ## 0.19.2-next.1 ### Patch Changes diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index fae3e2117d..f0fa15efb0 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -9,6 +9,7 @@ import { AppConfig } from '@backstage/config'; import { AwsCredentialsManager } from '@backstage/integration-aws-node'; import { AwsS3Integration } from '@backstage/integration'; +import { AzureDevOpsCredentialsProvider } from '@backstage/integration'; import { AzureIntegration } from '@backstage/integration'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { BitbucketCloudIntegration } from '@backstage/integration'; @@ -94,6 +95,7 @@ export class AzureUrlReader implements UrlReader { integration: AzureIntegration, deps: { treeResponseFactory: ReadTreeResponseFactory; + credentialsProvider: AzureDevOpsCredentialsProvider; }, ); // (undocumented) @@ -610,7 +612,9 @@ export interface ReadTreeResponseFactory { ): Promise; // (undocumented) fromTarArchive( - options: ReadTreeResponseFactoryOptions, + options: ReadTreeResponseFactoryOptions & { + stripFirstDirectory?: boolean; + }, ): Promise; // (undocumented) fromZipArchive( diff --git a/packages/backend-common/catalog-info.yaml b/packages/backend-common/catalog-info.yaml new file mode 100644 index 0000000000..ff38a4df75 --- /dev/null +++ b/packages/backend-common/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-backend-common + title: '@backstage/backend-common' + description: Common functionality library for Backstage backends +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index e18369b7e7..bb9427da51 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.19.2-next.1", + "version": "0.19.5-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -92,6 +92,7 @@ "minimatch": "^5.0.0", "minimist": "^1.2.5", "morgan": "^1.10.0", + "mysql2": "^2.2.5", "node-fetch": "^2.6.7", "node-forge": "^1.3.1", "pg": "^8.3.0", diff --git a/packages/backend-common/src/database/DatabaseManager.test.ts b/packages/backend-common/src/database/DatabaseManager.test.ts index 69386d77c5..bf4af17d05 100644 --- a/packages/backend-common/src/database/DatabaseManager.test.ts +++ b/packages/backend-common/src/database/DatabaseManager.test.ts @@ -425,6 +425,33 @@ describe('DatabaseManager', () => { ); }); + it('generates a database name override when prefix is not explicitly set for mysql', async () => { + const testManager = DatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'mysql', + connection: { + host: 'localhost', + user: 'foo', + password: 'bar', + database: 'foodb', + }, + }, + }, + }), + ); + + await testManager.forPlugin('testplugin').getClient(); + const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); + const [_baseConfig, overrides] = mockCalls[0]; + + expect(overrides).toHaveProperty( + 'connection.database', + expect.stringContaining('backstage_plugin_'), + ); + }); + it('uses values from plugin connection string if top level client should be used', async () => { const pluginId = 'stringoverride'; await manager.forPlugin(pluginId).getClient(); diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index f969b3b7eb..6b2f41a2bf 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -17,7 +17,11 @@ import { ConfigReader } from '@backstage/config'; import { AzureIntegration, + DefaultAzureDevOpsCredentialsProvider, readAzureIntegrationConfig, + ScmIntegrations, + AzureDevOpsCredentialLike, + AzureIntegrationConfig, } from '@backstage/integration'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; @@ -31,12 +35,40 @@ import { getVoidLogger } from '../logging'; import { AzureUrlReader } from './AzureUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; +type AzureIntegrationConfigLike = Partial< + Omit +> & { + credentials?: Partial[]; +}; + const logger = getVoidLogger(); const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), }); +const urlReaderFactory = (azureIntegration: AzureIntegrationConfigLike) => { + const credentialsProvider = + DefaultAzureDevOpsCredentialsProvider.fromIntegrations( + ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + azure: [azureIntegration], + }, + }), + ), + ); + return new AzureUrlReader( + new AzureIntegration( + readAzureIntegrationConfig(new ConfigReader(azureIntegration)), + ), + { + treeResponseFactory, + credentialsProvider, + }, + ); +}; + const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; describe('AzureUrlReader', () => { @@ -68,13 +100,29 @@ describe('AzureUrlReader', () => { ); }); - const createConfig = (token?: string) => - new ConfigReader( + const createConfig = (token?: string) => { + let credentials: AzureDevOpsCredentialLike[] | undefined = undefined; + if (token !== undefined) { + credentials = [ + { + personalAccessToken: token, + }, + ]; + } + return new ConfigReader( { - integrations: { azure: [{ host: 'dev.azure.com', token }] }, + integrations: { + azure: [ + { + host: 'dev.azure.com', + credentials: credentials, + }, + ], + }, }, 'test-config', ); + }; it.each([ { @@ -104,8 +152,8 @@ describe('AzureUrlReader', () => { url: 'https://dev.azure.com/a/b/_git/repo-name?path=my-template.yaml', config: createConfig(undefined), response: expect.objectContaining({ - headers: expect.not.objectContaining({ - authorization: expect.anything(), + headers: expect.objectContaining({ + authorization: expect.stringMatching(/^Bearer /), }), }), }, @@ -137,7 +185,7 @@ describe('AzureUrlReader', () => { url: '', config: createConfig(''), error: - "Invalid type in config for key 'integrations.azure[0].token' in 'test-config', got empty-string, wanted string", + "Invalid type in config for key 'integrations.azure[0].credentials[0].personalAccessToken' in 'test-config', got empty-string, wanted string", }, ])('should handle error path %#', async ({ url, config, error }) => { await expect(async () => { @@ -156,16 +204,14 @@ describe('AzureUrlReader', () => { path.resolve(__dirname, '__fixtures__/mock-main.zip'), ); - const processor = new AzureUrlReader( - new AzureIntegration( - readAzureIntegrationConfig( - new ConfigReader({ - host: 'dev.azure.com', - }), - ), - ), - { treeResponseFactory }, - ); + const processor = urlReaderFactory({ + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 'my-pat', + }, + ], + }); beforeEach(() => { worker.use( @@ -268,16 +314,14 @@ describe('AzureUrlReader', () => { path.resolve(__dirname, '__fixtures__/mock-main.zip'), ); - const processor = new AzureUrlReader( - new AzureIntegration( - readAzureIntegrationConfig( - new ConfigReader({ - host: 'dev.azure.com', - }), - ), - ), - { treeResponseFactory }, - ); + const processor = urlReaderFactory({ + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 'my-pat', + }, + ], + }); beforeEach(() => { worker.use( diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts index 94abf3a178..783736b639 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.ts @@ -15,12 +15,13 @@ */ import { - AzureIntegration, getAzureCommitsUrl, getAzureDownloadUrl, getAzureFileFetchUrl, - getAzureRequestOptions, + AzureDevOpsCredentialsProvider, + DefaultAzureDevOpsCredentialsProvider, ScmIntegrations, + AzureIntegration, } from '@backstage/integration'; import fetch, { Response } from 'node-fetch'; import { Minimatch } from 'minimatch'; @@ -47,8 +48,13 @@ import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; export class AzureUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, treeResponseFactory }) => { const integrations = ScmIntegrations.fromConfig(config); + const credentialProvider = + DefaultAzureDevOpsCredentialsProvider.fromIntegrations(integrations); return integrations.azure.list().map(integration => { - const reader = new AzureUrlReader(integration, { treeResponseFactory }); + const reader = new AzureUrlReader(integration, { + treeResponseFactory, + credentialsProvider: credentialProvider, + }); const predicate = (url: URL) => url.host === integration.config.host; return { reader, predicate }; }); @@ -56,7 +62,10 @@ export class AzureUrlReader implements UrlReader { constructor( private readonly integration: AzureIntegration, - private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, + private readonly deps: { + treeResponseFactory: ReadTreeResponseFactory; + credentialsProvider: AzureDevOpsCredentialsProvider; + }, ) {} async read(url: string): Promise { @@ -72,11 +81,13 @@ export class AzureUrlReader implements UrlReader { const { signal } = options ?? {}; const builtUrl = getAzureFileFetchUrl(url); - let response: Response; try { + const credentials = await this.deps.credentialsProvider.getCredentials({ + url: builtUrl, + }); response = await fetch(builtUrl, { - ...(await getAzureRequestOptions(this.integration.config)), + headers: credentials?.headers, // 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 @@ -111,10 +122,13 @@ export class AzureUrlReader implements UrlReader { // Get latest commit SHA - const commitsAzureResponse = await fetch( - getAzureCommitsUrl(url), - await getAzureRequestOptions(this.integration.config), - ); + const credentials = await this.deps.credentialsProvider.getCredentials({ + url: url, + }); + + const commitsAzureResponse = await fetch(getAzureCommitsUrl(url), { + headers: credentials?.headers, + }); if (!commitsAzureResponse.ok) { const message = `Failed to read tree from ${url}, ${commitsAzureResponse.status} ${commitsAzureResponse.statusText}`; if (commitsAzureResponse.status === 404) { @@ -129,9 +143,10 @@ export class AzureUrlReader implements UrlReader { } const archiveAzureResponse = await fetch(getAzureDownloadUrl(url), { - ...(await getAzureRequestOptions(this.integration.config, { + headers: { + ...credentials?.headers, Accept: 'application/zip', - })), + }, // 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 be @@ -198,7 +213,9 @@ export class AzureUrlReader implements UrlReader { } toString() { - const { host, token } = this.integration.config; - return `azure{host=${host},authed=${Boolean(token)}}`; + const { host, credentials } = this.integration.config; + return `azure{host=${host},authed=${Boolean( + credentials !== undefined && credentials.length > 0, + )}}`; } } diff --git a/packages/backend-common/src/reading/GerritUrlReader.test.ts b/packages/backend-common/src/reading/GerritUrlReader.test.ts index 4e0b97fc58..9de9aac33d 100644 --- a/packages/backend-common/src/reading/GerritUrlReader.test.ts +++ b/packages/backend-common/src/reading/GerritUrlReader.test.ts @@ -37,14 +37,16 @@ const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), }); +const cloneMock = jest.fn(() => Promise.resolve()); jest.mock('../scm', () => ({ Git: { fromAuth: () => ({ - clone: jest.fn(() => Promise.resolve({})), + clone: cloneMock, }), }, })); +// Gerrit processor without a gitilesBaseUrl configured const gerritProcessor = new GerritUrlReader( new GerritIntegration( readGerritIntegrationConfig( @@ -57,6 +59,21 @@ const gerritProcessor = new GerritUrlReader( '/tmp', ); +// Gerrit processor with a gitilesBaseUrl configured. +// Use to test readTree with Gitiles archive download. +const gerritProcessorWithGitiles = new GerritUrlReader( + new GerritIntegration( + readGerritIntegrationConfig( + new ConfigReader({ + host: 'gerrit.com', + gitilesBaseUrl: 'https://gerrit.com/gitiles', + }), + ), + ), + { treeResponseFactory }, + '/tmp', +); + const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => { return GerritUrlReader.factory({ config: new ConfigReader(config), @@ -217,9 +234,17 @@ describe('GerritUrlReader', () => { path.resolve(__dirname, '__fixtures__/gerrit/branch-info-response.txt'), ); const treeUrl = 'https://gerrit.com/app/web/+/refs/heads/master/'; + const treeUrlGitiles = + 'https://gerrit.com/gitiles/app/web/+/refs/heads/master/'; const etag = '52432507a70b677b5674b019c9a46b2e9f29d0a1'; - const mkdocsContent = 'great content'; + const mkdocsContent = 'a repo fetched using git clone'; const mdContent = 'doc'; + const repoArchiveBuffer = fs.readFileSync( + path.resolve(__dirname, '__fixtures__/gerrit/gerrit-master.tar.gz'), + ); + const repoArchiveDocsBuffer = fs.readFileSync( + path.resolve(__dirname, '__fixtures__/gerrit/gerrit-master-docs.tar.gz'), + ); beforeEach(() => { mockFs({ @@ -229,6 +254,35 @@ describe('GerritUrlReader', () => { }); const spy = jest.spyOn(fs, 'mkdtemp'); spy.mockImplementation(() => '/tmp/gerrit-clone-123abc'); + + worker.use( + rest.get( + 'https://gerrit.com/gitiles/app/web/\\+archive/refs/heads/master.tar.gz', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/x-gzip'), + ctx.set( + 'content-disposition', + 'attachment; filename=web-refs/heads/master.tar.gz', + ), + ctx.body(repoArchiveBuffer), + ), + ), + rest.get( + 'https://gerrit.com/gitiles/app/web/\\+archive/refs/heads/master/docs.tar.gz', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/x-gzip'), + ctx.set( + 'content-disposition', + 'attachment; filename=web-refs/heads/master-docs.tar.gz', + ), + ctx.body(repoArchiveDocsBuffer), + ), + ), + ); }); afterEach(() => { @@ -236,7 +290,32 @@ describe('GerritUrlReader', () => { jest.clearAllMocks(); }); - it('reads the wanted files correctly.', async () => { + it('reads the wanted files correctly using gitiles.', async () => { + worker.use( + rest.get(branchAPIUrl, (_, res, ctx) => { + return res(ctx.status(200), ctx.body(branchAPIresponse)); + }), + ); + + const response = await gerritProcessorWithGitiles.readTree( + treeUrlGitiles, + ); + + expect(response.etag).toBe(etag); + + const files = await response.files(); + expect(files.length).toBe(2); + + const docsYaml = await files[0].content(); + expect(docsYaml.toString()).toBe('# Test\n'); + + const mdFile = await files[1].content(); + expect(mdFile.toString()).toBe('site_name: Test\n'); + + expect(cloneMock).not.toHaveBeenCalled(); + }); + + it('reads the wanted files correctly using git clone.', async () => { worker.use( rest.get(branchAPIUrl, (_, res, ctx) => { return res(ctx.status(200), ctx.body(branchAPIresponse)); @@ -255,6 +334,8 @@ describe('GerritUrlReader', () => { const mdFile = await files[1].content(); expect(mdFile.toString()).toBe(mdContent); + + expect(cloneMock).toHaveBeenCalled(); }); it('throws NotModifiedError for matching etags.', async () => { @@ -291,7 +372,29 @@ describe('GerritUrlReader', () => { await expect(gerritProcessor.readTree(treeUrl)).rejects.toThrow(Error); }); - it('should returns wanted files with a subpath', async () => { + it('should returns wanted files with a subpath using gitiles', async () => { + worker.use( + rest.get(branchAPIUrl, (_, res, ctx) => { + return res(ctx.status(200), ctx.body(branchAPIresponse)); + }), + ); + + const response = await gerritProcessorWithGitiles.readTree( + `${treeUrlGitiles}/docs`, + ); + + expect(response.etag).toBe(etag); + + const files = await response.files(); + expect(files.length).toBe(1); + + const mdFile = await files[0].content(); + expect(mdFile.toString()).toBe('# Test\n'); + + expect(cloneMock).not.toHaveBeenCalled(); + }); + + it('should returns wanted files with a subpath using git clone', async () => { worker.use( rest.get(branchAPIUrl, (_, res, ctx) => { return res(ctx.status(200), ctx.body(branchAPIresponse)); @@ -307,6 +410,8 @@ describe('GerritUrlReader', () => { const mdFile = await files[0].content(); expect(mdFile.toString()).toBe(mdContent); + + expect(cloneMock).toHaveBeenCalled(); }); }); }); diff --git a/packages/backend-common/src/reading/GerritUrlReader.ts b/packages/backend-common/src/reading/GerritUrlReader.ts index 8ac5452a4c..f4a5fdb9f5 100644 --- a/packages/backend-common/src/reading/GerritUrlReader.ts +++ b/packages/backend-common/src/reading/GerritUrlReader.ts @@ -14,16 +14,17 @@ * limitations under the License. */ -import { Git } from '../scm'; import { NotFoundError, NotModifiedError } from '@backstage/errors'; import { GerritIntegration, - getGerritCloneRepoUrl, + ScmIntegrations, + buildGerritGitilesArchiveUrl, getGerritBranchApiUrl, + getGerritCloneRepoUrl, getGerritFileContentsApiUrl, getGerritRequestOptions, - parseGerritJsonResponse, parseGerritGitilesUrl, + parseGerritJsonResponse, } from '@backstage/integration'; import { Base64Decode } from 'base64-stream'; import concatStream from 'concat-stream'; @@ -31,20 +32,20 @@ import fs from 'fs-extra'; import fetch, { Response } from 'node-fetch'; import os from 'os'; import { join as joinPath } from 'path'; +import { Readable, pipeline as pipelineCb } from 'stream'; import tar from 'tar'; -import { pipeline as pipelineCb, Readable } from 'stream'; import { promisify } from 'util'; +import { Git } from '../scm'; import { - ReaderFactory, ReadTreeOptions, ReadTreeResponse, ReadTreeResponseFactory, ReadUrlOptions, ReadUrlResponse, + ReaderFactory, SearchResponse, UrlReader, } from './types'; -import { ScmIntegrations } from '@backstage/integration'; const pipeline = promisify(pipelineCb); @@ -59,6 +60,8 @@ const createTemporaryDirectory = async (workDir: string): Promise => * way we are depending on that there is a Gitiles installation somewhere * that we can link to. It is perfectly possible to integrate Gerrit with * Backstage without Gitiles since all API calls goes directly to Gerrit. + * However if Gitiles is configured, readTree will use it to fetch + * an archive instead of cloning the repository. * * The "host" variable in the config is the Gerrit host. The address where * Gitiles is installed may be on the same host but it could be on a @@ -125,6 +128,7 @@ export class GerritUrlReader implements UrlReader { } catch (e) { throw new Error(`Unable to read gerrit file ${url}, ${e}`); } + if (response.ok) { let responseBody: string; return { @@ -152,7 +156,6 @@ export class GerritUrlReader implements UrlReader { url: string, options?: ReadTreeOptions, ): Promise { - const { filePath } = parseGerritGitilesUrl(this.integration.config, url); const apiUrl = getGerritBranchApiUrl(this.integration.config, url); let response: Response; try { @@ -180,6 +183,30 @@ export class GerritUrlReader implements UrlReader { throw new NotModifiedError(); } + if ( + this.integration.config.gitilesBaseUrl !== this.integration.config.baseUrl + ) { + return this.readTreeFromGitiles(url, branchInfo.revision, options); + } + return this.readTreeFromGitClone(url, branchInfo.revision, options); + } + + async search(): Promise { + throw new Error('GerritReader does not implement search'); + } + + toString() { + const { host, password } = this.integration.config; + return `gerrit{host=${host},authed=${Boolean(password)}}`; + } + + private async readTreeFromGitClone( + url: string, + revision: string, + options?: ReadTreeOptions, + ) { + const { filePath } = parseGerritGitilesUrl(this.integration.config, url); + const git = Git.fromAuth({ username: this.integration.config.username, password: this.integration.config.password, @@ -192,7 +219,7 @@ export class GerritUrlReader implements UrlReader { await git.clone({ url: cloneUrl, dir: joinPath(tempDir, 'repo'), - ref: branchInfo.revision, + ref: revision, depth: 1, }); @@ -206,7 +233,7 @@ export class GerritUrlReader implements UrlReader { return await this.deps.treeResponseFactory.fromTarArchive({ stream: tarArchive, subpath: filePath === '/' ? undefined : filePath, - etag: branchInfo.revision, + etag: revision, filter: options?.filter, }); } catch (error) { @@ -216,12 +243,47 @@ export class GerritUrlReader implements UrlReader { } } - async search(): Promise { - throw new Error('GerritReader does not implement search'); - } + private async readTreeFromGitiles( + url: string, + revision: string, + options?: ReadTreeOptions, + ) { + const { branch, filePath, project } = parseGerritGitilesUrl( + this.integration.config, + url, + ); + const archiveUrl = buildGerritGitilesArchiveUrl( + this.integration.config, + project, + branch, + filePath, + ); + const archiveResponse = await fetch(archiveUrl, { + ...getGerritRequestOptions(this.integration.config), + // 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 + // be removed after we support ESM for CLI dependencies and migrate to + // version 3 of node-fetch. + // https://github.com/backstage/backstage/issues/8242 + signal: options?.signal as any, + }); - toString() { - const { host, password } = this.integration.config; - return `gerrit{host=${host},authed=${Boolean(password)}}`; + if (archiveResponse.status === 404) { + throw new NotFoundError(`Not found: ${archiveUrl}`); + } + + if (!archiveResponse.ok) { + throw new Error( + `${url} could not be read as ${archiveUrl}, ${archiveResponse.status} ${archiveResponse.statusText}`, + ); + } + + return await this.deps.treeResponseFactory.fromTarArchive({ + stream: archiveResponse.body as unknown as Readable, + etag: revision, + filter: options?.filter, + stripFirstDirectory: false, + }); } } diff --git a/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master-docs.tar.gz b/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master-docs.tar.gz new file mode 100644 index 0000000000..425cf0876b Binary files /dev/null and b/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master-docs.tar.gz differ diff --git a/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master.tar.gz b/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master.tar.gz new file mode 100644 index 0000000000..8a27502734 Binary files /dev/null and b/packages/backend-common/src/reading/__fixtures__/gerrit/gerrit-master.tar.gz differ diff --git a/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts b/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts index 11a5ef168a..90c58c0efa 100644 --- a/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts +++ b/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts @@ -37,7 +37,9 @@ export class DefaultReadTreeResponseFactory implements ReadTreeResponseFactory { constructor(private readonly workDir: string) {} async fromTarArchive( - options: ReadTreeResponseFactoryOptions, + options: ReadTreeResponseFactoryOptions & { + stripFirstDirectory?: boolean; + }, ): Promise { return new TarArchiveResponse( options.stream, @@ -45,6 +47,7 @@ export class DefaultReadTreeResponseFactory implements ReadTreeResponseFactory { this.workDir, options.etag, options.filter, + options.stripFirstDirectory ?? true, ); } diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts index e1529c196a..294ec22609 100644 --- a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts @@ -44,6 +44,7 @@ export class TarArchiveResponse implements ReadTreeResponse { private readonly workDir: string, public readonly etag: string, private readonly filter?: (path: string, info: { size: number }) => boolean, + private readonly stripFirstDirectory: boolean = true, ) { if (subPath) { if (!subPath.endsWith('/')) { @@ -81,7 +82,9 @@ export class TarArchiveResponse implements ReadTreeResponse { // File path relative to the root extracted directory. Will remove the // top level dir name from the path since its name is hard to predetermine. - const relativePath = stripFirstDirectoryFromPath(entry.path); + const relativePath = this.stripFirstDirectory + ? stripFirstDirectoryFromPath(entry.path) + : entry.path; if (this.subPath) { if (!relativePath.startsWith(this.subPath)) { @@ -148,7 +151,10 @@ export class TarArchiveResponse implements ReadTreeResponse { // Equivalent of tar --strip-components=N // When no subPath is given, remove just 1 top level directory - const strip = this.subPath ? this.subPath.split('/').length : 1; + let strip = this.subPath ? this.subPath.split('/').length : 1; + if (!this.stripFirstDirectory) { + strip--; + } let filterError: Error | undefined = undefined; await pipeline( @@ -164,7 +170,9 @@ export class TarArchiveResponse implements ReadTreeResponse { // File path relative to the root extracted directory. Will remove the // top level dir name from the path since its name is hard to predetermine. - const relativePath = stripFirstDirectoryFromPath(path); + const relativePath = this.stripFirstDirectory + ? stripFirstDirectoryFromPath(path) + : path; if (this.subPath && !relativePath.startsWith(this.subPath)) { return false; } diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts index 7f4d407fd0..90a3c6287e 100644 --- a/packages/backend-common/src/reading/types.ts +++ b/packages/backend-common/src/reading/types.ts @@ -116,7 +116,13 @@ export type FromReadableArrayOptions = Array<{ */ export interface ReadTreeResponseFactory { fromTarArchive( - options: ReadTreeResponseFactoryOptions, + options: ReadTreeResponseFactoryOptions & { + /** + * Strip the first parent directory of a tar archive. + * Defaults to true. + */ + stripFirstDirectory?: boolean; + }, ): Promise; fromZipArchive( options: ReadTreeResponseFactoryOptions, diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index 089f80d745..3176a34528 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/backend-defaults +## 0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.5.3-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + +## 0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.5.3-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + +## 0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-app-api@0.5.2-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + +## 0.2.0 + +### Minor Changes + +- d008aefef808: **BREAKING**: Removing shared environments concept from the new experimental backend system. +- a6d7983f349c: **BREAKING**: Removed the `services` option from `createBackend`. Service factories are now `BackendFeature`s and should be installed with `backend.add(...)` instead. The following should be migrated: + + ```ts + const backend = createBackend({ services: [myCustomServiceFactory] }); + ``` + + To instead pass the service factory via `backend.add(...)`: + + ```ts + const backend = createBackend(); + backend.add(customRootLoggerServiceFactory); + ``` + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-app-api@0.5.0 + - @backstage/backend-plugin-api@0.6.0 + +## 0.2.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.5.0-next.2 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + ## 0.2.0-next.1 ### Minor Changes diff --git a/packages/backend-defaults/api-report.md b/packages/backend-defaults/api-report.md index 6cc58de663..ab9cc5775c 100644 --- a/packages/backend-defaults/api-report.md +++ b/packages/backend-defaults/api-report.md @@ -4,14 +4,7 @@ ```ts import { Backend } from '@backstage/backend-app-api'; -import { ServiceFactoryOrFunction } from '@backstage/backend-plugin-api'; // @public (undocumented) -export function createBackend(options?: CreateBackendOptions): Backend; - -// @public (undocumented) -export interface CreateBackendOptions { - // (undocumented) - services?: ServiceFactoryOrFunction[]; -} +export function createBackend(): Backend; ``` diff --git a/packages/backend-defaults/catalog-info.yaml b/packages/backend-defaults/catalog-info.yaml new file mode 100644 index 0000000000..d3d00cc3d2 --- /dev/null +++ b/packages/backend-defaults/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-backend-defaults + title: '@backstage/backend-defaults' + description: Backend defaults used by Backstage backend apps +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 5a89bb51f0..bdbbd26d3b 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-defaults", "description": "Backend defaults used by Backstage backend apps", - "version": "0.2.0-next.1", + "version": "0.2.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-defaults/src/CreateBackend.test.ts b/packages/backend-defaults/src/CreateBackend.test.ts index de04d4e350..23e5c571bd 100644 --- a/packages/backend-defaults/src/CreateBackend.test.ts +++ b/packages/backend-defaults/src/CreateBackend.test.ts @@ -18,64 +18,59 @@ import { coreServices, createServiceFactory, } from '@backstage/backend-plugin-api'; +import { mockServices } from '@backstage/backend-test-utils'; import { createBackend } from './CreateBackend'; describe('createBackend', () => { - it('should not throw when overriding a default service implementation', () => { - expect(() => - createBackend({ - services: [ - createServiceFactory({ - service: coreServices.rootLifecycle, - deps: {}, - factory: async () => ({ - addStartupHook: () => {}, - addShutdownHook: () => {}, - }), - }), - ], - }), - ).not.toThrow(); + it('should not throw when overriding a default service implementation', async () => { + const backend = createBackend(); + + backend.add(mockServices.rootConfig.factory()); + + await expect(backend.start()).resolves.toBe(undefined); }); - it('should throw on duplicate service implementations', () => { - expect(() => - createBackend({ - services: [ - createServiceFactory({ - service: coreServices.rootLifecycle, - deps: {}, - factory: async () => ({ - addStartupHook: () => {}, - addShutdownHook: () => {}, - }), - }), - createServiceFactory({ - service: coreServices.rootLifecycle, - deps: {}, - factory: async () => ({ - addStartupHook: () => {}, - addShutdownHook: () => {}, - }), - }), - ], + it('should throw on duplicate service implementations', async () => { + const backend = createBackend(); + + backend.add( + createServiceFactory({ + service: coreServices.rootLifecycle, + deps: {}, + factory: async () => ({ + addStartupHook: () => {}, + addShutdownHook: () => {}, + }), }), - ).toThrow( + ); + backend.add( + createServiceFactory({ + service: coreServices.rootLifecycle, + deps: {}, + factory: async () => ({ + addStartupHook: () => {}, + addShutdownHook: () => {}, + }), + }), + ); + + await expect(backend.start()).rejects.toThrow( 'Duplicate service implementations provided for core.rootLifecycle', ); }); - it('should throw when providing a plugin metadata service implementation', () => { - expect(() => - createBackend({ - services: [ - createServiceFactory({ - service: coreServices.pluginMetadata, - deps: {}, - factory: async () => ({ getId: () => 'test' }), - }), - ], + it('should throw when providing a plugin metadata service implementation', async () => { + const backend = createBackend(); + backend.add( + createServiceFactory({ + service: coreServices.pluginMetadata, + deps: {}, + factory: () => ({ getId: () => 'test' }), }), - ).toThrow('The core.pluginMetadata service cannot be overridden'); + ); + + await expect(backend.start()).rejects.toThrow( + 'The core.pluginMetadata service cannot be overridden', + ); }); }); diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index 3db013a290..c07d1cc38a 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -33,10 +33,6 @@ import { urlReaderServiceFactory, identityServiceFactory, } from '@backstage/backend-app-api'; -import { - ServiceFactory, - ServiceFactoryOrFunction, -} from '@backstage/backend-plugin-api'; export const defaultServiceFactories = [ cacheServiceFactory(), @@ -59,27 +55,6 @@ export const defaultServiceFactories = [ /** * @public */ -export interface CreateBackendOptions { - services?: ServiceFactoryOrFunction[]; -} - -/** - * @public - */ -export function createBackend(options?: CreateBackendOptions): Backend { - const services = new Array(); - - // Highest priority: Services passed directly to createBackend - const providedServices = (options?.services ?? []).map(sf => - typeof sf === 'function' ? sf() : sf, - ); - services.push(...providedServices); - - // Lowest priority: Default services that are not already provided by environment or directly to createBackend - const defaultServices = defaultServiceFactories.filter( - sf => !services.some(({ service }) => service.id === sf.service.id), - ); - services.push(...defaultServices); - - return createSpecializedBackend({ services }); +export function createBackend(): Backend { + return createSpecializedBackend({ defaultServiceFactories }); } diff --git a/packages/backend-defaults/src/index.ts b/packages/backend-defaults/src/index.ts index 833878744e..d3e36cf0fb 100644 --- a/packages/backend-defaults/src/index.ts +++ b/packages/backend-defaults/src/index.ts @@ -25,5 +25,4 @@ // TODO(Rugvip): Remove this once backend-common is no longer used by backend-app-api import '@backstage/backend-common'; -export type { CreateBackendOptions } from './CreateBackend'; export { createBackend } from './CreateBackend'; diff --git a/packages/backend-dev-utils/catalog-info.yaml b/packages/backend-dev-utils/catalog-info.yaml new file mode 100644 index 0000000000..ecbcb4e0bb --- /dev/null +++ b/packages/backend-dev-utils/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-backend-dev-utils + title: '@backstage/backend-dev-utils' +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/packages/backend-next/.snyk b/packages/backend-next/.snyk new file mode 100644 index 0000000000..ea5abd653d --- /dev/null +++ b/packages/backend-next/.snyk @@ -0,0 +1,10 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.22.1 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + SNYK-JS-ISOLATEDVM-3037320: + - '*': + reason: We do not pass any V8 cache data, and are therefore unaffected by this vulnerability + expires: 2033-07-02T16:55:57.077Z + created: 2023-07-02T16:55:57.077Z +patch: {} diff --git a/packages/backend-next/CHANGELOG.md b/packages/backend-next/CHANGELOG.md index a5eb6fe466..a7b2fb8b33 100644 --- a/packages/backend-next/CHANGELOG.md +++ b/packages/backend-next/CHANGELOG.md @@ -1,5 +1,170 @@ # example-backend-next +## 0.0.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.16.6-next.2 + - @backstage/plugin-permission-backend@0.5.26-next.2 + - @backstage/plugin-catalog-backend@1.13.0-next.2 + - @backstage/plugin-badges-backend@0.2.6-next.2 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-defaults@0.2.3-next.2 + - @backstage/plugin-app-backend@0.3.51-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-entity-feedback-backend@0.1.9-next.2 + - @backstage/plugin-kubernetes-backend@0.11.6-next.2 + - @backstage/plugin-linguist-backend@0.4.3-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/plugin-proxy-backend@0.3.3-next.2 + - @backstage/plugin-search-backend@1.4.3-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.7-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.7-next.2 + - @backstage/plugin-search-backend-module-techdocs@0.1.7-next.2 + - @backstage/plugin-techdocs-backend@1.7.0-next.2 + - @backstage/plugin-devtools-backend@0.1.6-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-adr-backend@0.3.9-next.2 + - @backstage/plugin-azure-devops-backend@0.3.30-next.2 + - @backstage/plugin-lighthouse-backend@0.2.7-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/plugin-todo-backend@0.2.3-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.3-next.2 + +## 0.0.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-backend@0.11.6-next.1 + - @backstage/plugin-catalog-backend@1.13.0-next.1 + - @backstage/plugin-devtools-backend@0.1.6-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/plugin-techdocs-backend@1.7.0-next.1 + - @backstage/plugin-scaffolder-backend@1.16.6-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-adr-backend@0.3.9-next.1 + - @backstage/plugin-app-backend@0.3.51-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-azure-devops-backend@0.3.30-next.1 + - @backstage/plugin-badges-backend@0.2.6-next.1 + - @backstage/plugin-entity-feedback-backend@0.1.9-next.1 + - @backstage/plugin-lighthouse-backend@0.2.7-next.1 + - @backstage/plugin-linguist-backend@0.4.3-next.1 + - @backstage/plugin-permission-backend@0.5.26-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/plugin-proxy-backend@0.3.3-next.1 + - @backstage/plugin-search-backend@1.4.3-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.7-next.1 + - @backstage/plugin-search-backend-module-explore@0.1.7-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.7-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.1 + - @backstage/plugin-todo-backend@0.2.3-next.1 + - @backstage/backend-defaults@0.2.3-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.3-next.1 + +## 0.0.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/plugin-scaffolder-backend@1.16.3-next.0 + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/plugin-linguist-backend@0.4.2-next.0 + - @backstage/plugin-entity-feedback-backend@0.1.8-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/plugin-app-backend@0.3.50-next.0 + - @backstage/backend-defaults@0.2.2-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/plugin-adr-backend@0.3.8-next.0 + - @backstage/plugin-azure-devops-backend@0.3.29-next.0 + - @backstage/plugin-badges-backend@0.2.5-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.2-next.0 + - @backstage/plugin-devtools-backend@0.1.5-next.0 + - @backstage/plugin-kubernetes-backend@0.11.5-next.0 + - @backstage/plugin-lighthouse-backend@0.2.6-next.0 + - @backstage/plugin-permission-backend@0.5.25-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-proxy-backend@0.3.2-next.0 + - @backstage/plugin-search-backend@1.4.2-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.6-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.6-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.6-next.0 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-techdocs-backend@1.6.7-next.0 + - @backstage/plugin-todo-backend@0.2.2-next.0 + +## 0.0.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-module-techdocs@0.1.4 + - @backstage/plugin-search-backend-module-catalog@0.1.4 + - @backstage/plugin-search-backend-module-explore@0.1.4 + - @backstage/plugin-azure-devops-backend@0.3.27 + - @backstage/plugin-kubernetes-backend@0.11.3 + - @backstage/plugin-lighthouse-backend@0.2.4 + - @backstage/plugin-permission-backend@0.5.23 + - @backstage/plugin-scaffolder-backend@1.16.0 + - @backstage/backend-defaults@0.2.0 + - @backstage/plugin-devtools-backend@0.1.3 + - @backstage/plugin-techdocs-backend@1.6.5 + - @backstage/plugin-catalog-backend@1.12.0 + - @backstage/plugin-badges-backend@0.2.3 + - @backstage/plugin-search-backend@1.4.0 + - @backstage/plugin-proxy-backend@0.3.0 + - @backstage/plugin-todo-backend@0.2.0 + - @backstage/plugin-app-backend@0.3.48 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0 + - @backstage/plugin-entity-feedback-backend@0.1.6 + - @backstage/plugin-search-backend-node@1.2.4 + - @backstage/plugin-linguist-backend@0.4.0 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/backend-tasks@0.5.5 + - @backstage/plugin-adr-backend@0.3.6 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/plugin-permission-common@0.7.7 + +## 0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-module-techdocs@0.1.4-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.4-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.4-next.2 + - @backstage/plugin-scaffolder-backend@1.15.2-next.2 + - @backstage/plugin-catalog-backend@1.12.0-next.2 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/plugin-proxy-backend@0.3.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/plugin-app-backend@0.3.48-next.2 + - @backstage/plugin-linguist-backend@0.4.0-next.2 + - @backstage/plugin-techdocs-backend@1.6.5-next.2 + - @backstage/backend-defaults@0.2.0-next.2 + - @backstage/plugin-adr-backend@0.3.6-next.2 + - @backstage/plugin-azure-devops-backend@0.3.27-next.2 + - @backstage/plugin-badges-backend@0.2.3-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0-next.2 + - @backstage/plugin-devtools-backend@0.1.3-next.2 + - @backstage/plugin-entity-feedback-backend@0.1.6-next.2 + - @backstage/plugin-kubernetes-backend@0.11.3-next.2 + - @backstage/plugin-lighthouse-backend@0.2.4-next.2 + - @backstage/plugin-permission-backend@0.5.23-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-search-backend@1.4.0-next.2 + - @backstage/plugin-search-backend-node@1.2.4-next.2 + - @backstage/plugin-todo-backend@0.2.0-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + ## 0.0.14-next.1 ### Patch Changes diff --git a/packages/backend-next/catalog-info.yaml b/packages/backend-next/catalog-info.yaml new file mode 100644 index 0000000000..d297365e4b --- /dev/null +++ b/packages/backend-next/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: example-backend-next + title: example-backend-next +spec: + lifecycle: experimental + type: backstage-backend + owner: maintainers diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json index af4f483db0..9b726fb307 100644 --- a/packages/backend-next/package.json +++ b/packages/backend-next/package.json @@ -1,6 +1,6 @@ { "name": "example-backend-next", - "version": "0.0.14-next.1", + "version": "0.0.15-next.2", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -26,6 +26,7 @@ }, "dependencies": { "@backstage/backend-defaults": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", "@backstage/plugin-adr-backend": "workspace:^", "@backstage/plugin-app-backend": "workspace:^", @@ -40,6 +41,7 @@ "@backstage/plugin-lighthouse-backend": "workspace:^", "@backstage/plugin-linguist-backend": "workspace:^", "@backstage/plugin-permission-backend": "workspace:^", + "@backstage/plugin-permission-backend-module-allow-all-policy": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", "@backstage/plugin-permission-node": "workspace:^", "@backstage/plugin-proxy-backend": "workspace:^", diff --git a/packages/backend-next/src/index.ts b/packages/backend-next/src/index.ts index bbb7290c73..cf7734c089 100644 --- a/packages/backend-next/src/index.ts +++ b/packages/backend-next/src/index.ts @@ -15,97 +15,31 @@ */ import { createBackend } from '@backstage/backend-defaults'; -import { appPlugin } from '@backstage/plugin-app-backend/alpha'; -import { catalogPlugin } from '@backstage/plugin-catalog-backend/alpha'; -import { kubernetesPlugin } from '@backstage/plugin-kubernetes-backend/alpha'; -import { - permissionModuleAllowAllPolicy, - permissionPlugin, -} from '@backstage/plugin-permission-backend/alpha'; -import { scaffolderPlugin } from '@backstage/plugin-scaffolder-backend/alpha'; -import { catalogModuleTemplateKind } from '@backstage/plugin-scaffolder-backend/alpha'; -import { searchModuleCatalogCollator } from '@backstage/plugin-search-backend-module-catalog/alpha'; -import { searchModuleExploreCollator } from '@backstage/plugin-search-backend-module-explore/alpha'; -import { searchModuleTechDocsCollator } from '@backstage/plugin-search-backend-module-techdocs/alpha'; -import { searchPlugin } from '@backstage/plugin-search-backend/alpha'; -import { techdocsPlugin } from '@backstage/plugin-techdocs-backend/alpha'; -import { todoPlugin } from '@backstage/plugin-todo-backend'; -import { entityFeedbackPlugin } from '@backstage/plugin-entity-feedback-backend'; -import { catalogModuleUnprocessedEntities } from '@backstage/plugin-catalog-backend-module-unprocessed'; -import { badgesPlugin } from '@backstage/plugin-badges-backend'; -import { azureDevOpsPlugin } from '@backstage/plugin-azure-devops-backend'; -import { linguistPlugin } from '@backstage/plugin-linguist-backend'; -import { devtoolsPlugin } from '@backstage/plugin-devtools-backend'; -import { TaskScheduleDefinition } from '@backstage/backend-tasks'; -import { adrPlugin } from '@backstage/plugin-adr-backend'; -import { lighthousePlugin } from '@backstage/plugin-lighthouse-backend'; -import { proxyPlugin } from '@backstage/plugin-proxy-backend'; const backend = createBackend(); -backend.add(appPlugin()); - -// Badges -backend.add(badgesPlugin()); - -// Azure DevOps -backend.add(azureDevOpsPlugin()); - -// DevTools -backend.add(devtoolsPlugin()); - -// Entity Feedback -backend.add(entityFeedbackPlugin()); - -// Linguist -const linguistSchedule: TaskScheduleDefinition = { - frequency: { minutes: 2 }, - timeout: { minutes: 15 }, - initialDelay: { seconds: 15 }, -}; - +backend.add(import('@backstage/plugin-adr-backend')); +backend.add(import('@backstage/plugin-app-backend/alpha')); +backend.add(import('@backstage/plugin-azure-devops-backend')); +backend.add(import('@backstage/plugin-badges-backend')); +backend.add(import('@backstage/plugin-catalog-backend-module-unprocessed')); +backend.add(import('@backstage/plugin-catalog-backend/alpha')); +backend.add(import('@backstage/plugin-devtools-backend')); +backend.add(import('@backstage/plugin-entity-feedback-backend')); +backend.add(import('@backstage/plugin-kubernetes-backend/alpha')); +backend.add(import('@backstage/plugin-lighthouse-backend')); +backend.add(import('@backstage/plugin-linguist-backend')); backend.add( - linguistPlugin({ - schedule: linguistSchedule, - age: { days: 30 }, - batchSize: 2, - useSourceLocation: false, - }), + import('@backstage/plugin-permission-backend-module-allow-all-policy'), ); - -// Todo -backend.add(todoPlugin()); - -backend.add(adrPlugin()); - -// Techdocs -backend.add(techdocsPlugin()); - -// Catalog -backend.add(catalogPlugin()); -backend.add(catalogModuleTemplateKind()); - -backend.add(scaffolderPlugin()); - -// Search -backend.add(searchPlugin()); -backend.add(searchModuleCatalogCollator()); -backend.add(searchModuleTechDocsCollator()); -backend.add(searchModuleExploreCollator()); - -// Kubernetes -backend.add(kubernetesPlugin()); - -// Lighthouse -backend.add(lighthousePlugin()); - -// Proxy -backend.add(proxyPlugin()); - -// Permissions -backend.add(permissionPlugin()); -backend.add(permissionModuleAllowAllPolicy()); - -backend.add(catalogModuleUnprocessedEntities()); +backend.add(import('@backstage/plugin-permission-backend/alpha')); +backend.add(import('@backstage/plugin-proxy-backend')); +backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); +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-search-backend/alpha')); +backend.add(import('@backstage/plugin-techdocs-backend/alpha')); +backend.add(import('@backstage/plugin-todo-backend')); backend.start(); diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md index 3eb95afe95..fc3e52fa92 100644 --- a/packages/backend-openapi-utils/CHANGELOG.md +++ b/packages/backend-openapi-utils/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/backend-openapi-utils +## 0.0.3 + +### Patch Changes + +- ebeb77586975: Add a new `createRouter` method for generating an `express` router that validates against your spec. Also fixes a bug with the query parameters type resolution. +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/errors@1.2.1 + ## 0.0.3-next.1 ### Patch Changes diff --git a/packages/backend-openapi-utils/catalog-info.yaml b/packages/backend-openapi-utils/catalog-info.yaml new file mode 100644 index 0000000000..ab0a4be883 --- /dev/null +++ b/packages/backend-openapi-utils/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-backend-openapi-utils + title: '@backstage/backend-openapi-utils' + description: OpenAPI typescript support. +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json index 9123d6dd77..102d8c36d6 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.0.3-next.1", + "version": "0.0.3", "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 510797672c..678f05a787 100644 --- a/packages/backend-plugin-api/CHANGELOG.md +++ b/packages/backend-plugin-api/CHANGELOG.md @@ -1,5 +1,80 @@ # @backstage/backend-plugin-api +## 0.6.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/types@1.1.0 + +## 0.6.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/types@1.1.0 + +## 0.6.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + +## 0.6.0 + +### Minor Changes + +- c49785f00cab: **BREAKING**: It is no longer possible to declare options as being required with `createServiceFactory`. +- 629cbd194a87: **BREAKING**: Renamed `coreServices.config` to `coreServices.rootConfig`. +- 51987dbdaf87: **BREAKING**: Removed the ability to define options for plugins and modules. Existing options should be migrated to instead use either static configuration or extension points. +- d008aefef808: **BREAKING**: Removing shared environments concept from the new experimental backend system. + +### Patch Changes + +- c7aa4ff1793c: Allow modules to register extension points. +- cc9256a33bcc: Added new experimental `featureDiscoveryServiceRef`, available as an `/alpha` export. +- a6d7983f349c: **BREAKING**: Removed the `services` option from `createBackend`. Service factories are now `BackendFeature`s and should be installed with `backend.add(...)` instead. The following should be migrated: + + ```ts + const backend = createBackend({ services: [myCustomServiceFactory] }); + ``` + + To instead pass the service factory via `backend.add(...)`: + + ```ts + const backend = createBackend(); + backend.add(customRootLoggerServiceFactory); + ``` + +- Updated dependencies + - @backstage/plugin-auth-node@0.2.17 + - @backstage/backend-tasks@0.5.5 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + +## 0.6.0-next.2 + +### Patch Changes + +- cc9256a33bcc: Added new experimental `featureDiscoveryServiceRef`, available as an `/alpha` export. +- Updated dependencies + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + ## 0.6.0-next.1 ### Minor Changes diff --git a/packages/backend-plugin-api/alpha-api-report.md b/packages/backend-plugin-api/alpha-api-report.md new file mode 100644 index 0000000000..81b378a671 --- /dev/null +++ b/packages/backend-plugin-api/alpha-api-report.md @@ -0,0 +1,24 @@ +## API Report File for "@backstage/backend-plugin-api" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { ServiceRef } from '@backstage/backend-plugin-api'; + +// @alpha (undocumented) +export interface FeatureDiscoveryService { + // (undocumented) + getBackendFeatures(): Promise<{ + features: Array; + }>; +} + +// @alpha +export const featureDiscoveryServiceRef: ServiceRef< + FeatureDiscoveryService, + 'root' +>; + +// (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 a1ae6284c6..7807dd96c6 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -31,6 +31,11 @@ export interface BackendModuleConfig { // @public export interface BackendModuleRegistrationPoints { + // (undocumented) + registerExtensionPoint( + ref: ExtensionPoint, + impl: TExtensionPoint, + ): void; // (undocumented) registerInit< Deps extends { @@ -114,14 +119,14 @@ export namespace coreServices { } // @public -export function createBackendModule( - config: BackendModuleConfig | ((...params: TOptions) => BackendModuleConfig), -): (...params: TOptions) => BackendFeature; +export function createBackendModule( + config: BackendModuleConfig, +): () => BackendFeature; // @public -export function createBackendPlugin( - config: BackendPluginConfig | ((...params: TOptions) => BackendPluginConfig), -): (...params: TOptions) => BackendFeature; +export function createBackendPlugin( + config: BackendPluginConfig, +): () => BackendFeature; // @public export function createExtensionPoint( @@ -133,7 +138,7 @@ export function createServiceFactory< TService, TImpl extends TService, TDeps extends { - [name in string]: ServiceRef; + [name in string]: ServiceRef; }, TOpts extends object | undefined = undefined, >( @@ -145,25 +150,13 @@ export function createServiceFactory< TService, TImpl extends TService, TDeps extends { - [name in string]: ServiceRef; + [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; - }, - TOpts extends object | undefined = undefined, ->( - config: (options: TOpts) => RootServiceFactoryConfig, -): (options: TOpts) => ServiceFactory; - // @public export function createServiceFactory< TService, @@ -192,23 +185,6 @@ export function createServiceFactory< ) => PluginServiceFactoryConfig, ): (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 - | (( - options: TOpts, - ) => PluginServiceFactoryConfig), -): (options: TOpts) => ServiceFactory; - // @public export function createServiceRef( config: ServiceRefConfig, @@ -433,9 +409,7 @@ export type SearchResponseFile = { export interface ServiceFactory< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', -> { - // (undocumented) - $$type: '@backstage/ServiceFactory'; +> extends BackendFeature { // (undocumented) service: ServiceRef; } diff --git a/packages/backend-plugin-api/catalog-info.yaml b/packages/backend-plugin-api/catalog-info.yaml new file mode 100644 index 0000000000..35ca9850fa --- /dev/null +++ b/packages/backend-plugin-api/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-backend-plugin-api + title: '@backstage/backend-plugin-api' + description: Core API used by Backstage backend plugins +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index 5d0bfe08fb..8e160f9311 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -1,17 +1,30 @@ { "name": "@backstage/backend-plugin-api", "description": "Core API used by Backstage backend plugins", - "version": "0.6.0-next.1", + "version": "0.6.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" }, "backstage": { "role": "node-library" }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } + }, "homepage": "https://backstage.io", "repository": { "type": "git", diff --git a/packages/backend-plugin-api/src/alpha.ts b/packages/backend-plugin-api/src/alpha.ts new file mode 100644 index 0000000000..baee739f49 --- /dev/null +++ b/packages/backend-plugin-api/src/alpha.ts @@ -0,0 +1,35 @@ +/* + * 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 { + BackendFeature, + createServiceRef, +} from '@backstage/backend-plugin-api'; + +/** @alpha */ +export interface FeatureDiscoveryService { + getBackendFeatures(): Promise<{ features: Array }>; +} + +/** + * An optional service that can be used to dynamically load in additional BackendFeatures at runtime. + * @alpha + */ +export const featureDiscoveryServiceRef = + createServiceRef({ + id: 'core.featureDiscovery', + scope: 'root', + }); diff --git a/packages/backend-plugin-api/src/index.ts b/packages/backend-plugin-api/src/index.ts index 452fa046f9..8e0b23d20b 100644 --- a/packages/backend-plugin-api/src/index.ts +++ b/packages/backend-plugin-api/src/index.ts @@ -21,4 +21,5 @@ */ export * from './services'; +export type { BackendFeature } from './types'; export * from './wiring'; 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 d3e9b9dd4e..adf097761b 100644 --- a/packages/backend-plugin-api/src/services/system/types.test.ts +++ b/packages/backend-plugin-api/src/services/system/types.test.ts @@ -26,7 +26,7 @@ interface TestOptions { function unused(..._any: any[]) {} describe('createServiceFactory', () => { - it('should create a sync factory with no options', () => { + it('should create a sync factory', () => { const metaFactory = createServiceFactory({ service: ref, deps: {}, @@ -51,7 +51,7 @@ describe('createServiceFactory', () => { metaFactory(); }); - it('should create a sync root factory with no options', () => { + it('should create a sync root factory', () => { const metaFactory = createServiceFactory({ service: rootDep, deps: {}, @@ -75,7 +75,7 @@ describe('createServiceFactory', () => { metaFactory(); }); - it('should create a factory with no options', () => { + it('should create a factory', () => { const metaFactory = createServiceFactory({ service: ref, deps: {}, @@ -100,7 +100,7 @@ describe('createServiceFactory', () => { metaFactory(); }); - it('should create a factory with optional options', () => { + it('should create a factory with options', () => { const metaFactory = createServiceFactory((_opts?: { x: number }) => ({ service: ref, deps: {}, @@ -124,7 +124,8 @@ describe('createServiceFactory', () => { metaFactory(); }); - it('should create a factory with required options', () => { + it('should not be allowed to require options', () => { + // @ts-expect-error const metaFactory = createServiceFactory((_opts: { x: number }) => ({ service: ref, deps: {}, @@ -134,23 +135,9 @@ describe('createServiceFactory', () => { }, })); expect(metaFactory).toEqual(expect.any(Function)); - - // @ts-expect-error - metaFactory('string'); - // @ts-expect-error - metaFactory({}); - metaFactory({ x: 1 }); - // @ts-expect-error - metaFactory({ x: 1, y: 2 }); - // @ts-expect-error - metaFactory(null); - // @ts-expect-error - metaFactory(undefined); - // @ts-expect-error - metaFactory(); }); - it('should create a factory with optional options as interface', () => { + it('should create a factory with options as interface', () => { const metaFactory = createServiceFactory((_opts?: TestOptions) => ({ service: ref, deps: {}, @@ -174,38 +161,11 @@ describe('createServiceFactory', () => { metaFactory(); }); - it('should create a factory with required options as interface', () => { - const metaFactory = createServiceFactory((_opts: TestOptions) => ({ - service: ref, - deps: {}, - async createRootContext() {}, - async factory() { - return 'x'; - }, - })); - expect(metaFactory).toEqual(expect.any(Function)); - - // @ts-expect-error - metaFactory('string'); - // @ts-expect-error - metaFactory({}); - metaFactory({ x: 1 }); - // @ts-expect-error - metaFactory({ x: 1, y: 2 }); - // @ts-expect-error - metaFactory(null); - // @ts-expect-error - metaFactory(undefined); - // @ts-expect-error - metaFactory(); - }); - it('should create root scoped factory with dependencies', () => { const metaFactory = createServiceFactory({ service: createServiceRef({ id: 'foo', scope: 'root' }), deps: { root: rootDep, - plugin: pluginDep, }, async factory({ root }) { const root1: number = root; @@ -226,12 +186,11 @@ describe('createServiceFactory', () => { metaFactory(); }); - it('should create root scoped factory with dependencies and optional options', () => { + it('should create root scoped factory with dependencies and options', () => { const metaFactory = createServiceFactory((_options?: TestOptions) => ({ service: createServiceRef({ id: 'foo', scope: 'root' }), deps: { root: rootDep, - plugin: pluginDep, }, async factory({ root }) { const root1: number = root; @@ -325,49 +284,7 @@ describe('createServiceFactory', () => { metaFactory(); }); - it('should create factory with required options and dependencies', () => { - const metaFactory = createServiceFactory((_opts: TestOptions) => ({ - service: ref, - deps: { - root: rootDep, - plugin: pluginDep, - }, - async createRootContext({ root }) { - const root1: number = root; - // @ts-expect-error - const root2: string = root; - unused(root1, root2); - return { root }; - }, - async factory({ plugin }, { root }) { - const root1: number = root; - // @ts-expect-error - const root2: string = root; - const plugin3: boolean = plugin; - // @ts-expect-error - const plugin4: number = plugin; - unused(root1, root2, plugin3, plugin4); - return 'x'; - }, - })); - expect(metaFactory).toEqual(expect.any(Function)); - - // @ts-expect-error - metaFactory('string'); - // @ts-expect-error - metaFactory({}); - metaFactory({ x: 1 }); - // @ts-expect-error - metaFactory({ x: 1, y: 2 }); - // @ts-expect-error - metaFactory(null); - // @ts-expect-error - metaFactory(undefined); - // @ts-expect-error - metaFactory(); - }); - - it('should create factory with optional options and dependencies', () => { + it('should create factory with options and dependencies', () => { const metaFactory = createServiceFactory((_opts?: TestOptions) => ({ service: ref, deps: { diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index 190ef7c13e..329502369e 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { BackendFeature } from '../../types'; + /** * TODO * @@ -51,9 +53,7 @@ export type ServiceRef< export interface ServiceFactory< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', -> { - $$type: '@backstage/ServiceFactory'; - +> extends BackendFeature { service: ServiceRef; } @@ -172,7 +172,7 @@ export interface PluginServiceFactoryConfig< export function createServiceFactory< TService, TImpl extends TService, - TDeps extends { [name in string]: ServiceRef }, + TDeps extends { [name in string]: ServiceRef }, TOpts extends object | undefined = undefined, >( config: RootServiceFactoryConfig, @@ -186,25 +186,11 @@ export function createServiceFactory< export function createServiceFactory< TService, TImpl extends TService, - TDeps extends { [name in string]: ServiceRef }, + TDeps extends { [name in string]: ServiceRef }, TOpts extends object | undefined = undefined, >( config: (options?: TOpts) => RootServiceFactoryConfig, ): (options?: TOpts) => ServiceFactory; -/** - * Creates a root scoped service factory with required options. - * - * @public - * @param config - The service factory configuration. - */ -export function createServiceFactory< - TService, - TImpl extends TService, - TDeps extends { [name in string]: ServiceRef }, - TOpts extends object | undefined = undefined, ->( - config: (options: TOpts) => RootServiceFactoryConfig, -): (options: TOpts) => ServiceFactory; /** * Creates a plugin scoped service factory without options. * @@ -237,25 +223,6 @@ export function createServiceFactory< options?: TOpts, ) => PluginServiceFactoryConfig, ): (options?: TOpts) => ServiceFactory; -/** - * Creates a plugin scoped service factory with required options. - * - * @public - * @param config - The service factory configuration. - */ -export function createServiceFactory< - TService, - TImpl extends TService, - TDeps extends { [name in string]: ServiceRef }, - TContext = undefined, - TOpts extends object | undefined = undefined, ->( - config: - | PluginServiceFactoryConfig - | (( - options: TOpts, - ) => PluginServiceFactoryConfig), -): (options: TOpts) => ServiceFactory; export function createServiceFactory< TService, TImpl extends TService, @@ -274,14 +241,14 @@ export function createServiceFactory< | (() => PluginServiceFactoryConfig), ): (options: TOpts) => ServiceFactory { const configCallback = typeof config === 'function' ? config : () => config; - return ( + const factory = ( options: TOpts, ): InternalServiceFactory => { const anyConf = configCallback(options); if (anyConf.service.scope === 'root') { const c = anyConf as RootServiceFactoryConfig; return { - $$type: '@backstage/ServiceFactory', + $$type: '@backstage/BackendFeature', version: 'v1', service: c.service, deps: c.deps, @@ -295,7 +262,7 @@ export function createServiceFactory< TDeps >; return { - $$type: '@backstage/ServiceFactory', + $$type: '@backstage/BackendFeature', version: 'v1', service: c.service, ...('createRootContext' in c @@ -308,4 +275,8 @@ export function createServiceFactory< factory: async (deps: TDeps, ctx: TContext) => c.factory(deps, ctx), }; }; + + factory.$$type = '@backstage/BackendFeatureFactory'; + + return factory; } diff --git a/packages/backend-plugin-api/src/types.ts b/packages/backend-plugin-api/src/types.ts new file mode 100644 index 0000000000..59f1106fab --- /dev/null +++ b/packages/backend-plugin-api/src/types.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +/** @internal */ +export interface BackendFeatureFactory< + TOptions extends [options?: object] = [], +> { + (...options: TOptions): BackendFeature; + $$type: '@backstage/BackendFeatureFactory'; +} + +/** @public */ +export interface BackendFeature { + // NOTE: This type is opaque in order to simplify future API evolution. + $$type: '@backstage/BackendFeature'; +} diff --git a/packages/backend-plugin-api/src/wiring/factories.test.ts b/packages/backend-plugin-api/src/wiring/factories.test.ts index ae199d84e4..1fd05a1605 100644 --- a/packages/backend-plugin-api/src/wiring/factories.test.ts +++ b/packages/backend-plugin-api/src/wiring/factories.test.ts @@ -33,22 +33,19 @@ describe('createExtensionPoint', () => { describe('createBackendPlugin', () => { it('should create a BackendPlugin', () => { - const plugin = createBackendPlugin((_options: { a: string }) => ({ + const plugin = createBackendPlugin({ pluginId: 'x', register(r) { r.registerInit({ deps: {}, async init() {} }); }, - })); + }); expect(plugin).toBeDefined(); - expect(plugin({ a: 'a' })).toBeDefined(); - expect(plugin({ a: 'a' })).toEqual({ + expect(plugin()).toEqual({ $$type: '@backstage/BackendFeature', version: 'v1', getRegistrations: expect.any(Function), }); - expect( - (plugin({ a: 'a' }) as InternalBackendFeature).getRegistrations(), - ).toEqual([ + expect((plugin() as InternalBackendFeature).getRegistrations()).toEqual([ { type: 'plugin', pluginId: 'x', @@ -60,95 +57,32 @@ describe('createBackendPlugin', () => { }, ]); // @ts-expect-error - expect(plugin()).toBeDefined(); - // @ts-expect-error - expect(plugin({ b: 'b' })).toBeDefined(); - }); - - it('should create plugins with optional options', () => { - const plugin = createBackendPlugin((_options?: { a: string }) => ({ - pluginId: 'x', - register() {}, - })); - expect(plugin).toBeDefined(); expect(plugin({ a: 'a' })).toBeDefined(); - expect(plugin()).toBeDefined(); - // @ts-expect-error - expect(plugin({ b: 'b' })).toBeDefined(); - }); - - it('should create plugins without options', () => { - const plugin = createBackendPlugin({ - pluginId: 'x', - register() {}, - }); - expect(plugin).toBeDefined(); - // @ts-expect-error - expect(plugin({ a: 'a' })).toBeDefined(); - // @ts-expect-error - expect(plugin({})).toBeDefined(); - }); - - it('should create a BackendPlugin with options as interface', () => { - interface TestOptions { - a: string; - } - const plugin = createBackendPlugin((_options: TestOptions) => ({ - pluginId: 'x', - register() {}, - })); - expect(plugin).toBeDefined(); - expect(plugin({ a: 'a' })).toBeDefined(); - expect(plugin({ a: 'a' })).toEqual({ - $$type: '@backstage/BackendFeature', - version: 'v1', - getRegistrations: expect.any(Function), - }); - // @ts-expect-error - expect(plugin()).toBeDefined(); - // @ts-expect-error - expect(plugin({ b: 'b' })).toBeDefined(); - }); - - it('should create plugins with optional options as interface', () => { - interface TestOptions { - a: string; - } - const plugin = createBackendPlugin((_options?: TestOptions) => ({ - pluginId: 'x', - register() {}, - })); - expect(plugin).toBeDefined(); - expect(plugin({ a: 'a' })).toBeDefined(); - expect(plugin()).toBeDefined(); - // @ts-expect-error - expect(plugin({ b: 'b' })).toBeDefined(); }); }); describe('createBackendModule', () => { it('should create a BackendModule', () => { - const mod = createBackendModule((_options: { a: string }) => ({ + const mod = createBackendModule({ pluginId: 'x', moduleId: 'y', register(r) { r.registerInit({ deps: {}, async init() {} }); }, - })); + }); expect(mod).toBeDefined(); - expect(mod({ a: 'a' })).toBeDefined(); - expect(mod({ a: 'a' })).toEqual({ + expect(mod()).toBeDefined(); + expect(mod()).toEqual({ $$type: '@backstage/BackendFeature', version: 'v1', getRegistrations: expect.any(Function), }); - expect( - (mod({ a: 'a' }) as InternalBackendFeature).getRegistrations(), - ).toEqual([ + expect((mod() as InternalBackendFeature).getRegistrations()).toEqual([ { type: 'module', pluginId: 'x', moduleId: 'y', + extensionPoints: [], init: { deps: expect.any(Object), func: expect.any(Function), @@ -156,72 +90,6 @@ describe('createBackendModule', () => { }, ]); // @ts-expect-error - expect(mod()).toBeDefined(); - // @ts-expect-error - expect(mod({ b: 'b' })).toBeDefined(); - }); - - it('should create modules with optional options', () => { - const mod = createBackendModule((_options?: { a: string }) => ({ - pluginId: 'x', - moduleId: 'y', - register() {}, - })); - expect(mod).toBeDefined(); expect(mod({ a: 'a' })).toBeDefined(); - expect(mod()).toBeDefined(); - // @ts-expect-error - expect(mod({ b: 'b' })).toBeDefined(); - }); - - it('should create modules without options', () => { - const mod = createBackendModule({ - pluginId: 'x', - moduleId: 'y', - register() {}, - }); - expect(mod).toBeDefined(); - // @ts-expect-error - expect(mod({ a: 'a' })).toBeDefined(); - // @ts-expect-error - expect(mod({})).toBeDefined(); - }); - - it('should create a BackendModule as interface', () => { - interface TestOptions { - a: string; - } - const mod = createBackendModule((_options: TestOptions) => ({ - pluginId: 'x', - moduleId: 'y', - register() {}, - })); - expect(mod).toBeDefined(); - expect(mod({ a: 'a' })).toBeDefined(); - expect(mod({ a: 'a' })).toEqual({ - $$type: '@backstage/BackendFeature', - version: 'v1', - getRegistrations: expect.any(Function), - }); - // @ts-expect-error - expect(mod()).toBeDefined(); - // @ts-expect-error - expect(mod({ b: 'b' })).toBeDefined(); - }); - - it('should create modules with optional options as interface', () => { - interface TestOptions { - a: string; - } - const mod = createBackendModule((_options?: TestOptions) => ({ - pluginId: 'x', - moduleId: 'y', - register() {}, - })); - expect(mod).toBeDefined(); - expect(mod({ a: 'a' })).toBeDefined(); - expect(mod()).toBeDefined(); - // @ts-expect-error - expect(mod({ b: 'b' })).toBeDefined(); }); }); diff --git a/packages/backend-plugin-api/src/wiring/factories.ts b/packages/backend-plugin-api/src/wiring/factories.ts index 928c933af0..9fa0f87bab 100644 --- a/packages/backend-plugin-api/src/wiring/factories.ts +++ b/packages/backend-plugin-api/src/wiring/factories.ts @@ -14,12 +14,11 @@ * limitations under the License. */ +import { BackendFeature, BackendFeatureFactory } from '../types'; import { BackendModuleRegistrationPoints, BackendPluginRegistrationPoints, - BackendFeature, ExtensionPoint, - InternalBackendFeature, InternalBackendModuleRegistration, InternalBackendPluginRegistration, } from './types'; @@ -85,13 +84,10 @@ export interface BackendPluginConfig { * @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( - config: BackendPluginConfig | ((...params: TOptions) => BackendPluginConfig), -): (...params: TOptions) => BackendFeature { - const configCallback = typeof config === 'function' ? config : () => config; - return (...options: TOptions): InternalBackendFeature => { - const c = configCallback(...options); - +export function createBackendPlugin( + config: BackendPluginConfig, +): () => BackendFeature { + const factory: BackendFeatureFactory = () => { let registrations: InternalBackendPluginRegistration[]; return { @@ -106,7 +102,7 @@ export function createBackendPlugin( let init: InternalBackendPluginRegistration['init'] | undefined = undefined; - c.register({ + config.register({ registerExtensionPoint(ext, impl) { if (init) { throw new Error( @@ -128,14 +124,14 @@ export function createBackendPlugin( if (!init) { throw new Error( - `registerInit was not called by register in ${c.pluginId}`, + `registerInit was not called by register in ${config.pluginId}`, ); } registrations = [ { type: 'plugin', - pluginId: c.pluginId, + pluginId: config.pluginId, extensionPoints, init, }, @@ -144,6 +140,9 @@ export function createBackendPlugin( }, }; }; + factory.$$type = '@backstage/BackendFeatureFactory'; + + return factory; } /** @@ -155,13 +154,14 @@ export function createBackendPlugin( */ export interface BackendModuleConfig { /** - * The ID of this plugin. + * 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; + /** - * Should exactly match the `id` of the plugin that the module extends. + * The ID of this module, used to identify the module and ensure that it is not installed twice. */ moduleId: string; register(reg: BackendModuleRegistrationPoints): void; @@ -174,13 +174,10 @@ export interface BackendModuleConfig { * @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( - config: BackendModuleConfig | ((...params: TOptions) => BackendModuleConfig), -): (...params: TOptions) => BackendFeature { - const configCallback = typeof config === 'function' ? config : () => config; - return (...options: TOptions): InternalBackendFeature => { - const c = configCallback(...options); - +export function createBackendModule( + config: BackendModuleConfig, +): () => BackendFeature { + const factory: BackendFeatureFactory = () => { let registrations: InternalBackendModuleRegistration[]; return { @@ -190,10 +187,20 @@ export function createBackendModule( if (registrations) { return registrations; } + const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] = + []; let init: InternalBackendModuleRegistration['init'] | undefined = undefined; - c.register({ + config.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'); @@ -207,15 +214,16 @@ export function createBackendModule( if (!init) { throw new Error( - `registerInit was not called by register in ${c.moduleId} module for ${c.pluginId}`, + `registerInit was not called by register in ${config.moduleId} module for ${config.pluginId}`, ); } registrations = [ { type: 'module', - pluginId: c.pluginId, - moduleId: c.moduleId, + pluginId: config.pluginId, + moduleId: config.moduleId, + extensionPoints, init, }, ]; @@ -223,4 +231,7 @@ export function createBackendModule( }, }; }; + factory.$$type = '@backstage/BackendFeatureFactory'; + + return factory; } diff --git a/packages/backend-plugin-api/src/wiring/index.ts b/packages/backend-plugin-api/src/wiring/index.ts index 9cb767b8ac..49f15c0b55 100644 --- a/packages/backend-plugin-api/src/wiring/index.ts +++ b/packages/backend-plugin-api/src/wiring/index.ts @@ -27,6 +27,5 @@ export { export type { BackendModuleRegistrationPoints, BackendPluginRegistrationPoints, - BackendFeature, ExtensionPoint, } from './types'; diff --git a/packages/backend-plugin-api/src/wiring/types.ts b/packages/backend-plugin-api/src/wiring/types.ts index c81ee3ceb7..55d43ccb7c 100644 --- a/packages/backend-plugin-api/src/wiring/types.ts +++ b/packages/backend-plugin-api/src/wiring/types.ts @@ -15,6 +15,7 @@ */ import { ServiceRef } from '../services/system/types'; +import { BackendFeature } from '../types'; /** * TODO @@ -59,6 +60,10 @@ export interface BackendPluginRegistrationPoints { * @public */ export interface BackendModuleRegistrationPoints { + registerExtensionPoint( + ref: ExtensionPoint, + impl: TExtensionPoint, + ): void; registerInit(options: { deps: { [name in keyof Deps]: ServiceRef | ExtensionPoint; @@ -67,12 +72,6 @@ export interface BackendModuleRegistrationPoints { }): void; } -/** @public */ -export interface BackendFeature { - // NOTE: This type is opaque in order to simplify future API evolution. - $$type: '@backstage/BackendFeature'; -} - /** @internal */ export interface InternalBackendFeature extends BackendFeature { version: 'v1'; @@ -97,6 +96,7 @@ export interface InternalBackendModuleRegistration { pluginId: string; moduleId: string; type: 'module'; + extensionPoints: Array, unknown]>; init: { deps: Record | ExtensionPoint>; func(deps: Record): Promise; diff --git a/packages/backend-plugin-manager/.eslintrc.js b/packages/backend-plugin-manager/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/backend-plugin-manager/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/backend-plugin-manager/CHANGELOG.md b/packages/backend-plugin-manager/CHANGELOG.md new file mode 100644 index 0000000000..7822f30008 --- /dev/null +++ b/packages/backend-plugin-manager/CHANGELOG.md @@ -0,0 +1,24 @@ +# @backstage/backend-plugin-manager + +## 0.0.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-events-backend@0.2.12-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.2 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-events-node@0.2.12-next.2 + - @backstage/plugin-search-common@1.2.6-next.1 diff --git a/packages/backend-plugin-manager/README.md b/packages/backend-plugin-manager/README.md new file mode 100644 index 0000000000..c9fd666fb3 --- /dev/null +++ b/packages/backend-plugin-manager/README.md @@ -0,0 +1,52 @@ +# @backstage/backend-plugin-manager + +This package adds experimental support for **dynamic backend plugins**, according to the content of the proposal in [RFC 18390](https://github.com/backstage/backstage/issues/18390) + +## Status + +**This package is EXPERIMENTAL, and is subject to change according to the discussions and conclusions that happen around the RFC mentioned above.** + +## Testing the backend dynamic plugins feature + +In order to test the dynamic backend plugins feature provided by this package, example applications, as well as example dynamic plugins have been provided in provided in the [showcase repository](https://github.com/janus-idp/dynamic-backend-plugins-showcase), and instructions are provided in the [related Readme](https://github.com/janus-idp/dynamic-backend-plugins-showcase#readme). + +## How it works + +The backend plugin manager is a service that scans a configured root directory (`dynamicPlugins.rootDirectory` in the app config) for dynamic plugin packages, and loads them dynamically. + +In the `backend-next` application, it can be enabled by adding the `backend-plugin-manager` as a dependency in the `package.json` and the following lines in the `src/index.ts` file: + +```ts +const backend = createBackend(); ++ ++ backend.add(dynamicPluginsFeatureDiscoveryServiceFactory()) // overridden version of the FeatureDiscoveryService which provides features loaded by dynamic plugins ++ backend.add(dynamicPluginsServiceFactory()) ++ +``` + +### About the expected dynamic plugin structure + +Due to some limitations of the current backstage codebase, the plugins need to be completed and repackaged to by used as dynamic plugins: + +1. they must provide a named entry point (`dynamicPluginInstaller`) of a specific type (`BackendDynamicPluginInstaller`), as can be found in the `src/dynamic` sub-folder of each dynamic plugin example provided in the [showcase repository](https://github.com/janus-idp/dynamic-backend-plugins-showcase). +2. they would have a modified `package.json` file in which dependencies are updated to share `@backstage` dependencies with the main application. +3. they may embed some dependency whose code is then merged with the plugin code. + +Points 2 and 3 can be done by the `export-dynamic-plugin` CLI command used to perform the repackaging + +### About the `export-dynamic-plugin` command + +The `export-dynamic-plugin` CLI command, used the dynamic plugin examples provided in the [showcase repository](https://github.com/janus-idp/dynamic-backend-plugins-showcase), is part of a `@backstage/cli` fork (`@dfatwork-pkgs/backstage-cli@0.22.9-next.6`), and can be used to help packaging the dynamic plugins according to the constraints mentioned above, in order to allow straightforward testing of the dynamic plugins feature. + +However the `backend-plugin-manager` experimental package does **not** depend on the use of this additional CLI command, and in future steps of this backend dynamic plugin work, the use of such a dedicated command might not even be necessary. + +### About the support of the legacy backend system + +The backend dynamic plugins feature clearly targets the new backend system. +However some level of compatibility is provided with current backstage codebase, which still uses the legacy backend system, in order to allow testing and exploring dynamic backend plugin support on the widest range of contexts and installations. +However, this is temporary and will be removed once the next backend is ready to be used and has completely replaced the old one. +This is why the API related to the old backend is already marked as deprecated. + +### Future work + +The current implementation of the backend plugin manager is a first step towards the final implementation of the dynamic backend plugins feature, which will be completed / simplified in future steps, as the status of the backstage codebase allows it. diff --git a/packages/backend-plugin-manager/api-report.md b/packages/backend-plugin-manager/api-report.md new file mode 100644 index 0000000000..9617dc1096 --- /dev/null +++ b/packages/backend-plugin-manager/api-report.md @@ -0,0 +1,199 @@ +## API Report File for "@backstage/backend-plugin-manager" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { BackstagePackageJson } from '@backstage/cli-node'; +import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; +import { Config } from '@backstage/config'; +import { EventBroker } from '@backstage/plugin-events-node'; +import { EventsBackend } from '@backstage/plugin-events-backend'; +import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha'; +import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; +import { IdentityApi } from '@backstage/plugin-auth-node'; +import { IndexBuilder } from '@backstage/plugin-search-backend-node'; +import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { PackagePlatform } from '@backstage/cli-node'; +import { PackageRole } from '@backstage/cli-node'; +import { PermissionEvaluator } from '@backstage/plugin-permission-common'; +import { PermissionPolicy } from '@backstage/plugin-permission-node'; +import { PluginCacheManager } from '@backstage/backend-common'; +import { PluginDatabaseManager } from '@backstage/backend-common'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { Router } from 'express'; +import { ServiceFactory } 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'; +import { TokenManager } from '@backstage/backend-common'; +import { UrlReader } from '@backstage/backend-common'; + +// @public (undocumented) +export interface BackendDynamicPlugin extends BaseDynamicPlugin { + // (undocumented) + installer: BackendDynamicPluginInstaller; + // (undocumented) + platform: 'node'; +} + +// @public (undocumented) +export type BackendDynamicPluginInstaller = + | LegacyBackendPluginInstaller + | NewBackendPluginInstaller; + +// @public (undocumented) +export interface BackendPluginProvider { + // (undocumented) + backendPlugins(): BackendDynamicPlugin[]; +} + +// @public (undocumented) +export interface BaseDynamicPlugin { + // (undocumented) + name: string; + // (undocumented) + platform: PackagePlatform; + // (undocumented) + role: PackageRole; + // (undocumented) + version: string; +} + +// @public (undocumented) +export type DynamicPlugin = FrontendDynamicPlugin | BackendDynamicPlugin; + +// @public (undocumented) +export interface DynamicPluginsFactoryOptions { + // (undocumented) + moduleLoader?(logger: LoggerService): ModuleLoader; +} + +// @public (undocumented) +export const dynamicPluginsFeatureDiscoveryServiceFactory: () => ServiceFactory< + FeatureDiscoveryService, + 'root' +>; + +// @public (undocumented) +export const dynamicPluginsServiceFactory: ( + options?: DynamicPluginsFactoryOptions | undefined, +) => ServiceFactory; + +// @public (undocumented) +export const dynamicPluginsServiceRef: ServiceRef< + BackendPluginProvider, + 'root' +>; + +// @public (undocumented) +export interface FrontendDynamicPlugin extends BaseDynamicPlugin { + // (undocumented) + platform: 'web'; +} + +// @public (undocumented) +export function isBackendDynamicPluginInstaller( + obj: any, +): obj is BackendDynamicPluginInstaller; + +// @public @deprecated (undocumented) +export interface LegacyBackendPluginInstaller { + // (undocumented) + catalog?(builder: CatalogBuilder, env: LegacyPluginEnvironment): void; + // (undocumented) + events?( + eventsBackend: EventsBackend, + env: LegacyPluginEnvironment, + ): HttpPostIngressOptions[]; + // (undocumented) + kind: 'legacy'; + // (undocumented) + permissions?: { + policy?: PermissionPolicy; + }; + // (undocumented) + router?: { + pluginID: string; + createPlugin(env: LegacyPluginEnvironment): Promise; + }; + // (undocumented) + scaffolder?(env: LegacyPluginEnvironment): TemplateAction[]; + // (undocumented) + search?( + indexBuilder: IndexBuilder, + schedule: TaskRunner, + env: LegacyPluginEnvironment, + ): void; +} + +// @public @deprecated (undocumented) +export type LegacyPluginEnvironment = { + logger: Logger; + cache: PluginCacheManager; + database: PluginDatabaseManager; + config: Config; + reader: UrlReader; + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + permissions: PermissionEvaluator; + scheduler: PluginTaskScheduler; + identity: IdentityApi; + eventBroker: EventBroker; + pluginProvider: BackendPluginProvider; +}; + +// @public (undocumented) +export interface ModuleLoader { + // (undocumented) + bootstrap(backstageRoot: string, dynamicPluginPaths: string[]): Promise; + // (undocumented) + load(id: string): Promise; +} + +// @public (undocumented) +export interface NewBackendPluginInstaller { + // (undocumented) + install(): BackendFeature | BackendFeature[]; + // (undocumented) + kind: 'new'; +} + +// @public (undocumented) +export class PluginManager implements BackendPluginProvider { + // (undocumented) + addBackendPlugin(plugin: BackendDynamicPlugin): void; + // (undocumented) + get availablePackages(): ScannedPluginPackage[]; + // (undocumented) + backendPlugins(): BackendDynamicPlugin[]; + // (undocumented) + static fromConfig( + config: Config, + logger: LoggerService, + preferAlpha?: boolean, + mooduleLoader?: ModuleLoader, + ): Promise; + // (undocumented) + readonly plugins: DynamicPlugin[]; +} + +// @public (undocumented) +export type ScannedPluginManifest = BackstagePackageJson & + Required> & + Required> & { + backstage: Required; + }; + +// @public (undocumented) +export interface ScannedPluginPackage { + // (undocumented) + location: URL; + // (undocumented) + manifest: ScannedPluginManifest; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-plugin-manager/config.d.ts b/packages/backend-plugin-manager/config.d.ts new file mode 100644 index 0000000000..9427458a23 --- /dev/null +++ b/packages/backend-plugin-manager/config.d.ts @@ -0,0 +1,24 @@ +/* + * 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 interface Config { + dynamicPlugins?: { + /** + * The local path, relative to the backstage root, that contains dynamic plugins. + */ + rootDirectory: string; + }; +} diff --git a/packages/backend-plugin-manager/package.json b/packages/backend-plugin-manager/package.json new file mode 100644 index 0000000000..f98ab5270a --- /dev/null +++ b/packages/backend-plugin-manager/package.json @@ -0,0 +1,64 @@ +{ + "name": "@backstage/backend-plugin-manager", + "description": "Backstage plugin management backend", + "version": "0.0.1-next.0", + "private": true, + "main": "src/index.ts", + "types": "src/index.ts", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "node-library" + }, + "keywords": [ + "backstage" + ], + "license": "Apache-2.0", + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/backend-tasks": "workspace:^", + "@backstage/cli-common": "workspace:^", + "@backstage/cli-node": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "@backstage/plugin-catalog-backend": "workspace:^", + "@backstage/plugin-events-backend": "workspace:^", + "@backstage/plugin-events-node": "workspace:^", + "@backstage/plugin-permission-common": "workspace:^", + "@backstage/plugin-permission-node": "workspace:^", + "@backstage/plugin-scaffolder-node": "workspace:^", + "@backstage/plugin-search-backend-node": "workspace:^", + "@backstage/plugin-search-common": "workspace:^", + "@backstage/types": "workspace:^", + "@types/express": "^4.17.6", + "chokidar": "^3.5.3", + "express": "^4.17.1", + "lodash": "^4.17.21", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/backend-app-api": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/config-loader": "workspace:^", + "mock-fs": "^5.2.0", + "wait-for-expect": "^3.0.2" + }, + "files": [ + "dist" + ] +} diff --git a/packages/backend-plugin-manager/src/__testUtils__/testUtils.ts b/packages/backend-plugin-manager/src/__testUtils__/testUtils.ts new file mode 100644 index 0000000000..11a4421239 --- /dev/null +++ b/packages/backend-plugin-manager/src/__testUtils__/testUtils.ts @@ -0,0 +1,77 @@ +/* + * 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 { LoggerService } from '@backstage/backend-plugin-api'; +import { JsonObject } from '@backstage/types'; + +export type LogContent = { message: string; meta?: Error | JsonObject }; + +export type Logs = { + errors?: LogContent[]; + warns?: LogContent[]; + infos?: LogContent[]; + debugs?: LogContent[]; +}; + +export class MockedLogger implements LoggerService { + logs: Logs = {}; + + error(message: string, meta?: Error | JsonObject): void { + if (!this.logs.errors) { + this.logs.errors = []; + } + this.logs.errors!.push(toLogContent(message, meta)); + } + warn(message: string, meta?: Error | JsonObject): void { + if (!this.logs.warns) { + this.logs.warns = []; + } + this.logs.warns!.push(toLogContent(message, meta)); + } + info(message: string, meta?: Error | JsonObject): void { + if (!this.logs.infos) { + this.logs.infos = []; + } + this.logs.infos!.push(toLogContent(message, meta)); + } + debug(message: string, meta?: Error | JsonObject): void { + if (!this.logs.debugs) { + this.logs.debugs = []; + } + this.logs.debugs!.push(toLogContent(message, meta)); + } + + child(_: JsonObject): LoggerService { + return this; + } +} + +function toLogContent(message: string, meta?: Error | JsonObject): LogContent { + if (!meta) { + return { message }; + } + const jsonObject = Object.fromEntries(Object.entries(meta)); + if ('name' in meta) { + jsonObject.name = meta.name; + } + if ('message' in meta) { + jsonObject.message = meta.message; + } + return { + message, + meta: jsonObject, + }; +} diff --git a/packages/backend-plugin-manager/src/index.ts b/packages/backend-plugin-manager/src/index.ts new file mode 100644 index 0000000000..5570d8ee29 --- /dev/null +++ b/packages/backend-plugin-manager/src/index.ts @@ -0,0 +1,19 @@ +/* + * 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 './loader'; +export * from './scanner'; +export * from './manager'; diff --git a/packages/backend-plugin-manager/src/loader/CommonJSModuleLoader.ts b/packages/backend-plugin-manager/src/loader/CommonJSModuleLoader.ts new file mode 100644 index 0000000000..dd64dc78f3 --- /dev/null +++ b/packages/backend-plugin-manager/src/loader/CommonJSModuleLoader.ts @@ -0,0 +1,50 @@ +/* + * 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 { ModuleLoader } from './types'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import path from 'path'; + +export class CommonJSModuleLoader implements ModuleLoader { + constructor(public readonly logger: LoggerService) {} + + async bootstrap( + backstageRoot: string, + dynamicPluginsPaths: string[], + ): Promise { + const allowedNodeModulesPaths = [ + `${backstageRoot}/node_modules`, + ...dynamicPluginsPaths.map(p => path.resolve(p, 'node_modules')), + ]; + const Module = require('module'); + const oldNodeModulePaths = Module._nodeModulePaths; + Module._nodeModulePaths = (from: string): string[] => { + const result: string[] = oldNodeModulePaths(from); + if (!dynamicPluginsPaths.some(p => from.startsWith(p))) { + return result; + } + + const filtered = result.filter(p => allowedNodeModulesPaths.includes(p)); + this.logger.debug( + `Overriding node_modules search path for dynamic plugin ${from} to: ${filtered}`, + ); + return filtered; + }; + } + + async load(packagePath: string): Promise { + return await import(/* webpackIgnore: true */ packagePath); + } +} diff --git a/packages/backend-plugin-manager/src/loader/index.ts b/packages/backend-plugin-manager/src/loader/index.ts new file mode 100644 index 0000000000..ff0892e66d --- /dev/null +++ b/packages/backend-plugin-manager/src/loader/index.ts @@ -0,0 +1,17 @@ +/* + * 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 type { ModuleLoader } from './types'; diff --git a/plugins/catalog-backend/src/service/request/parseFullTextFilterFields.ts b/packages/backend-plugin-manager/src/loader/types.ts similarity index 64% rename from plugins/catalog-backend/src/service/request/parseFullTextFilterFields.ts rename to packages/backend-plugin-manager/src/loader/types.ts index 02ba5864fc..12a051e482 100644 --- a/plugins/catalog-backend/src/service/request/parseFullTextFilterFields.ts +++ b/packages/backend-plugin-manager/src/loader/types.ts @@ -1,5 +1,3 @@ -import { parseStringParam } from './common'; - /* * Copyright 2023 The Backstage Authors * @@ -15,14 +13,12 @@ import { parseStringParam } from './common'; * See the License for the specific language governing permissions and * limitations under the License. */ -export function parseFullTextFilterFields(params: Record) { - const fullTextFilterFields = parseStringParam( - params.fullTextFilterFields, - 'fullTextFilterFields', - ); - if (!fullTextFilterFields) { - return undefined; - } - return fullTextFilterFields.split(','); +/** + * @public + */ +export interface ModuleLoader { + bootstrap(backstageRoot: string, dynamicPluginPaths: string[]): Promise; + + load(id: string): Promise; } diff --git a/packages/backend-plugin-manager/src/manager/index.ts b/packages/backend-plugin-manager/src/manager/index.ts new file mode 100644 index 0000000000..a9cd163617 --- /dev/null +++ b/packages/backend-plugin-manager/src/manager/index.ts @@ -0,0 +1,38 @@ +/* + * 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 { isBackendDynamicPluginInstaller } from './types'; + +export type { + BaseDynamicPlugin, + DynamicPlugin, + FrontendDynamicPlugin, + BackendDynamicPlugin, + BackendDynamicPluginInstaller, + NewBackendPluginInstaller, + LegacyBackendPluginInstaller, + LegacyPluginEnvironment, + BackendPluginProvider, +} from './types'; + +export { + PluginManager, + dynamicPluginsFeatureDiscoveryServiceFactory, + dynamicPluginsServiceFactory, + dynamicPluginsServiceRef, +} from './plugin-manager'; + +export type { DynamicPluginsFactoryOptions } from './plugin-manager'; diff --git a/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts b/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts new file mode 100644 index 0000000000..8a17fe082a --- /dev/null +++ b/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts @@ -0,0 +1,538 @@ +/* + * 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 { PluginManager, dynamicPluginsServiceFactory } from './plugin-manager'; +import { + BackendFeature, + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import mockFs, { directory, symlink } from 'mock-fs'; +import * as path from 'path'; +import * as url from 'url'; + +import { + BackendDynamicPlugin, + BaseDynamicPlugin, + DynamicPlugin, + LegacyBackendPluginInstaller, + NewBackendPluginInstaller, + LegacyPluginEnvironment, +} from './types'; +import { ScannedPluginManifest, ScannedPluginPackage } from '../scanner/types'; +import { randomUUID } from 'crypto'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { + createSpecializedBackend, + rootLifecycleServiceFactory, +} from '@backstage/backend-app-api'; +import { ConfigSources } from '@backstage/config-loader'; +import { Logs, MockedLogger, LogContent } from '../__testUtils__/testUtils'; +import { PluginScanner } from '../scanner/plugin-scanner'; +import { findPaths } from '@backstage/cli-common'; + +describe('backend-plugin-manager', () => { + describe('loadPlugins', () => { + afterEach(() => { + mockFs.restore(); + jest.resetModules(); + }); + + type TestCase = { + name: string; + packageManifest: ScannedPluginManifest; + indexFile?: { + retativePath: string[]; + content?: string; + }; + expectedLogs?(location: URL): { + errors?: LogContent[]; + warns?: LogContent[]; + infos?: LogContent[]; + debugs?: LogContent[]; + }; + checkLoadedPlugins: (loadedPlugins: BaseDynamicPlugin[]) => void; + }; + + it.each([ + { + name: 'should successfully load a new backend plugin', + packageManifest: { + name: 'backend-dynamic-plugin-test', + version: '0.0.0', + backstage: { + role: 'backend-plugin', + }, + main: 'dist/index.cjs.js', + }, + indexFile: { + retativePath: ['dist', 'index.cjs.js'], + content: + 'exports.dynamicPluginInstaller={ kind: "new", install: () => [] }', + }, + expectedLogs(location) { + return { + infos: [ + { + message: `loaded dynamic backend plugin 'backend-dynamic-plugin-test' from '${location}'`, + }, + ], + }; + }, + checkLoadedPlugins(plugins) { + expect(plugins).toMatchObject([ + { + name: 'backend-dynamic-plugin-test', + version: '0.0.0', + role: 'backend-plugin', + platform: 'node', + installer: { + kind: 'new', + }, + }, + ]); + const installer: NewBackendPluginInstaller = ( + plugins[0] as BackendDynamicPlugin + ).installer as NewBackendPluginInstaller; + expect(installer.install()).toEqual< + BackendFeature | BackendFeature[] + >([]); + }, + }, + { + name: 'should successfully load a new backend plugin module', + packageManifest: { + name: 'backend-dynamic-plugin-test', + version: '0.0.0', + backstage: { + role: 'backend-plugin-module', + }, + main: 'dist/index.cjs.js', + }, + indexFile: { + retativePath: ['dist', 'index.cjs.js'], + content: + 'exports.dynamicPluginInstaller={ kind: "new", install: () => [] }', + }, + expectedLogs(location) { + return { + infos: [ + { + message: `loaded dynamic backend plugin 'backend-dynamic-plugin-test' from '${location}'`, + }, + ], + }; + }, + checkLoadedPlugins(plugins) { + expect(plugins).toMatchObject([ + { + name: 'backend-dynamic-plugin-test', + version: '0.0.0', + role: 'backend-plugin-module', + platform: 'node', + installer: { + kind: 'new', + }, + }, + ]); + const installer: NewBackendPluginInstaller = ( + plugins[0] as BackendDynamicPlugin + ).installer as NewBackendPluginInstaller; + expect(installer.install()).toEqual< + BackendFeature | BackendFeature[] + >([]); + }, + }, + { + name: 'should fail when no index file', + packageManifest: { + name: 'backend-dynamic-plugin-test', + version: '0.0.0', + backstage: { + role: 'backend-plugin', + }, + main: 'dist/index.cjs.js', + }, + expectedLogs(location) { + return { + errors: [ + { + message: `an error occured while loading dynamic backend plugin 'backend-dynamic-plugin-test' from '${location}'`, + meta: { + name: 'Error', + message: expect.stringContaining( + `Cannot find module '${url.fileURLToPath( + location, + )}/dist/index.cjs.js' from `, + ), + _originalMessage: expect.stringContaining( + `Cannot find module '${url.fileURLToPath( + location, + )}/dist/index.cjs.js' from `, + ), + code: 'MODULE_NOT_FOUND', + hint: '', + moduleName: `${url.fileURLToPath( + location, + )}/dist/index.cjs.js`, + siblingWithSimilarExtensionFound: false, + requireStack: undefined, + }, + }, + ], + }; + }, + checkLoadedPlugins(plugins) { + expect(plugins).toMatchObject([]); + }, + }, + { + name: 'should fail when the expected entry point is not in the index file', + packageManifest: { + name: 'backend-dynamic-plugin-test', + version: '0.0.0', + backstage: { + role: 'backend-plugin', + }, + main: 'dist/index.cjs.js', + }, + indexFile: { + retativePath: ['dist', 'index.cjs.js'], + content: '', + }, + expectedLogs(location) { + return { + errors: [ + { + message: `dynamic backend plugin 'backend-dynamic-plugin-test' could not be loaded from '${location}': the module should export a 'const dynamicPluginInstaller: BackendDynamicPluginInstaller' field.`, + }, + ], + }; + }, + checkLoadedPlugins(plugins) { + expect(plugins).toMatchObject([]); + }, + }, + { + name: 'should fail when the expected entry point is not of the expected type', + packageManifest: { + name: 'backend-dynamic-plugin-test', + version: '0.0.0', + backstage: { + role: 'backend-plugin', + }, + main: 'dist/index.cjs.js', + }, + indexFile: { + retativePath: ['dist', 'index.cjs.js'], + content: + 'exports.dynamicPluginInstaller={ something: "else", unexpectedMethod() {} }', + }, + expectedLogs(location) { + return { + errors: [ + { + message: `dynamic backend plugin 'backend-dynamic-plugin-test' could not be loaded from '${location}': the module should export a 'const dynamicPluginInstaller: BackendDynamicPluginInstaller' field.`, + }, + ], + }; + }, + checkLoadedPlugins(plugins) { + expect(plugins).toMatchObject([]); + }, + }, + { + name: 'should fail when the index file has a syntax error', + packageManifest: { + name: 'backend-dynamic-plugin-test', + version: '0.0.0', + backstage: { + role: 'backend-plugin', + }, + main: 'dist/index.cjs.js', + }, + indexFile: { + retativePath: ['dist', 'index.cjs.js'], + content: 'strange text with syntax error', + }, + expectedLogs(location) { + return { + errors: [ + { + message: `an error occured while loading dynamic backend plugin 'backend-dynamic-plugin-test' from '${location}'`, + meta: { + message: 'Unexpected identifier', + name: 'SyntaxError', + }, + }, + ], + }; + }, + checkLoadedPlugins(plugins) { + expect(plugins).toMatchObject([]); + }, + }, + { + name: 'should successfully load a legacy backend plugin', + packageManifest: { + name: 'backend-dynamic-plugin-test', + version: '0.0.0', + backstage: { + role: 'backend-plugin', + }, + main: 'dist/index.cjs.js', + }, + indexFile: { + retativePath: ['dist', 'index.cjs.js'], + content: + 'exports.dynamicPluginInstaller={ kind: "legacy", scaffolder: (env)=>[] }', + }, + expectedLogs(location) { + return { + infos: [ + { + message: `loaded dynamic backend plugin 'backend-dynamic-plugin-test' from '${location}'`, + }, + ], + }; + }, + checkLoadedPlugins(plugins) { + expect(plugins).toMatchObject([ + { + name: 'backend-dynamic-plugin-test', + version: '0.0.0', + role: 'backend-plugin', + platform: 'node', + installer: { + kind: 'legacy', + }, + }, + ]); + const installer = (plugins[0] as BackendDynamicPlugin) + .installer as LegacyBackendPluginInstaller; + expect(installer.scaffolder!({} as LegacyPluginEnvironment)).toEqual< + TemplateAction[] + >([]); + }, + }, + { + name: 'should successfully load a frontend plugin', + packageManifest: { + name: 'frontend-dynamic-plugin-test', + version: '0.0.0', + backstage: { + role: 'frontend-plugin', + }, + main: 'dist/index.esm.js', + }, + checkLoadedPlugins(plugins) { + expect(plugins).toMatchObject([ + { + name: 'frontend-dynamic-plugin-test', + version: '0.0.0', + role: 'frontend-plugin', + platform: 'web', + }, + ]); + }, + }, + ])('$name', async (tc: TestCase): Promise => { + const plugin: ScannedPluginPackage = { + location: new URL(`file:///node_modules/jest-tests/${randomUUID()}`), + manifest: tc.packageManifest, + }; + + const mockedFiles = { + [path.join(url.fileURLToPath(plugin.location), 'package.json')]: + mockFs.file({ + content: JSON.stringify(plugin), + }), + }; + if (tc.indexFile) { + mockedFiles[ + path.join( + url.fileURLToPath(plugin.location), + ...tc.indexFile.retativePath, + ) + ] = mockFs.file({ + content: tc.indexFile.content, + }); + } + mockFs(mockedFiles); + + const logger = new MockedLogger(); + const pluginManager = new (PluginManager as any)(logger, [plugin], { + logger, + async bootstrap(_: string, __: string[]): Promise {}, + load: async (packagePath: string) => + await import(/* webpackIgnore: true */ packagePath), + }); + + const loadedPlugins: DynamicPlugin[] = await pluginManager.loadPlugins(); + + const expectedLogs = tc.expectedLogs + ? tc.expectedLogs(plugin.location) + : {}; + expect(logger.logs).toEqual(expectedLogs); + + tc.checkLoadedPlugins(loadedPlugins); + }); + }); + + describe('backendPlugins', () => { + it('should return only backend plugins and modules', async () => { + const logger = new MockedLogger(); + const pluginManager = new (PluginManager as any)(logger, '', []); + const plugins: BaseDynamicPlugin[] = [ + { + name: 'a-frontend-plugin', + platform: 'web', + role: 'frontend-plugin', + version: '0.0.0', + }, + { + name: 'a-backend-plugin', + platform: 'node', + role: 'backend-plugin', + version: '0.0.0', + }, + { + name: 'a-backend-module', + platform: 'node', + role: 'backend-plugin-module', + version: '0.0.0', + }, + ]; + pluginManager.plugins = plugins; + expect(pluginManager.backendPlugins()).toEqual([ + { + name: 'a-backend-plugin', + platform: 'node', + role: 'backend-plugin', + version: '0.0.0', + }, + { + name: 'a-backend-module', + platform: 'node', + role: 'backend-plugin-module', + version: '0.0.0', + }, + ]); + }); + }); + + describe('dynamicPluginsServiceFactory', () => { + afterEach(() => { + mockFs.restore(); + jest.resetModules(); + }); + + it('should call PluginManager.fromConfig', async () => { + const logger = new MockedLogger(); + const rootLogger = new MockedLogger(); + + mockFs({ + [findPaths(__dirname).resolveTargetRoot('package.json')]: mockFs.load( + findPaths(__dirname).resolveTargetRoot('package.json'), + ), + '/somewhere/dynamic-plugins-root/a-dynamic-plugin': symlink({ + path: '/somewhere-else/a-dynamic-plugin', + }), + '/somewhere-else/a-dynamic-plugin': directory({}), + }); + + const fromConfigSpier = jest.spyOn(PluginManager, 'fromConfig'); + const applyConfigSpier = jest + .spyOn(PluginScanner.prototype as any, 'applyConfig') + .mockImplementation(() => {}); + const scanRootSpier = jest + .spyOn(PluginScanner.prototype, 'scanRoot') + .mockImplementation(async () => [ + { + location: new URL( + 'file:///somewhere/dynamic-plugins-root/a-dynamic-plugin', + ), + manifest: { + name: 'test', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { + role: 'backend-plugin', + }, + }, + }, + ]); + const mockedModuleLoader = { + logger, + bootstrap: jest.fn(), + load: jest.fn(), + }; + + const backend = createSpecializedBackend({ + defaultServiceFactories: [ + rootLifecycleServiceFactory(), + createServiceFactory({ + service: coreServices.rootConfig, + deps: {}, + async factory({}) { + return await ConfigSources.toConfig({ + async *readConfigData() { + yield { + configs: [ + { + context: 'test', + data: {}, + }, + ], + }; + }, + }); + }, + }), + createServiceFactory({ + service: coreServices.logger, + deps: {}, + async factory({}) { + return logger; + }, + }), + createServiceFactory({ + service: coreServices.rootLogger, + deps: {}, + async factory({}) { + return rootLogger; + }, + }), + dynamicPluginsServiceFactory({ + moduleLoader: _ => mockedModuleLoader, + }), + ], + }); + + await backend.start(); + expect(fromConfigSpier).toHaveBeenCalled(); + expect(applyConfigSpier).toHaveBeenCalled(); + expect(scanRootSpier).toHaveBeenCalled(); + expect(mockedModuleLoader.bootstrap).toHaveBeenCalledWith( + findPaths(__dirname).targetRoot, + ['/somewhere-else/a-dynamic-plugin'], + ); + expect(mockedModuleLoader.load).toHaveBeenCalledWith( + '/somewhere/dynamic-plugins-root/a-dynamic-plugin/dist/index.cjs.js', + ); + }); + }); +}); diff --git a/packages/backend-plugin-manager/src/manager/plugin-manager.ts b/packages/backend-plugin-manager/src/manager/plugin-manager.ts new file mode 100644 index 0000000000..89ea144426 --- /dev/null +++ b/packages/backend-plugin-manager/src/manager/plugin-manager.ts @@ -0,0 +1,265 @@ +/* + * 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 { Config } from '@backstage/config'; +import { + BackendPluginProvider, + BackendDynamicPlugin, + isBackendDynamicPluginInstaller, + DynamicPlugin, +} from './types'; +import { ScannedPluginPackage } from '../scanner'; +import { PluginScanner } from '../scanner/plugin-scanner'; +import { ModuleLoader } from '../loader'; +import { CommonJSModuleLoader } from '../loader/CommonJSModuleLoader'; +import * as url from 'url'; +import { + BackendFeature, + LoggerService, + coreServices, + createServiceFactory, + createServiceRef, +} from '@backstage/backend-plugin-api'; +import { PackageRoles } from '@backstage/cli-node'; +import { findPaths } from '@backstage/cli-common'; +import path from 'path'; +import * as fs from 'fs'; +import { + FeatureDiscoveryService, + featureDiscoveryServiceRef, +} from '@backstage/backend-plugin-api/alpha'; + +/** + * @public + */ +export class PluginManager implements BackendPluginProvider { + static async fromConfig( + config: Config, + logger: LoggerService, + preferAlpha: boolean = false, + mooduleLoader: ModuleLoader = new CommonJSModuleLoader(logger), + ): Promise { + /* eslint-disable-next-line no-restricted-syntax */ + const backstageRoot = findPaths(__dirname).targetRoot; + const scanner = new PluginScanner( + config, + logger, + backstageRoot, + preferAlpha, + ); + const scannedPlugins = await scanner.scanRoot(); + scanner.trackChanges(); + const manager = new PluginManager(logger, scannedPlugins, mooduleLoader); + + const dynamicPluginsPaths = scannedPlugins.map(p => + fs.realpathSync( + path.dirname( + path.dirname( + path.resolve(url.fileURLToPath(p.location), p.manifest.main), + ), + ), + ), + ); + + mooduleLoader.bootstrap(backstageRoot, dynamicPluginsPaths); + + scanner.subscribeToRootDirectoryChange(async () => { + manager._availablePackages = await scanner.scanRoot(); + // TODO: do not store _scannedPlugins again, but instead store a diff of the changes + }); + manager.plugins.push(...(await manager.loadPlugins())); + + return manager; + } + + readonly plugins: DynamicPlugin[]; + private _availablePackages: ScannedPluginPackage[]; + + private constructor( + private readonly logger: LoggerService, + private packages: ScannedPluginPackage[], + private readonly moduleLoader: ModuleLoader, + ) { + this.plugins = []; + this._availablePackages = packages; + } + + get availablePackages(): ScannedPluginPackage[] { + return this._availablePackages; + } + + addBackendPlugin(plugin: BackendDynamicPlugin): void { + this.plugins.push(plugin); + } + + private async loadPlugins(): Promise { + const loadedPlugins: DynamicPlugin[] = []; + + for (const scannedPlugin of this.packages) { + const platform = PackageRoles.getRoleInfo( + scannedPlugin.manifest.backstage.role, + ).platform; + + if ( + platform === 'node' && + scannedPlugin.manifest.backstage.role.includes('-plugin') + ) { + const plugin = await this.loadBackendPlugin(scannedPlugin); + if (plugin !== undefined) { + loadedPlugins.push(plugin); + } + } else { + loadedPlugins.push({ + name: scannedPlugin.manifest.name, + version: scannedPlugin.manifest.version, + role: scannedPlugin.manifest.backstage.role, + platform: 'web', + // TODO(davidfestal): add required front-end plugin information here. + }); + } + } + return loadedPlugins; + } + + private async loadBackendPlugin( + plugin: ScannedPluginPackage, + ): Promise { + const packagePath = url.fileURLToPath( + `${plugin.location}/${plugin.manifest.main}`, + ); + try { + const { dynamicPluginInstaller } = await this.moduleLoader.load( + packagePath, + ); + if (!isBackendDynamicPluginInstaller(dynamicPluginInstaller)) { + this.logger.error( + `dynamic backend plugin '${plugin.manifest.name}' could not be loaded from '${plugin.location}': the module should export a 'const dynamicPluginInstaller: BackendDynamicPluginInstaller' field.`, + ); + return undefined; + } + this.logger.info( + `loaded dynamic backend plugin '${plugin.manifest.name}' from '${plugin.location}'`, + ); + return { + name: plugin.manifest.name, + version: plugin.manifest.version, + platform: 'node', + role: plugin.manifest.backstage.role, + installer: dynamicPluginInstaller, + }; + } catch (error) { + this.logger.error( + `an error occured while loading dynamic backend plugin '${plugin.manifest.name}' from '${plugin.location}'`, + error, + ); + return undefined; + } + } + + backendPlugins(): BackendDynamicPlugin[] { + return this.plugins.filter( + (p): p is BackendDynamicPlugin => p.platform === 'node', + ); + } +} + +/** + * @public + */ +export const dynamicPluginsServiceRef = createServiceRef( + { + id: 'core.dynamicplugins', + scope: 'root', + }, +); + +/** + * @public + */ +export interface DynamicPluginsFactoryOptions { + moduleLoader?(logger: LoggerService): ModuleLoader; +} + +/** + * @public + */ +export const dynamicPluginsServiceFactory = createServiceFactory( + (options?: DynamicPluginsFactoryOptions) => ({ + service: dynamicPluginsServiceRef, + deps: { + config: coreServices.rootConfig, + logger: coreServices.rootLogger, + }, + async factory({ config, logger }) { + if (options?.moduleLoader) { + return await PluginManager.fromConfig( + config, + logger, + true, + options.moduleLoader(logger), + ); + } + return await PluginManager.fromConfig(config, logger, true); + }, + }), +); + +class DynamicPluginsEnabledFeatureDiscoveryService + implements FeatureDiscoveryService +{ + constructor( + private readonly dynamicPlugins: BackendPluginProvider, + private readonly featureDiscoveryService?: FeatureDiscoveryService, + ) {} + + async getBackendFeatures(): Promise<{ features: Array }> { + const staticFeatures = + (await this.featureDiscoveryService?.getBackendFeatures())?.features ?? + []; + + return { + features: [ + ...this.dynamicPlugins + .backendPlugins() + .flatMap((plugin): BackendFeature[] => { + if (plugin.installer.kind === 'new') { + const installed = plugin.installer.install(); + if (Array.isArray(installed)) { + return installed; + } + return [installed]; + } + return []; + }), + ...staticFeatures, + ], + }; + } +} + +/** + * @public + */ +export const dynamicPluginsFeatureDiscoveryServiceFactory = + createServiceFactory({ + service: featureDiscoveryServiceRef, + deps: { + config: coreServices.rootConfig, + dynamicPlugins: dynamicPluginsServiceRef, + }, + factory({ dynamicPlugins }) { + return new DynamicPluginsEnabledFeatureDiscoveryService(dynamicPlugins); + }, + }); diff --git a/packages/backend-plugin-manager/src/manager/types.ts b/packages/backend-plugin-manager/src/manager/types.ts new file mode 100644 index 0000000000..201941292d --- /dev/null +++ b/packages/backend-plugin-manager/src/manager/types.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 { Logger } from 'winston'; +import { Config } from '@backstage/config'; +import { + PluginCacheManager, + PluginDatabaseManager, + PluginEndpointDiscovery, + TokenManager, + UrlReader, +} from '@backstage/backend-common'; +import { Router } from 'express'; +import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks'; +import { IdentityApi } from '@backstage/plugin-auth-node'; +import { PermissionEvaluator } from '@backstage/plugin-permission-common'; +import { + EventBroker, + HttpPostIngressOptions, +} from '@backstage/plugin-events-node'; + +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { PackagePlatform, PackageRole } from '@backstage/cli-node'; +import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { IndexBuilder } from '@backstage/plugin-search-backend-node'; +import { EventsBackend } from '@backstage/plugin-events-backend'; +import { PermissionPolicy } from '@backstage/plugin-permission-node'; + +/** + * @public + * + * @deprecated + * + * Support for the legacy backend system will be removed in the future. + * + * When adding a legacy plugin installer entrypoint in your plugin, + * you should always take the opportunity to also implement support + * for the new backend system if not already done. + * + */ +export type LegacyPluginEnvironment = { + logger: Logger; + cache: PluginCacheManager; + database: PluginDatabaseManager; + config: Config; + reader: UrlReader; + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + permissions: PermissionEvaluator; + scheduler: PluginTaskScheduler; + identity: IdentityApi; + eventBroker: EventBroker; + pluginProvider: BackendPluginProvider; +}; + +/** + * @public + */ +export interface BackendPluginProvider { + backendPlugins(): BackendDynamicPlugin[]; +} + +/** + * @public + */ +export interface BaseDynamicPlugin { + name: string; + version: string; + role: PackageRole; + platform: PackagePlatform; +} + +/** + * @public + */ +export type DynamicPlugin = FrontendDynamicPlugin | BackendDynamicPlugin; + +/** + * @public + */ +export interface FrontendDynamicPlugin extends BaseDynamicPlugin { + platform: 'web'; +} + +/** + * @public + */ +export interface BackendDynamicPlugin extends BaseDynamicPlugin { + platform: 'node'; + installer: BackendDynamicPluginInstaller; +} + +/** + * @public + */ +export type BackendDynamicPluginInstaller = + | LegacyBackendPluginInstaller + | NewBackendPluginInstaller; + +/** + * @public + */ +export interface NewBackendPluginInstaller { + kind: 'new'; + + install(): BackendFeature | BackendFeature[]; +} + +/** + * @public + * @deprecated + * + * Support for the legacy backend system will be removed in the future. + * + * When adding a legacy plugin installer entrypoint in your plugin, + * you should always take the opportunity to also implement support + * for the new backend system if not already done. + * + */ +export interface LegacyBackendPluginInstaller { + kind: 'legacy'; + + router?: { + pluginID: string; + createPlugin(env: LegacyPluginEnvironment): Promise; + }; + + catalog?(builder: CatalogBuilder, env: LegacyPluginEnvironment): void; + scaffolder?(env: LegacyPluginEnvironment): TemplateAction[]; + search?( + indexBuilder: IndexBuilder, + schedule: TaskRunner, + env: LegacyPluginEnvironment, + ): void; + events?( + eventsBackend: EventsBackend, + env: LegacyPluginEnvironment, + ): HttpPostIngressOptions[]; + permissions?: { + policy?: PermissionPolicy; + }; +} + +/** + * @public + */ +export function isBackendDynamicPluginInstaller( + obj: any, +): obj is BackendDynamicPluginInstaller { + return ( + obj !== undefined && + 'kind' in obj && + (obj.kind === 'new' || obj.kind === 'legacy') + ); +} diff --git a/packages/backend-plugin-manager/src/scanner/index.ts b/packages/backend-plugin-manager/src/scanner/index.ts new file mode 100644 index 0000000000..c5574cc38e --- /dev/null +++ b/packages/backend-plugin-manager/src/scanner/index.ts @@ -0,0 +1,17 @@ +/* + * 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 type { ScannedPluginManifest, ScannedPluginPackage } from './types'; diff --git a/packages/backend-plugin-manager/src/scanner/plugin-scanner-watcher.test.ts b/packages/backend-plugin-manager/src/scanner/plugin-scanner-watcher.test.ts new file mode 100644 index 0000000000..5ced3cfbab --- /dev/null +++ b/packages/backend-plugin-manager/src/scanner/plugin-scanner-watcher.test.ts @@ -0,0 +1,365 @@ +/* + * 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 { PluginScanner } from './plugin-scanner'; +import { Logs, MockedLogger } from '../__testUtils__/testUtils'; +import { Config, ConfigReader } from '@backstage/config'; +import path, { join } from 'path'; +import { ScannedPluginPackage } from './types'; +import { mkdtempSync, rmSync } from 'fs'; +import { mkdir, writeFile, rm } from 'fs/promises'; +import { tmpdir } from 'os'; +import waitForExpect from 'wait-for-expect'; + +describe('plugin-scanner', () => { + describe('rootDirectoryWatcher', () => { + let backstageRootDirectory: string; + beforeEach(() => { + backstageRootDirectory = mkdtempSync( + path.join(tmpdir(), 'backstage-plugin-scanner-test'), + ); + }); + afterEach(() => { + if (backstageRootDirectory) { + rmSync(backstageRootDirectory, { recursive: true, force: true }); + } + }); + process.on('beforeExit', () => { + if (backstageRootDirectory) { + rmSync(backstageRootDirectory, { recursive: true, force: true }); + } + }); + + it('watch for config and file system changes', async () => { + const logger = new MockedLogger(); + + await mkdir(join(backstageRootDirectory, 'first-dir')); + await mkdir(join(backstageRootDirectory, 'second-dir')); + + const config: Config = new ConfigReader({ + dynamicPlugins: { + rootDirectory: 'first-dir', + }, + }); + const getOptional = jest.spyOn(config, 'getOptional'); + + let onConfigChange: (() => Promise) | undefined; + const configUnsubscribe = jest.fn(); + config.subscribe = onChange => { + onConfigChange = onChange as any; + return { + unsubscribe: configUnsubscribe, + }; + }; + const pluginScanner = new PluginScanner( + config, + logger, + backstageRootDirectory, + false, + ); + await pluginScanner.trackChanges(); + + expect(onConfigChange).toBeDefined(); + + let scannedPlugins: ScannedPluginPackage[] = + await pluginScanner.scanRoot(); + expect(scannedPlugins).toEqual([]); + + const rootDirectorySubscriber = jest.fn(async () => { + scannedPlugins = await pluginScanner.scanRoot(); + }); + pluginScanner.subscribeToRootDirectoryChange(rootDirectorySubscriber); + + await mkdir( + join(backstageRootDirectory, 'first-dir', 'test-backend-plugin'), + ); + await writeFile( + join( + backstageRootDirectory, + 'first-dir', + 'test-backend-plugin', + 'package.json', + ), + JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + ); + + await waitForExpect(() => { + expect(rootDirectorySubscriber).toHaveBeenCalledTimes(1); + }); + rootDirectorySubscriber.mockClear(); + await waitForExpect(() => { + expect(scannedPlugins).toEqual([ + { + location: new URL( + `file://${backstageRootDirectory}/first-dir/test-backend-plugin`, + ), + manifest: { + backstage: { + role: 'backend-plugin', + }, + main: 'dist/index.cjs.js', + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + }, + }, + ]); + }); + + expect(logger.logs).toEqual({ + infos: [ + { + message: `rootDirectory changed (addDir - ${backstageRootDirectory}/first-dir/test-backend-plugin): scanning plugins again`, + }, + { + message: `rootDirectory changed (add - ${backstageRootDirectory}/first-dir/test-backend-plugin/package.json): scanning plugins again`, + }, + ], + }); + logger.logs = {}; + + getOptional.mockReturnValue({ + rootDirectory: 'second-dir', + }); + await onConfigChange!(); + + await waitForExpect(() => { + expect(rootDirectorySubscriber).toHaveBeenCalledTimes(1); + }); + rootDirectorySubscriber.mockClear(); + await waitForExpect(() => { + expect(scannedPlugins).toEqual([]); + }); + + expect(logger.logs).toEqual({ + infos: [ + { + message: `rootDirectory changed in Config from '${backstageRootDirectory}/first-dir' to '${backstageRootDirectory}/second-dir'`, + }, + ], + }); + logger.logs = {}; + + await mkdir( + join( + backstageRootDirectory, + 'second-dir', + 'second-test-backend-plugin', + ), + ); + await writeFile( + join( + backstageRootDirectory, + 'second-dir', + 'second-test-backend-plugin', + 'package.json', + ), + JSON.stringify({ + name: 'second-test-backend-plugin-dynamic', + version: '1.0.3', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + ); + + await waitForExpect(() => { + expect(rootDirectorySubscriber).toHaveBeenCalledTimes(1); + }); + rootDirectorySubscriber.mockClear(); + await waitForExpect(() => { + expect(scannedPlugins).toEqual([ + { + location: new URL( + `file://${backstageRootDirectory}/second-dir/second-test-backend-plugin`, + ), + manifest: { + backstage: { + role: 'backend-plugin', + }, + main: 'dist/index.cjs.js', + name: 'second-test-backend-plugin-dynamic', + version: '1.0.3', + }, + }, + ]); + }); + + expect(logger.logs).toEqual({ + infos: [ + { + message: `rootDirectory changed (addDir - ${backstageRootDirectory}/second-dir/second-test-backend-plugin): scanning plugins again`, + }, + { + message: `rootDirectory changed (add - ${backstageRootDirectory}/second-dir/second-test-backend-plugin/package.json): scanning plugins again`, + }, + ], + }); + logger.logs = {}; + + const debug = jest.spyOn(logger, 'debug'); + + // Check that not all file changes trigger a new scan of plugins + await mkdir( + join( + backstageRootDirectory, + 'second-dir', + 'second-test-backend-plugin', + 'sub-directory', + ), + ); + await waitForExpect(() => { + expect(debug).toHaveBeenCalledTimes(1); + }); + await writeFile( + join( + backstageRootDirectory, + 'second-dir', + 'second-test-backend-plugin', + 'not-package.json', + ), + 'content', + ); + await waitForExpect(() => { + expect(debug).toHaveBeenCalledTimes(2); + }); + await rm( + join( + backstageRootDirectory, + 'second-dir', + 'second-test-backend-plugin', + 'not-package.json', + ), + ); + await waitForExpect(() => { + expect(debug).toHaveBeenCalledTimes(3); + }); + await rm( + join( + backstageRootDirectory, + 'second-dir', + 'second-test-backend-plugin', + 'sub-directory', + ), + { recursive: true }, + ); + await waitForExpect(() => { + expect(debug).toHaveBeenCalledTimes(4); + }); + + expect(logger.logs).toEqual({ + debugs: [ + { + message: `rootDirectory changed (addDir - ${backstageRootDirectory}/second-dir/second-test-backend-plugin/sub-directory): no need to scan plugins again`, + }, + { + message: `rootDirectory changed (add - ${backstageRootDirectory}/second-dir/second-test-backend-plugin/not-package.json): no need to scan plugins again`, + }, + { + message: `rootDirectory changed (unlink - ${backstageRootDirectory}/second-dir/second-test-backend-plugin/not-package.json): no need to scan plugins again`, + }, + { + message: `rootDirectory changed (unlinkDir - ${backstageRootDirectory}/second-dir/second-test-backend-plugin/sub-directory): no need to scan plugins again`, + }, + ], + }); + logger.logs = {}; + + // Now check that removal of some plugin home directory triggers a new scan of plugins + + await rm( + join( + backstageRootDirectory, + 'second-dir', + 'second-test-backend-plugin', + 'package.json', + ), + ); + await waitForExpect(() => { + expect(rootDirectorySubscriber).toHaveBeenCalledTimes(1); + }); + rootDirectorySubscriber.mockClear(); + await waitForExpect(() => { + expect(scannedPlugins).toEqual([]); + }); + + expect(logger.logs).toEqual({ + infos: [ + { + message: `rootDirectory changed (unlink - ${backstageRootDirectory}/second-dir/second-test-backend-plugin/package.json): scanning plugins again`, + }, + ], + errors: [ + { + message: `failed to load dynamic plugin manifest from '${backstageRootDirectory}/second-dir/second-test-backend-plugin'`, + meta: { + code: 'ENOENT', + errno: -2, + message: `ENOENT: no such file or directory, open '${backstageRootDirectory}/second-dir/second-test-backend-plugin/package.json'`, + name: 'Error', + path: `${backstageRootDirectory}/second-dir/second-test-backend-plugin/package.json`, + syscall: 'open', + }, + }, + ], + }); + logger.logs = {}; + + await rm( + join( + backstageRootDirectory, + 'second-dir', + 'second-test-backend-plugin', + ), + { recursive: true }, + ); + await waitForExpect(() => { + expect(rootDirectorySubscriber).toHaveBeenCalledTimes(1); + }); + rootDirectorySubscriber.mockClear(); + + expect(logger.logs).toEqual({ + infos: [ + { + message: `rootDirectory changed (unlinkDir - ${backstageRootDirectory}/second-dir/second-test-backend-plugin): scanning plugins again`, + }, + ], + }); + logger.logs = {}; + + getOptional.mockReturnValue({ + rootDirectory: '/somewhere-else/second-dir', + }); + await onConfigChange!(); + + expect(logger.logs).toEqual({ + errors: [ + { + message: 'failed to apply new config for dynamic plugins', + meta: { + message: `Dynamic plugins under '/somewhere-else/second-dir' cannot access backstage modules in '${backstageRootDirectory}/node_modules'. +Please add '${backstageRootDirectory}/node_modules' to the 'NODE_PATH' when running the backstage backend.`, + name: 'Error', + }, + }, + ], + }); + }, 120000); + }); +}); diff --git a/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts b/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts new file mode 100644 index 0000000000..25f07baeaa --- /dev/null +++ b/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts @@ -0,0 +1,720 @@ +/* + * 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 { PluginScanner } from './plugin-scanner'; +import mockFs from 'mock-fs'; +import { JsonObject } from '@backstage/types'; +import { Logs, MockedLogger } from '../__testUtils__/testUtils'; +import { ConfigReader } from '@backstage/config'; +import path from 'path'; +import { ScannedPluginPackage } from './types'; + +describe('plugin-scanner', () => { + const env = process.env; + beforeEach(() => { + jest.resetModules(); + }); + + afterEach(() => { + mockFs.restore(); + process.env = env; + }); + + describe('applyConfig', () => { + type TestCase = { + name: string; + backstageRoot?: string; + fileSystem?: any; + config: JsonObject; + environment?: { [key: string]: string }; + expectedLogs?: Logs; + expectedRootDirectory?: string; + expectedError?: string; + }; + + it.each([ + { + name: 'no dynamic plugins', + config: {}, + expectedRootDirectory: undefined, + expectedLogs: { + infos: [ + { + message: "'dynamicPlugins' config entry not found.", + }, + ], + }, + }, + { + name: 'valid config with relative root directory path', + backstageRoot: '/backstageRoot', + fileSystem: { + '/backstageRoot': mockFs.directory({ + items: { + 'dist-dynamic': mockFs.directory(), + }, + }), + }, + config: { + dynamicPlugins: { + rootDirectory: 'dist-dynamic', + }, + }, + expectedRootDirectory: '/backstageRoot/dist-dynamic', + }, + { + name: 'valid config with absolute root directory path inside the backstage root', + backstageRoot: '/backstageRoot', + fileSystem: { + '/backstageRoot': mockFs.directory({ + items: { + 'dist-dynamic': mockFs.directory(), + }, + }), + }, + config: { + dynamicPlugins: { + rootDirectory: '/backstageRoot/dist-dynamic', + }, + }, + expectedRootDirectory: '/backstageRoot/dist-dynamic', + }, + { + name: 'valid config with absolute root directory path outside the backstage root', + backstageRoot: '/backstageRoot', + fileSystem: { + '/somewhere': mockFs.directory({ + items: { + 'dist-dynamic': mockFs.directory(), + }, + }), + }, + config: { + dynamicPlugins: { + rootDirectory: '/somewhere/dist-dynamic', + }, + }, + expectedError: `Dynamic plugins under '/somewhere/dist-dynamic' cannot access backstage modules in '/backstageRoot/node_modules'. +Please add '/backstageRoot/node_modules' to the 'NODE_PATH' when running the backstage backend.`, + }, + { + name: 'valid config with absolute root directory path outside the backstage root but with backstage root included in NODE_PATH', + backstageRoot: '/backstageRoot', + fileSystem: { + '/somewhere': mockFs.directory({ + items: { + 'dist-dynamic': mockFs.directory(), + }, + }), + }, + config: { + dynamicPlugins: { + rootDirectory: '/somewhere/dist-dynamic', + }, + }, + environment: { + NODE_PATH: `/somewhere-else${path.delimiter}/backstageRoot/node_modules${path.delimiter}/anywhere-else`, + }, + expectedRootDirectory: '/somewhere/dist-dynamic', + }, + { + name: 'invalid config: dynamicPlugins not an object', + config: { + dynamicPlugins: 1, + }, + expectedLogs: { + warns: [ + { + message: "'dynamicPlugins' config entry should be an object.", + }, + ], + }, + }, + { + name: 'invalid config: rootDirectory not a string', + config: { + dynamicPlugins: { + rootDirectory: 1, + }, + }, + expectedLogs: { + warns: [ + { + message: + "'dynamicPlugins.rootDirectory' config entry should be a string.", + }, + ], + }, + }, + { + name: 'invalid config: no rootDirectory entry', + config: { + dynamicPlugins: {}, + }, + expectedLogs: { + warns: [ + { + message: + "'dynamicPlugins' config entry does not contain the 'rootDirectory' field.", + }, + ], + }, + }, + { + name: 'valid config pointing to a file instead of a directory', + backstageRoot: '/backstageRoot', + fileSystem: { + '/backstageRoot': mockFs.directory({ + items: { + 'dist-dynamic': mockFs.file(), + }, + }), + }, + config: { + dynamicPlugins: { + rootDirectory: 'dist-dynamic', + }, + }, + expectedError: 'Not a directory', + }, + ])('$name', (tc: TestCase): void => { + if (tc.environment) { + process.env = { + ...env, + ...tc.environment, + }; + } + const logger = new MockedLogger(); + const backstageRoot = tc.backstageRoot ? tc.backstageRoot : ''; + function toTest(): PluginScanner { + return new PluginScanner( + new ConfigReader(tc.config), + logger, + backstageRoot, + ); + } + if (tc.fileSystem) { + mockFs(tc.fileSystem); + } + if (tc.expectedError) { + /* eslint-disable-next-line jest/no-conditional-expect */ + expect(toTest).toThrow(tc.expectedError); + return; + } + const pluginScanner = toTest(); + if (tc.expectedLogs) { + /* eslint-disable-next-line jest/no-conditional-expect */ + expect(logger.logs).toEqual(tc.expectedLogs); + } else { + /* eslint-disable-next-line jest/no-conditional-expect */ + expect(logger.logs).toEqual({}); + } + expect(pluginScanner.rootDirectory).toEqual(tc.expectedRootDirectory); + }); + }); + describe('scanRoot', () => { + type TestCase = { + name: string; + preferAlpha?: boolean; + fileSystem?: any; + expectedLogs?: Logs; + expectedPluginPackages?: ScannedPluginPackage[]; + expectedError?: string; + }; + + it.each([ + { + name: 'dynamic plugins disabled', + expectedPluginPackages: [], + expectedLogs: { + infos: [ + { + message: "'dynamicPlugins' config entry not found.", + }, + ], + }, + }, + { + name: 'manifest found in directory', + fileSystem: { + '/backstageRoot': mockFs.directory({ + items: { + 'dist-dynamic': mockFs.directory({ + items: { + 'test-backend-plugin': mockFs.directory({ + items: { + 'package.json': mockFs.file({ + content: JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + }), + }, + }), + }, + }), + }, + }), + }, + expectedPluginPackages: [ + { + location: new URL( + 'file:///backstageRoot/dist-dynamic/test-backend-plugin', + ), + manifest: { + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }, + }, + ], + }, + { + name: 'backend plugin found in symlink', + fileSystem: { + '/backstageRoot': mockFs.directory({ + items: { + 'dist-dynamic': mockFs.directory({ + items: { + 'test-backend-plugin': mockFs.symlink({ + path: '/somewhere-else/test-backend-plugin-target', + }), + }, + }), + }, + }), + '/somewhere-else': mockFs.directory({ + items: { + 'test-backend-plugin-target': mockFs.directory({ + items: { + 'package.json': mockFs.file({ + content: JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + }), + }, + }), + }, + }), + }, + expectedPluginPackages: [ + { + location: new URL( + 'file:///backstageRoot/dist-dynamic/test-backend-plugin', + ), + manifest: { + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }, + }, + ], + }, + { + name: 'ignored folder child: not a directory', + fileSystem: { + '/backstageRoot': mockFs.directory({ + items: { + 'dist-dynamic': mockFs.directory({ + items: { + 'test-backend-plugin': mockFs.file({}), + }, + }), + }, + }), + }, + expectedPluginPackages: [], + expectedLogs: { + infos: [ + { + message: + "skipping '/backstageRoot/dist-dynamic/test-backend-plugin' since it is not a directory", + }, + ], + }, + }, + { + name: 'ignored folder child symlink: target is not a directory', + fileSystem: { + '/backstageRoot': mockFs.directory({ + items: { + 'dist-dynamic': mockFs.directory({ + items: { + 'test-backend-plugin': mockFs.symlink({ + path: '/somewhere-else/test-backend-plugin-target', + }), + }, + }), + }, + }), + '/somewhere-else': mockFs.directory({ + items: { + 'test-backend-plugin-target': mockFs.file({}), + }, + }), + }, + expectedPluginPackages: [], + expectedLogs: { + infos: [ + { + message: + "skipping '/backstageRoot/dist-dynamic/test-backend-plugin' since it is not a directory", + }, + ], + }, + }, + { + name: 'alpha manifest available but not preferred', + preferAlpha: false, + fileSystem: { + '/backstageRoot': mockFs.directory({ + items: { + 'dist-dynamic': mockFs.directory({ + items: { + 'test-backend-plugin': mockFs.directory({ + items: { + 'package.json': mockFs.file({ + content: JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + }), + alpha: mockFs.directory({ + items: { + 'package.json': mockFs.file({ + content: JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: '../dist/alpha.cjs.js', + }), + }), + }, + }), + }, + }), + }, + }), + }, + }), + }, + expectedPluginPackages: [ + { + location: new URL( + 'file:///backstageRoot/dist-dynamic/test-backend-plugin', + ), + manifest: { + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }, + }, + ], + }, + { + name: 'alpha manifest preferred and found in directory', + preferAlpha: true, + fileSystem: { + '/backstageRoot': mockFs.directory({ + items: { + 'dist-dynamic': mockFs.directory({ + items: { + 'test-backend-plugin': mockFs.directory({ + items: { + 'package.json': mockFs.file({ + content: JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + }), + alpha: mockFs.directory({ + items: { + 'package.json': mockFs.file({ + content: JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: '../dist/alpha.cjs.js', + }), + }), + }, + }), + }, + }), + }, + }), + }, + }), + }, + expectedPluginPackages: [ + { + location: new URL( + 'file:///backstageRoot/dist-dynamic/test-backend-plugin/alpha', + ), + manifest: { + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: '../dist/alpha.cjs.js', + backstage: { role: 'backend-plugin' }, + }, + }, + ], + }, + { + name: 'alpha manifest preferred but skipped because not a directory', + preferAlpha: true, + fileSystem: { + '/backstageRoot': mockFs.directory({ + items: { + 'dist-dynamic': mockFs.directory({ + items: { + 'test-backend-plugin': mockFs.directory({ + items: { + 'package.json': mockFs.file({ + content: JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + }), + alpha: mockFs.file({}), + }, + }), + }, + }), + }, + }), + }, + expectedPluginPackages: [ + { + location: new URL( + 'file:///backstageRoot/dist-dynamic/test-backend-plugin', + ), + manifest: { + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }, + }, + ], + expectedLogs: { + warns: [ + { + message: + "skipping '/backstageRoot/dist-dynamic/test-backend-plugin/alpha' since it is not a directory", + }, + ], + }, + }, + { + name: 'invalid alpha package.json', + preferAlpha: true, + fileSystem: { + '/backstageRoot': mockFs.directory({ + items: { + 'dist-dynamic': mockFs.directory({ + items: { + 'test-backend-plugin': mockFs.directory({ + items: { + 'package.json': mockFs.file({ + content: JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + }), + alpha: mockFs.directory({ + items: { + 'package.json': mockFs.file({ + content: "invalid json content, 1, '", + }), + }, + }), + }, + }), + }, + }), + }, + }), + }, + expectedPluginPackages: [], + expectedLogs: { + errors: [ + { + message: + "failed to load dynamic plugin manifest from '/backstageRoot/dist-dynamic/test-backend-plugin/alpha'", + meta: { + name: 'SyntaxError', + message: 'Unexpected token i in JSON at position 0', + }, + }, + ], + }, + }, + { + name: 'invalid package.json', + fileSystem: { + '/backstageRoot': mockFs.directory({ + items: { + 'dist-dynamic': mockFs.directory({ + items: { + 'test-backend-plugin': mockFs.directory({ + items: { + 'package.json': mockFs.file({ + content: "invalid json content, 1, '", + }), + }, + }), + }, + }), + }, + }), + }, + expectedPluginPackages: [], + expectedLogs: { + errors: [ + { + message: + "failed to load dynamic plugin manifest from '/backstageRoot/dist-dynamic/test-backend-plugin'", + meta: { + name: 'SyntaxError', + message: 'Unexpected token i in JSON at position 0', + }, + }, + ], + }, + }, + { + name: 'missing backstage role in package.json', + fileSystem: { + '/backstageRoot': mockFs.directory({ + items: { + 'dist-dynamic': mockFs.directory({ + items: { + 'test-backend-plugin': mockFs.directory({ + items: { + 'package.json': mockFs.file({ + content: JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + }), + }), + }, + }), + }, + }), + }, + }), + }, + expectedPluginPackages: [], + expectedLogs: { + errors: [ + { + message: + "failed to load dynamic plugin manifest from '/backstageRoot/dist-dynamic/test-backend-plugin'", + meta: { + name: 'Error', + message: "field 'backstage.role' not found in 'package.json'", + }, + }, + ], + }, + }, + { + name: 'missing main field in package.json', + fileSystem: { + '/backstageRoot': mockFs.directory({ + items: { + 'dist-dynamic': mockFs.directory({ + items: { + 'test-backend-plugin': mockFs.directory({ + items: { + 'package.json': mockFs.file({ + content: JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + backstage: { role: 'backend-plugin' }, + }), + }), + }, + }), + }, + }), + }, + }), + }, + expectedPluginPackages: [], + expectedLogs: { + errors: [ + { + message: + "failed to load dynamic plugin manifest from '/backstageRoot/dist-dynamic/test-backend-plugin'", + meta: { + name: 'Error', + message: "field 'main' not found in 'package.json'", + }, + }, + ], + }, + }, + ])('$name', async (tc: TestCase): Promise => { + const logger = new MockedLogger(); + const backstageRoot = '/backstageRoot'; + async function toTest(): Promise { + const pluginScanner = new PluginScanner( + new ConfigReader( + tc.fileSystem + ? { + dynamicPlugins: { + rootDirectory: 'dist-dynamic', + }, + } + : {}, + ), + logger, + backstageRoot, + tc.preferAlpha === undefined ? false : tc.preferAlpha, + ); + return await pluginScanner.scanRoot(); + } + if (tc.fileSystem) { + mockFs(tc.fileSystem); + } + if (tc.expectedError) { + /* eslint-disable-next-line jest/no-conditional-expect */ + expect(toTest).toThrow(tc.expectedError); + return; + } + const plugins = await toTest(); + expect(logger.logs).toEqual(tc.expectedLogs ? tc.expectedLogs : {}); + expect(plugins).toEqual(tc.expectedPluginPackages); + }); + }); +}); diff --git a/packages/backend-plugin-manager/src/scanner/plugin-scanner.ts b/packages/backend-plugin-manager/src/scanner/plugin-scanner.ts new file mode 100644 index 0000000000..bdcf786619 --- /dev/null +++ b/packages/backend-plugin-manager/src/scanner/plugin-scanner.ts @@ -0,0 +1,304 @@ +/* + * 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 { Config } from '@backstage/config'; +import { ScannedPluginPackage, ScannedPluginManifest } from './types'; +import * as fs from 'fs/promises'; +import { Stats, lstatSync } from 'fs'; +import * as chokidar from 'chokidar'; +import * as path from 'path'; +import * as url from 'url'; +import debounce from 'lodash/debounce'; +import { PackagePlatform, PackageRoles } from '@backstage/cli-node'; +import { LoggerService } from '@backstage/backend-plugin-api'; + +export class PluginScanner { + private readonly logger: LoggerService; + private backstageRoot: string; + readonly #config: Config; + private _rootDirectory?: string; + private readonly preferAlpha: boolean; + private configUnsubscribe?: () => void; + private rootDirectoryWatcher?: chokidar.FSWatcher; + private subscribers: (() => void)[] = []; + + constructor( + config: Config, + logger: LoggerService, + backstageRoot: string, + preferAlpha: boolean = false, + ) { + this.backstageRoot = backstageRoot; + this.logger = logger; + this.preferAlpha = preferAlpha; + this.#config = config; + + this.applyConfig(); + } + + subscribeToRootDirectoryChange(subscriber: () => void) { + this.subscribers.push(subscriber); + } + + get rootDirectory(): string | undefined { + return this._rootDirectory; + } + + private applyConfig(): void | never { + const dynamicPlugins = this.#config.getOptional('dynamicPlugins'); + if (!dynamicPlugins) { + this.logger.info("'dynamicPlugins' config entry not found."); + this._rootDirectory = undefined; + return; + } + if (typeof dynamicPlugins !== 'object') { + this.logger.warn("'dynamicPlugins' config entry should be an object."); + this._rootDirectory = undefined; + return; + } + if (!('rootDirectory' in dynamicPlugins)) { + this.logger.warn( + "'dynamicPlugins' config entry does not contain the 'rootDirectory' field.", + ); + this._rootDirectory = undefined; + return; + } + if (typeof dynamicPlugins.rootDirectory !== 'string') { + this.logger.warn( + "'dynamicPlugins.rootDirectory' config entry should be a string.", + ); + this._rootDirectory = undefined; + return; + } + + const dynamicPluginsRootPath = path.isAbsolute(dynamicPlugins.rootDirectory) + ? dynamicPlugins.rootDirectory + : path.resolve(this.backstageRoot, dynamicPlugins.rootDirectory); + + if ( + !path + .dirname(path.normalize(dynamicPluginsRootPath)) + .startsWith(path.normalize(this.backstageRoot)) + ) { + const nodePath = process.env.NODE_PATH; + const backstageNodeModules = path.resolve( + this.backstageRoot, + 'node_modules', + ); + if ( + !nodePath || + !nodePath.split(path.delimiter).includes(backstageNodeModules) + ) { + throw new Error( + `Dynamic plugins under '${dynamicPluginsRootPath}' cannot access backstage modules in '${backstageNodeModules}'.\n` + + `Please add '${backstageNodeModules}' to the 'NODE_PATH' when running the backstage backend.`, + ); + } + } + if (!lstatSync(dynamicPluginsRootPath).isDirectory()) { + throw new Error('Not a directory'); + } + + this._rootDirectory = dynamicPluginsRootPath; + } + + async scanRoot(): Promise { + if (!this._rootDirectory) { + return []; + } + + const dynamicPluginsLocation = this._rootDirectory; + const scannedPlugins: ScannedPluginPackage[] = []; + for (const dirEnt of await fs.readdir(dynamicPluginsLocation, { + withFileTypes: true, + })) { + const pluginDir = dirEnt; + const pluginHome = path.normalize( + path.resolve(dynamicPluginsLocation, pluginDir.name), + ); + if (dirEnt.isSymbolicLink()) { + if (!(await fs.lstat(await fs.readlink(pluginHome))).isDirectory()) { + this.logger.info( + `skipping '${pluginHome}' since it is not a directory`, + ); + continue; + } + } else if (!dirEnt.isDirectory()) { + this.logger.info( + `skipping '${pluginHome}' since it is not a directory`, + ); + continue; + } + + let scannedPlugin: ScannedPluginPackage; + let platform: PackagePlatform; + try { + scannedPlugin = await this.scanDir(pluginHome); + if (!scannedPlugin.manifest.main) { + throw new Error("field 'main' not found in 'package.json'"); + } + if (scannedPlugin.manifest.backstage?.role) { + platform = PackageRoles.getRoleInfo( + scannedPlugin.manifest.backstage.role, + ).platform; + } else { + throw new Error("field 'backstage.role' not found in 'package.json'"); + } + } catch (e) { + this.logger.error( + `failed to load dynamic plugin manifest from '${pluginHome}'`, + e, + ); + continue; + } + + if (platform === 'node') { + if (this.preferAlpha) { + const pluginHomeAlpha = path.resolve(pluginHome, 'alpha'); + if ((await fs.lstat(pluginHomeAlpha)).isDirectory()) { + const backstage = scannedPlugin.manifest.backstage; + try { + scannedPlugin = await this.scanDir(pluginHomeAlpha); + } catch (e) { + this.logger.error( + `failed to load dynamic plugin manifest from '${pluginHomeAlpha}'`, + e, + ); + continue; + } + scannedPlugin.manifest.backstage = backstage; + } else { + this.logger.warn( + `skipping '${pluginHomeAlpha}' since it is not a directory`, + ); + } + } + } + + scannedPlugins.push(scannedPlugin); + } + return scannedPlugins; + } + + private async scanDir(pluginHome: string): Promise { + const manifestFile = path.resolve(pluginHome, 'package.json'); + const content = await fs.readFile(manifestFile); + const manifest: ScannedPluginManifest = JSON.parse(content.toString()); + return { + location: url.pathToFileURL(pluginHome), + manifest: manifest, + }; + } + + async trackChanges(): Promise { + const setupRootDirectoryWatcher = async (): Promise => { + return new Promise((resolve, reject) => { + if (!this._rootDirectory) { + resolve(); + return; + } + const callSubscribers = debounce(() => { + this.subscribers.forEach(s => s()); + }, 500); + let ready = false; + this.rootDirectoryWatcher = chokidar + .watch(this._rootDirectory, { + ignoreInitial: true, + followSymlinks: true, + }) + .on( + 'all', + ( + event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', + eventPath: string, + _: Stats | undefined, + ): void => { + if ( + (['addDir', 'unlinkDir'].includes(event) && + path.dirname(eventPath) === this._rootDirectory) || + (['add', 'unlink', 'change'].includes(event) && + path.dirname(path.dirname(eventPath)) === + this._rootDirectory && + path.basename(eventPath) === 'package.json') + ) { + this.logger.info( + `rootDirectory changed (${event} - ${eventPath}): scanning plugins again`, + ); + callSubscribers(); + } else { + this.logger.debug( + `rootDirectory changed (${event} - ${eventPath}): no need to scan plugins again`, + ); + } + }, + ) + .on('error', (error: Error) => { + this.logger.error( + `error while watching '${this.rootDirectory}'`, + error, + ); + if (!ready) { + reject(error); + } + }) + .on('ready', () => { + ready = true; + resolve(); + }); + }); + }; + + await setupRootDirectoryWatcher(); + if (this.#config.subscribe) { + const { unsubscribe } = this.#config.subscribe( + async (): Promise => { + const oldRootDirectory = this._rootDirectory; + try { + this.applyConfig(); + } catch (e) { + this.logger.error( + 'failed to apply new config for dynamic plugins', + e, + ); + } + if (oldRootDirectory !== this._rootDirectory) { + this.logger.info( + `rootDirectory changed in Config from '${oldRootDirectory}' to '${this._rootDirectory}'`, + ); + this.subscribers.forEach(s => s()); + if (this.rootDirectoryWatcher) { + await this.rootDirectoryWatcher.close(); + } + await setupRootDirectoryWatcher(); + } + }, + ); + this.configUnsubscribe = unsubscribe; + } + } + + async untrackChanges() { + if (this.rootDirectoryWatcher) { + this.rootDirectoryWatcher.close(); + } + if (this.configUnsubscribe) { + this.configUnsubscribe(); + } + } + + destructor() { + this.untrackChanges(); + } +} diff --git a/packages/backend-plugin-manager/src/scanner/types.ts b/packages/backend-plugin-manager/src/scanner/types.ts new file mode 100644 index 0000000000..b456ffeea1 --- /dev/null +++ b/packages/backend-plugin-manager/src/scanner/types.ts @@ -0,0 +1,34 @@ +/* + * 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 { BackstagePackageJson } from '@backstage/cli-node'; + +/** + * @public + */ +export interface ScannedPluginPackage { + location: URL; + manifest: ScannedPluginManifest; +} + +/** + * @public + */ +export type ScannedPluginManifest = BackstagePackageJson & + Required> & + Required> & { + backstage: Required; + }; diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md index 05e51c661c..f3cd349f9f 100644 --- a/packages/backend-tasks/CHANGELOG.md +++ b/packages/backend-tasks/CHANGELOG.md @@ -1,5 +1,57 @@ # @backstage/backend-tasks +## 0.5.8-next.2 + +### Patch Changes + +- 814feeed7343: Update to handle invalid luxon values +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.5.8-next.1 + +### Patch Changes + +- 62f448edb0b5: Use `readDurationFromConfig` from the config package +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.5.7-next.0 + +### Patch Changes + +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.5.5 + +### Patch Changes + +- dfd1b6b2fc33: Make `readTaskScheduleDefinitionFromConfig` properly handle bad inputs +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.5.5-next.2 + +### Patch Changes + +- dfd1b6b2fc33: Make `readTaskScheduleDefinitionFromConfig` properly handle bad inputs +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + ## 0.5.5-next.1 ### Patch Changes diff --git a/packages/backend-tasks/catalog-info.yaml b/packages/backend-tasks/catalog-info.yaml new file mode 100644 index 0000000000..f9b81066e6 --- /dev/null +++ b/packages/backend-tasks/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-backend-tasks + title: '@backstage/backend-tasks' + description: Common distributed task management library for Backstage backends +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/packages/backend-tasks/migrations/20210928160613_init.js b/packages/backend-tasks/migrations/20210928160613_init.js index 21d80ae24c..f9900cab11 100644 --- a/packages/backend-tasks/migrations/20210928160613_init.js +++ b/packages/backend-tasks/migrations/20210928160613_init.js @@ -26,7 +26,7 @@ exports.up = async function up(knex) { await knex.schema.createTable('backstage_backend_tasks__tasks', table => { table.comment('Tasks used for scheduling work on multiple workers'); table - .text('id') + .string('id') .primary() .notNullable() .comment('The unique ID of this particular task'); diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index 9dd2c9e557..ecc4bf3d35 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.5-next.1", + "version": "0.5.8-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 7e471a1c16..1d701c971d 100644 --- a/packages/backend-tasks/src/migrations.test.ts +++ b/packages/backend-tasks/src/migrations.test.ts @@ -43,7 +43,7 @@ jest.setTimeout(60_000); describe('migrations', () => { const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['POSTGRES_13', 'POSTGRES_9', 'MYSQL_8', 'SQLITE_3'], }); it.each(databases.eachSupportedId())( diff --git a/packages/backend-tasks/src/setupTests.ts b/packages/backend-tasks/src/setupTests.ts index d3232290a7..5245d37c26 100644 --- a/packages/backend-tasks/src/setupTests.ts +++ b/packages/backend-tasks/src/setupTests.ts @@ -14,4 +14,7 @@ * limitations under the License. */ -export {}; +import { Settings } from 'luxon'; + +// TS still thinks that methods can return null / placeholders, but we still want to throw as soon as possible when things go wrong +Settings.throwOnInvalid = true; diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts index 253d7869df..b9ac80cb47 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts @@ -112,9 +112,15 @@ export function parseDuration( return frequency.cron; } - if (Duration.isDuration(frequency)) { - return frequency.toISO(); + const parsed = Duration.isDuration(frequency) + ? frequency + : Duration.fromObject(frequency); + + if (!parsed.isValid) { + throw new Error( + `Invalid duration, ${parsed.invalidReason}: ${parsed.invalidExplanation}`, + ); } - return Duration.fromObject(frequency).toISO(); + return parsed.toISO()!; } diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.test.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.test.ts index 5b5d592b7b..efbad2e8b6 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.test.ts @@ -42,6 +42,7 @@ describe('PluginTaskSchedulerJanitor', () => { 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3', + 'MYSQL_8', ], }); diff --git a/packages/backend-tasks/src/tasks/TaskScheduler.test.ts b/packages/backend-tasks/src/tasks/TaskScheduler.test.ts index 89eff433db..6aa77dd7fe 100644 --- a/packages/backend-tasks/src/tasks/TaskScheduler.test.ts +++ b/packages/backend-tasks/src/tasks/TaskScheduler.test.ts @@ -25,7 +25,7 @@ jest.setTimeout(60_000); describe('TaskScheduler', () => { const logger = getVoidLogger(); const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3', 'MYSQL_8'], }); async function createDatabase( diff --git a/packages/backend-tasks/src/tasks/TaskWorker.test.ts b/packages/backend-tasks/src/tasks/TaskWorker.test.ts index fa2577ce53..70be2bf415 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.test.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.test.ts @@ -28,7 +28,7 @@ jest.setTimeout(60_000); describe('TaskWorker', () => { const logger = getVoidLogger(); const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3', 'MYSQL_8'], }); beforeEach(() => { @@ -47,8 +47,8 @@ describe('TaskWorker', () => { const settings: TaskSettingsV2 = { version: 2, cadence: '*/2 * * * * *', - initialDelayDuration: Duration.fromObject({ seconds: 1 }).toISO(), - timeoutAfterDuration: Duration.fromObject({ minutes: 1 }).toISO(), + initialDelayDuration: Duration.fromObject({ seconds: 1 }).toISO()!, + timeoutAfterDuration: Duration.fromObject({ minutes: 1 }).toISO()!, }; const worker = new TaskWorker('task1', fn, knex, logger); @@ -131,7 +131,7 @@ describe('TaskWorker', () => { version: 2, initialDelayDuration: undefined, cadence: '* * * * * *', - timeoutAfterDuration: Duration.fromMillis(60000).toISO(), + timeoutAfterDuration: Duration.fromMillis(60000).toISO()!, }; const checkFrequency = Duration.fromObject({ milliseconds: 100 }); const worker = new TaskWorker('task1', fn, knex, logger, checkFrequency); @@ -154,7 +154,7 @@ describe('TaskWorker', () => { version: 2, initialDelayDuration: undefined, cadence: '* * * * * *', - timeoutAfterDuration: Duration.fromMillis(60000).toISO(), + timeoutAfterDuration: Duration.fromMillis(60000).toISO()!, }; const checkFrequency = Duration.fromObject({ milliseconds: 100 }); const worker = new TaskWorker('task1', fn, knex, logger, checkFrequency); @@ -179,7 +179,7 @@ describe('TaskWorker', () => { version: 2, initialDelayDuration: undefined, cadence: '* * * * * *', - timeoutAfterDuration: Duration.fromMillis(60000).toISO(), + timeoutAfterDuration: Duration.fromMillis(60000).toISO()!, }; const worker = new TaskWorker('task1', fn, knex, logger); @@ -235,7 +235,7 @@ describe('TaskWorker', () => { version: 2, initialDelayDuration: undefined, cadence: '* * * * * *', - timeoutAfterDuration: Duration.fromMillis(60000).toISO(), + timeoutAfterDuration: Duration.fromMillis(60000).toISO()!, }; const worker1 = new TaskWorker('task1', fn, knex, logger); diff --git a/packages/backend-tasks/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts index 483fb1a02d..a03d1ec058 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.ts @@ -175,11 +175,15 @@ export class TaskWorker { const time = new CronTime(settings.cadence) .sendAt() .minus({ seconds: 1 }) // immediately, if "* * * * * *" - .toUTC() - .toISO(); - startAt = this.knex.client.config.client.includes('sqlite3') - ? this.knex.raw('datetime(?)', [time]) - : this.knex.raw(`?`, [time]); + .toUTC(); + + if (this.knex.client.config.client.includes('sqlite3')) { + startAt = this.knex.raw('datetime(?)', [time.toISO()]); + } else if (this.knex.client.config.client.includes('mysql')) { + startAt = this.knex.raw(`?`, [time.toSQL({ includeOffset: false })]); + } else { + startAt = this.knex.raw(`?`, [time.toISO()]); + } } else { startAt = this.knex.fn.now(); } @@ -279,11 +283,16 @@ export class TaskWorker { let nextRun: Knex.Raw; if (isCron) { - const time = new CronTime(settings.cadence).sendAt().toUTC().toISO(); + const time = new CronTime(settings.cadence).sendAt().toUTC(); this.logger.debug(`task: ${this.taskId} will next occur around ${time}`); - nextRun = this.knex.client.config.client.includes('sqlite3') - ? this.knex.raw('datetime(?)', [time]) - : this.knex.raw(`?`, [time]); + + if (this.knex.client.config.client.includes('sqlite3')) { + nextRun = this.knex.raw('datetime(?)', [time.toISO()]); + } else if (this.knex.client.config.client.includes('mysql')) { + nextRun = this.knex.raw(`?`, [time.toSQL({ includeOffset: false })]); + } else { + nextRun = this.knex.raw(`?`, [time.toISO()]); + } } 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 21f51954b5..c52d59b016 100644 --- a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.test.ts +++ b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.test.ts @@ -85,7 +85,7 @@ describe('readTaskScheduleDefinitionFromConfig', () => { }); expect(() => readTaskScheduleDefinitionFromConfig(config)).toThrow( - 'HumanDuration needs at least one of', + "Failed to read duration from config at 'frequency', Error: Needs one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'", ); }); @@ -98,7 +98,7 @@ describe('readTaskScheduleDefinitionFromConfig', () => { }); expect(() => readTaskScheduleDefinitionFromConfig(config)).toThrow( - "Unable to convert config value for key 'frequency.minutes' in 'mock-config' to a number", + "Failed to read duration from config, Error: Unable to convert config value for key 'frequency.minutes' in 'mock-config' to a number", ); }); @@ -112,7 +112,7 @@ describe('readTaskScheduleDefinitionFromConfig', () => { }); expect(() => readTaskScheduleDefinitionFromConfig(config)).toThrow( - 'HumanDuration does not contain properties: invalid', + "Failed to read duration from config at 'frequency', Error: Unknown property 'invalid'; expected one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'", ); }); diff --git a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts index b31cf68f74..8053936263 100644 --- a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts +++ b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts @@ -14,54 +14,11 @@ * limitations under the License. */ -import { Config } from '@backstage/config'; +import { Config, readDurationFromConfig } from '@backstage/config'; import { HumanDuration } from '@backstage/types'; import { TaskScheduleDefinition } from './types'; import { Duration } from 'luxon'; -const propsOfHumanDuration = [ - 'years', - 'months', - 'weeks', - 'days', - 'hours', - 'minutes', - 'seconds', - 'milliseconds', -]; - -function convertToHumanDuration(config: Config, key: string): HumanDuration { - // Ensures that the root is an object - const root = config.getConfig(key); - - const result: Record = {}; - let found = false; - for (const prop of propsOfHumanDuration) { - const value = root.getOptionalNumber(prop); - if (value !== undefined) { - result[prop] = value; - found = true; - } - } - - if (!found) { - throw new Error( - `HumanDuration needs at least one of: ${propsOfHumanDuration}`, - ); - } - - const invalidProps = root - .keys() - .filter(prop => !propsOfHumanDuration.includes(prop)); - if (invalidProps.length > 0) { - throw new Error( - `HumanDuration does not contain properties: ${invalidProps}`, - ); - } - - return result as HumanDuration; -} - function readDuration(config: Config, key: string): Duration | HumanDuration { if (typeof config.get(key) === 'string') { const value = config.getString(key); @@ -72,7 +29,7 @@ function readDuration(config: Config, key: string): Duration | HumanDuration { return duration; } - return convertToHumanDuration(config, key); + return readDurationFromConfig(config, { key }); } function readCronOrDuration( diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 78a8e7cd6d..ea0ffee7c8 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,82 @@ # @backstage/backend-test-utils +## 0.2.3-next.2 + +### Patch Changes + +- 58cb5e5cea7b: Introduced a new utility for testing service factories, `ServiceFactoryTester`. +- Updated dependencies + - @backstage/backend-app-api@0.5.3-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/types@1.1.0 + +## 0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-app-api@0.5.3-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/types@1.1.0 + +## 0.2.2-next.0 + +### Patch Changes + +- 9fb3b5373c45: Extended `mockService` to also include mocked variants, for example `mockServices.lifecycle.mock()`. The returned mocked implementation will have a `factory` property which is a service factory for itself. You can also pass a partial implementation of the service to the mock function to use a mock implementation of specific methods. +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-app-api@0.5.2-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + +## 0.2.0 + +### Minor Changes + +- b9c57a4f857e: **BREAKING**: Renamed `mockServices.config` to `mockServices.rootConfig`. +- a6d7983f349c: **BREAKING**: Removed the `services` option from `createBackend`. Service factories are now `BackendFeature`s and should be installed with `backend.add(...)` instead. The following should be migrated: + + ```ts + const backend = createBackend({ services: [myCustomServiceFactory] }); + ``` + + To instead pass the service factory via `backend.add(...)`: + + ```ts + const backend = createBackend(); + backend.add(customRootLoggerServiceFactory); + ``` + +### Patch Changes + +- ae9304818136: Add needed constants and constructs to support PostgreSQL version 14 as test database +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-app-api@0.5.0 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + +## 0.2.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.5.0-next.2 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + ## 0.2.0-next.1 ### Minor Changes diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 1cb7a83a0e..43e671e440 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -3,6 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + import { Backend } from '@backstage/backend-app-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { CacheService } from '@backstage/backend-plugin-api'; @@ -18,7 +20,10 @@ 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 { RootHttpRouterFactoryOptions } from '@backstage/backend-app-api'; +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 { ServiceRef } from '@backstage/backend-plugin-api'; @@ -34,11 +39,19 @@ export namespace mockServices { export namespace cache { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace database { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace httpRouter { @@ -46,6 +59,10 @@ export namespace mockServices { factory: ( options?: HttpRouterFactoryOptions | undefined, ) => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export function identity(): IdentityService; @@ -53,21 +70,37 @@ export namespace mockServices { export namespace identity { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace lifecycle { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace logger { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace permissions { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export function rootConfig(options?: rootConfig.Options): RootConfigService; @@ -83,9 +116,24 @@ export namespace mockServices { ) => ServiceFactory; } // (undocumented) + export namespace rootHttpRouter { + const // (undocumented) + factory: ( + options?: RootHttpRouterFactoryOptions | undefined, + ) => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; + } + // (undocumented) export namespace rootLifecycle { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export function rootLogger(options?: rootLogger.Options): LoggerService; @@ -99,11 +147,19 @@ export namespace mockServices { factory: ( options?: Options | undefined, ) => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace scheduler { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export function tokenManager(): TokenManagerService; @@ -111,14 +167,56 @@ export namespace mockServices { export namespace tokenManager { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace urlReader { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } } +// @public +export class ServiceFactoryTester { + static from( + subject: + | ServiceFactory + | (() => ServiceFactory), + options?: ServiceFactoryTesterOptions, + ): ServiceFactoryTester; + get( + ...args: 'root' extends TScope ? [] : [pluginId?: string] + ): Promise; + getService( + service: ServiceRef, + ...args: 'root' extends TGetScope ? [] : [pluginId?: string] + ): Promise; +} + +// @public +export interface ServiceFactoryTesterOptions { + dependencies?: Array ServiceFactory)>; +} + +// @public (undocumented) +export type ServiceMock = { + factory: ServiceFactory; +} & { + [Key in keyof TService]: TService[Key] extends ( + this: infer This, + ...args: infer Args + ) => infer Return + ? TService[Key] & jest.MockInstance + : TService[Key]; +}; + // @public export function setupRequestMockHandlers(worker: { listen: (t: any) => void; @@ -127,11 +225,8 @@ export function setupRequestMockHandlers(worker: { }): void; // @public (undocumented) -export function startTestBackend< - TServices extends any[], - TExtensionPoints extends any[], ->( - options: TestBackendOptions, +export function startTestBackend( + options: TestBackendOptions, ): Promise; // @public (undocumented) @@ -140,10 +235,7 @@ export interface TestBackend extends Backend { } // @public (undocumented) -export interface TestBackendOptions< - TServices extends any[], - TExtensionPoints extends any[], -> { +export interface TestBackendOptions { // (undocumented) extensionPoints?: readonly [ ...{ @@ -154,16 +246,13 @@ export interface TestBackendOptions< }, ]; // (undocumented) - features?: BackendFeature[]; - // (undocumented) - services?: readonly [ - ...{ - [index in keyof TServices]: - | ServiceFactory - | (() => ServiceFactory) - | [ServiceRef, Partial]; - }, - ]; + features?: Array< + | BackendFeature + | (() => BackendFeature) + | Promise<{ + default: BackendFeature | (() => BackendFeature); + }> + >; } // @public diff --git a/packages/backend-test-utils/catalog-info.yaml b/packages/backend-test-utils/catalog-info.yaml new file mode 100644 index 0000000000..396f190ef4 --- /dev/null +++ b/packages/backend-test-utils/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-backend-test-utils + title: '@backstage/backend-test-utils' + description: Test helpers library for Backstage backends +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index beebf06624..8cdbcc7aca 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-test-utils", "description": "Test helpers library for Backstage backends", - "version": "0.2.0-next.1", + "version": "0.2.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -57,6 +57,9 @@ "testcontainers": "^8.1.2", "uuid": "^8.0.0" }, + "peerDependencies": { + "@types/jest": "*" + }, "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", diff --git a/packages/backend-test-utils/src/next/services/index.ts b/packages/backend-test-utils/src/next/services/index.ts index f562150efa..97a22567fd 100644 --- a/packages/backend-test-utils/src/next/services/index.ts +++ b/packages/backend-test-utils/src/next/services/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { mockServices } from './mockServices'; +export { mockServices, type ServiceMock } from './mockServices'; diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 0455b9f1ae..22fba38fda 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -31,6 +31,7 @@ import { lifecycleServiceFactory, loggerServiceFactory, permissionsServiceFactory, + rootHttpRouterServiceFactory, rootLifecycleServiceFactory, schedulerServiceFactory, urlReaderServiceFactory, @@ -40,6 +41,7 @@ import { JsonObject } from '@backstage/types'; import { MockIdentityService } from './MockIdentityService'; import { MockRootLoggerService } from './MockRootLoggerService'; +/** @internal */ function simpleFactory< TService, TScope extends 'root' | 'plugin', @@ -57,6 +59,44 @@ function simpleFactory< })) as (...options: TOptions) => ServiceFactory; } +/** @public */ +export type ServiceMock = { + factory: ServiceFactory; +} & { + [Key in keyof TService]: TService[Key] extends ( + this: infer This, + ...args: infer Args + ) => infer Return + ? TService[Key] & jest.MockInstance + : TService[Key]; +}; + +/** @internal */ +function simpleMock( + ref: ServiceRef, + mockFactory: () => jest.Mocked, +): (partialImpl?: Partial) => ServiceMock { + return partialImpl => { + const mock = mockFactory(); + if (partialImpl) { + for (const [key, impl] of Object.entries(partialImpl)) { + if (typeof impl === 'function') { + (mock as any)[key].mockImplementation(impl); + } else { + (mock as any)[key] = impl; + } + } + } + return Object.assign(mock, { + factory: createServiceFactory({ + service: ref, + deps: {}, + factory: () => mock, + })(), + }) as ServiceMock; + }; +} + /** * @public */ @@ -79,6 +119,13 @@ export namespace mockServices { }; export const factory = simpleFactory(coreServices.rootLogger, rootLogger); + export const mock = simpleMock(coreServices.rootLogger, () => ({ + child: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + })); } export function tokenManager(): TokenManagerService { @@ -98,6 +145,10 @@ export namespace mockServices { coreServices.tokenManager, tokenManager, ); + export const mock = simpleMock(coreServices.tokenManager, () => ({ + authenticate: jest.fn(), + getToken: jest.fn(), + })); } export function identity(): IdentityService { @@ -105,6 +156,9 @@ export namespace mockServices { } export namespace identity { export const factory = simpleFactory(coreServices.identity, identity); + export const mock = simpleMock(coreServices.identity, () => ({ + getIdentity: jest.fn(), + })); } // TODO(Rugvip): Not all core services have implementations available here yet. @@ -112,29 +166,77 @@ export namespace mockServices { // re-implement functioning mock versions here. export namespace cache { export const factory = cacheServiceFactory; + export const mock = simpleMock(coreServices.cache, () => ({ + delete: jest.fn(), + get: jest.fn(), + set: jest.fn(), + withOptions: jest.fn(), + })); } export namespace database { export const factory = databaseServiceFactory; + export const mock = simpleMock(coreServices.database, () => ({ + getClient: jest.fn(), + })); } export namespace httpRouter { export const factory = httpRouterServiceFactory; + export const mock = simpleMock(coreServices.httpRouter, () => ({ + use: jest.fn(), + })); + } + export namespace rootHttpRouter { + export const factory = rootHttpRouterServiceFactory; + export const mock = simpleMock(coreServices.rootHttpRouter, () => ({ + use: jest.fn(), + })); } export namespace lifecycle { export const factory = lifecycleServiceFactory; + export const mock = simpleMock(coreServices.lifecycle, () => ({ + addShutdownHook: jest.fn(), + addStartupHook: jest.fn(), + })); } export namespace logger { export const factory = loggerServiceFactory; + export const mock = simpleMock(coreServices.logger, () => ({ + child: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + })); } export namespace permissions { export const factory = permissionsServiceFactory; + export const mock = simpleMock(coreServices.permissions, () => ({ + authorize: jest.fn(), + authorizeConditional: jest.fn(), + })); } export namespace rootLifecycle { export const factory = rootLifecycleServiceFactory; + export const mock = simpleMock(coreServices.rootLifecycle, () => ({ + addShutdownHook: jest.fn(), + addStartupHook: jest.fn(), + })); } export namespace scheduler { export const factory = schedulerServiceFactory; + export const mock = simpleMock(coreServices.scheduler, () => ({ + createScheduledTaskRunner: jest.fn(), + getScheduledTasks: jest.fn(), + scheduleTask: jest.fn(), + triggerTask: jest.fn(), + })); } export namespace urlReader { export const factory = urlReaderServiceFactory; + export const mock = simpleMock(coreServices.urlReader, () => ({ + readTree: jest.fn(), + readUrl: jest.fn(), + search: jest.fn(), + })); } } diff --git a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts new file mode 100644 index 0000000000..14dd427ecf --- /dev/null +++ b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts @@ -0,0 +1,145 @@ +/* + * 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 { + coreServices, + createServiceFactory, + createServiceRef, +} from '@backstage/backend-plugin-api'; +import { ServiceFactoryTester } from './ServiceFactoryTester'; + +const rootServiceRef = createServiceRef({ id: 'a', scope: 'root' }); +const pluginServiceRef = createServiceRef({ id: 'b', scope: 'plugin' }); +const sharedPluginServiceRef = createServiceRef({ + id: 'c', + scope: 'plugin', +}); + +const rootFactory = createServiceFactory({ + service: rootServiceRef, + deps: {}, + factory: async () => 'root', +}); + +const pluginFactory = createServiceFactory({ + service: pluginServiceRef, + deps: { plugin: coreServices.pluginMetadata }, + factory: async ({ plugin }) => `${plugin.getId()}-plugin`, +}); + +const sharedPluginFactory = createServiceFactory({ + service: sharedPluginServiceRef, + deps: { plugin: coreServices.pluginMetadata }, + createRootContext() { + return { counter: 0 }; + }, + factory: async ({ plugin }, state) => { + state.counter += 1; + return `${plugin.getId()}-${state.counter}-plugin`; + }, +}); + +describe('ServiceFactoryTester', () => { + it('should test a root service factory', async () => { + const tester = ServiceFactoryTester.from(rootFactory); + + await expect(tester.get()).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'); + }); + + 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'); + + 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'); + }); + + it('should use dependencies', async () => { + const tester = ServiceFactoryTester.from( + createServiceFactory({ + service: createServiceRef({ id: 'concat' }), + deps: { root: rootServiceRef, plugin: pluginServiceRef }, + factory: async ({ root, plugin }) => `${root}, ${plugin}`, + }), + { dependencies: [rootFactory, pluginFactory()] }, + ); + + await expect(tester.get('x')).resolves.toBe('root, x-plugin'); + }); + + it('should use dependencies with root context', async () => { + const tester = ServiceFactoryTester.from( + createServiceFactory({ + service: createServiceRef({ id: 'concat' }), + deps: { shared: sharedPluginServiceRef, plugin: pluginServiceRef }, + factory: async ({ shared, plugin }) => `${shared}, ${plugin}`, + }), + { 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'); + }); + + it('should prioritize the subject implementation', async () => { + const tester = ServiceFactoryTester.from( + createServiceFactory({ + service: rootServiceRef, + deps: {}, + factory: async () => 'other-root', + }), + { dependencies: [rootFactory] }, + ); + + await expect(tester.get()).resolves.toBe('other-root'); + }); + + it('should throw on missing dependencies', async () => { + const tester = ServiceFactoryTester.from( + createServiceFactory({ + service: pluginServiceRef, + deps: { root: rootServiceRef }, + factory: async () => 'plugin', + }), + ); + + await expect(tester.get('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 new file mode 100644 index 0000000000..0555ab10a4 --- /dev/null +++ b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts @@ -0,0 +1,117 @@ +/* + * 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 { ServiceFactory, ServiceRef } from '@backstage/backend-plugin-api'; +import { defaultServiceFactories } from './TestBackend'; +// Direct internal import to avoid duplication. +// This is a relative import in order to make sure that the implementation is duplicated +// rather than leading to an import from @backstage/backend-app-api. +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { ServiceRegistry } from '../../../../backend-app-api/src/wiring/ServiceRegistry'; + +/** + * Options for {@link ServiceFactoryTester}. + * @public + */ +export interface ServiceFactoryTesterOptions { + /** + * Additional service factories to make available as dependencies. + * + * @remarks + * + * 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)>; +} + +/** + * A utility to help test service factories in isolation. + * + * @public + */ +export class ServiceFactoryTester { + readonly #subject: ServiceRef; + readonly #registry: ServiceRegistry; + + /** + * Creates a new {@link ServiceFactoryTester} used to test the provided subject. + * + * @param subject - The service factory to test. + * @param options - Additional options + * @returns A new tester instance for the provided subject. + */ + static from( + subject: + | ServiceFactory + | (() => ServiceFactory), + options?: ServiceFactoryTesterOptions, + ) { + const subjectFactory = typeof subject === 'function' ? subject() : subject; + const registry = ServiceRegistry.create([ + ...defaultServiceFactories, + ...(options?.dependencies?.map(f => + typeof f === 'function' ? f() : f, + ) ?? []), + subjectFactory, + ]); + return new ServiceFactoryTester(subjectFactory.service, registry); + } + + private constructor( + subject: ServiceRef, + registry: ServiceRegistry, + ) { + this.#subject = subject; + this.#registry = registry; + } + + /** + * Returns the service instance for the subject. + * + * @remarks + * + * If the subject is a plugin scoped service factory a plugin ID + * can be provided to instantiate the service for a specific plugin. + * + * By default the plugin ID 'test' is used. + */ + async get( + ...args: 'root' extends TScope ? [] : [pluginId?: string] + ): Promise { + const [pluginId] = args; + return this.#registry.get(this.#subject, pluginId ?? 'test')!; + } + + /** + * Return the service instance for any of the provided dependencies or built-in services. + * + * @remarks + * + * A plugin ID can optionally be provided for plugin scoped services, otherwise the plugin ID 'test' is used. + */ + async getService( + service: ServiceRef, + ...args: 'root' extends TGetScope ? [] : [pluginId?: string] + ): Promise { + const [pluginId] = args; + const instance = await this.#registry.get(service, pluginId ?? 'test'); + if (instance === undefined) { + throw new Error(`Service '${service.id}' not found`); + } + return instance; + } +} 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 cc916d4309..b127712927 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts @@ -31,7 +31,6 @@ import { startTestBackend } from './TestBackend'; let globalTestBackendHasBeenStopped = false; beforeAll(async () => { await startTestBackend({ - services: [], features: [ createBackendModule({ moduleId: 'test.module', @@ -46,7 +45,7 @@ beforeAll(async () => { }, }); }, - })(), + }), ], }); }); @@ -68,17 +67,37 @@ describe('TestBackend', () => { const extensionPoint5 = createExtensionPoint({ id: 'b5' }); await expect( startTestBackend({ - services: [ + features: [ // @ts-expect-error [extensionPoint1, { a: 'a' }], - [serviceRef, { a: 'a' }], - [serviceRef, { a: 'a', b: 'b' }], - // @ts-expect-error - [serviceRef, { c: 'c' }], - // @ts-expect-error - [serviceRef, { a: 'a', c: 'c' }], - // @ts-expect-error - [serviceRef, { a: 'a', b: 'b', c: 'c' }], + createServiceFactory(() => ({ + service: serviceRef, + deps: {}, + // @ts-expect-error + factory: async () => ({ a: 'a' }), + })), + createServiceFactory(() => ({ + service: serviceRef, + deps: {}, + factory: async () => ({ a: 'a', b: 'b' }), + })), + createServiceFactory(() => ({ + service: serviceRef, + deps: {}, + // @ts-expect-error + factory: async () => ({ c: 'c' }), + })), + createServiceFactory(() => ({ + service: serviceRef, + deps: {}, + // @ts-expect-error + factory: async () => ({ a: 'a', c: 'c' }), + })), + createServiceFactory(() => ({ + service: serviceRef, + deps: {}, + factory: async () => ({ a: 'a', b: 'b', c: 'c' }), + })), ], extensionPoints: [ // @ts-expect-error @@ -124,8 +143,7 @@ describe('TestBackend', () => { }); await startTestBackend({ - services: [sf], - features: [testModule()], + features: [testModule(), sf()], }); expect(testFn).toHaveBeenCalledWith('winning'); @@ -150,7 +168,6 @@ describe('TestBackend', () => { }); const backend = await startTestBackend({ - services: [], features: [testModule()], }); @@ -192,7 +209,6 @@ describe('TestBackend', () => { }); await startTestBackend({ - services: [], features: [testPlugin()], }); }); @@ -220,4 +236,89 @@ describe('TestBackend', () => { expect(res.status).toEqual(200); expect(res.body).toEqual({ message: 'pong' }); }); + + it('should provide extension point implementations', async () => { + expect.assertions(3); + + const extensionPointA = createExtensionPoint({ id: 'a' }); + const extensionPointB = createExtensionPoint({ id: 'b' }); + await expect( + startTestBackend({ + extensionPoints: [ + [extensionPointA, 'a'], + [extensionPointB, 'b'], + ], + features: [ + createBackendModule({ + pluginId: 'testA', + moduleId: 'test', + register(reg) { + reg.registerInit({ + deps: { ext: extensionPointA }, + async init({ ext }) { + expect(ext).toBe('a'); + }, + }); + }, + }), + createBackendModule({ + pluginId: 'testB', + moduleId: 'test', + register(reg) { + reg.registerInit({ + deps: { ext: extensionPointB }, + async init({ ext }) { + expect(ext).toBe('b'); + }, + }); + }, + }), + ], + }), + ).resolves.not.toBeUndefined(); + }); + + it('should reject extension point used by multiple plugins', async () => { + const extensionPointA = createExtensionPoint({ id: 'a' }); + await expect( + startTestBackend({ + extensionPoints: [[extensionPointA, 'a']], + features: [ + createBackendModule({ + pluginId: 'testA', + moduleId: 'test', + register(reg) { + reg.registerInit({ + deps: { ext: extensionPointA }, + async init() {}, + }); + }, + }), + createBackendModule({ + pluginId: 'testB', + moduleId: 'test', + register(reg) { + reg.registerInit({ + deps: { ext: extensionPointA }, + async init() {}, + }); + }, + }), + ], + }), + ).rejects.toThrow( + "Extension point registered for plugin 'testA' may not be used by module for plugin 'testB'", + ); + }); + + it('should reject extension point not used by any plugin', async () => { + const extensionPointA = createExtensionPoint({ id: 'a' }); + await expect( + startTestBackend({ + extensionPoints: [[extensionPointA, 'a']], + }), + ).rejects.toThrow( + "Unable to determine the plugin ID of extension point(s) 'a'. Tested extension points must be depended on by one or more tested modules.", + ); + }); }); diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index 8a4e1d8471..59b5906eb4 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -24,31 +24,21 @@ import { } from '@backstage/backend-app-api'; import { HostDiscovery } from '@backstage/backend-common'; import { - ServiceFactory, - ServiceRef, createServiceFactory, BackendFeature, ExtensionPoint, coreServices, - createBackendPlugin, + createBackendModule, } from '@backstage/backend-plugin-api'; import { mockServices } from '../services'; 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'; /** @public */ -export interface TestBackendOptions< - TServices extends any[], - TExtensionPoints extends any[], -> { - services?: readonly [ - ...{ - [index in keyof TServices]: - | ServiceFactory - | (() => ServiceFactory) - | [ServiceRef, Partial]; - }, - ]; +export interface TestBackendOptions { extensionPoints?: readonly [ ...{ [index in keyof TExtensionPoints]: [ @@ -57,7 +47,11 @@ export interface TestBackendOptions< ]; }, ]; - features?: BackendFeature[]; + features?: Array< + | BackendFeature + | (() => BackendFeature) + | Promise<{ default: BackendFeature | (() => BackendFeature) }> + >; } /** @public */ @@ -71,7 +65,7 @@ export interface TestBackend extends Backend { readonly server: ExtendedHttpServer; } -const defaultServiceFactories = [ +export const defaultServiceFactories = [ mockServices.cache.factory(), mockServices.rootConfig.factory(), mockServices.database.factory(), @@ -87,21 +81,130 @@ const defaultServiceFactories = [ mockServices.urlReader.factory(), ]; +/** + * Given a set of extension points and features, find the extension + * points that we mock and tie them to the correct plugin ID. + * @returns + */ +function createExtensionPointTestModules( + features: Array, + extensionPointTuples?: readonly [ + ref: ExtensionPoint, + impl: unknown, + ][], +): Array<() => BackendFeature> { + if (!extensionPointTuples) { + return []; + } + + 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}'`, + ); + } + return feature.getRegistrations(); + } + return []; + }); + + const extensionPointMap = new Map( + extensionPointTuples.map(ep => [ep[0].id, ep]), + ); + const extensionPointsToSort = new Set(extensionPointMap.keys()); + const extensionPointsByPlugin = new Map(); + + for (const registration of registrations) { + if (registration.type === 'module') { + const testDep = Object.values(registration.init.deps).filter(dep => + extensionPointsToSort.has(dep.id), + ); + if (testDep.length > 0) { + let points = extensionPointsByPlugin.get(registration.pluginId); + if (!points) { + points = []; + extensionPointsByPlugin.set(registration.pluginId, points); + } + for (const { id } of testDep) { + points.push(id); + extensionPointsToSort.delete(id); + } + } + } + } + + if (extensionPointsToSort.size > 0) { + const list = Array.from(extensionPointsToSort) + .map(id => `'${id}'`) + .join(', '); + throw new Error( + `Unable to determine the plugin ID of extension point(s) ${list}. ` + + 'Tested extension points must be depended on by one or more tested modules.', + ); + } + + const modules = []; + + for (const [pluginId, pluginExtensionPointIds] of extensionPointsByPlugin) { + modules.push( + createBackendModule({ + pluginId, + moduleId: 'testExtensionPointRegistration', + register(reg) { + for (const id of pluginExtensionPointIds) { + const tuple = extensionPointMap.get(id)!; + reg.registerExtensionPoint(...tuple); + } + + reg.registerInit({ deps: {}, async init() {} }); + }, + }), + ); + } + + return modules; +} + +function isPromise(value: unknown | Promise): value is Promise { + return ( + typeof value === 'object' && + value !== null && + 'then' in value && + typeof value.then === 'function' + ); +} + +function unwrapFeature( + feature: BackendFeature | (() => BackendFeature), +): BackendFeature { + return typeof feature === 'function' ? feature() : feature; +} + const backendInstancesToCleanUp = new Array(); /** @public */ -export async function startTestBackend< - TServices extends any[], - TExtensionPoints extends any[], ->( - options: TestBackendOptions, +export async function startTestBackend( + options: TestBackendOptions, ): Promise { - const { - services = [], - extensionPoints = [], - features = [], - ...otherOptions - } = options; + const { extensionPoints, ...otherOptions } = options; + + // Unpack input into awaited plain BackendFeatures + const features: BackendFeature[] = await Promise.all( + options.features?.map(async val => { + if (isPromise(val)) { + const { default: feature } = await val; + return unwrapFeature(feature); + } + return unwrapFeature(val); + }) ?? [], + ); let server: ExtendedHttpServer; @@ -157,55 +260,20 @@ export async function startTestBackend< }, }); - const factories = services.map(serviceDef => { - if (Array.isArray(serviceDef)) { - // if type is ExtensionPoint? - // do something differently? - const [ref, impl] = serviceDef; - if (ref.scope === 'plugin') { - return createServiceFactory({ - service: ref as ServiceRef, - deps: {}, - factory: async () => impl, - })(); - } - return createServiceFactory({ - service: ref as ServiceRef, - deps: {}, - factory: async () => impl, - })(); - } - if (typeof serviceDef === 'function') { - return serviceDef(); - } - return serviceDef as ServiceFactory; - }); - - for (const factory of defaultServiceFactories) { - if (!factories.some(f => f.service.id === factory.service.id)) { - factories.push(factory); - } - } - const backend = createSpecializedBackend({ ...otherOptions, - services: [...factories, rootHttpRouterFactory, discoveryFactory], + defaultServiceFactories: [ + ...defaultServiceFactories, + rootHttpRouterFactory, + discoveryFactory, + ], }); backendInstancesToCleanUp.push(backend); - backend.add( - createBackendPlugin({ - pluginId: `---test-extension-point-registrar`, - register(reg) { - for (const [ref, impl] of extensionPoints) { - reg.registerExtensionPoint(ref, impl); - } - - reg.registerInit({ deps: {}, async init() {} }); - }, - })(), - ); + for (const m of createExtensionPointTestModules(features, extensionPoints)) { + backend.add(m); + } for (const feature of features) { backend.add(feature); @@ -248,3 +316,11 @@ function registerTestHooks() { } registerTestHooks(); + +function isInternalBackendFeature( + feature: BackendFeature, +): feature is InternalBackendFeature { + return ( + typeof (feature as InternalBackendFeature).getRegistrations === 'function' + ); +} diff --git a/packages/backend-test-utils/src/next/wiring/index.ts b/packages/backend-test-utils/src/next/wiring/index.ts index 7c39474d4c..bd6a61317f 100644 --- a/packages/backend-test-utils/src/next/wiring/index.ts +++ b/packages/backend-test-utils/src/next/wiring/index.ts @@ -14,5 +14,9 @@ * limitations under the License. */ +export { + ServiceFactoryTester, + type ServiceFactoryTesterOptions, +} from './ServiceFactoryTester'; export { startTestBackend } from './TestBackend'; export type { TestBackend, TestBackendOptions } from './TestBackend'; diff --git a/packages/backend/.snyk b/packages/backend/.snyk new file mode 100644 index 0000000000..ea5abd653d --- /dev/null +++ b/packages/backend/.snyk @@ -0,0 +1,10 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.22.1 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + SNYK-JS-ISOLATEDVM-3037320: + - '*': + reason: We do not pass any V8 cache data, and are therefore unaffected by this vulnerability + expires: 2033-07-02T16:55:57.077Z + created: 2023-07-02T16:55:57.077Z +patch: {} diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 84c802ae58..6c3dfcf2fd 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,280 @@ # example-backend +## 0.2.87-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.16.6-next.2 + - @backstage/plugin-code-coverage-backend@0.2.17-next.2 + - @backstage/plugin-permission-backend@0.5.26-next.2 + - @backstage/plugin-catalog-backend@1.13.0-next.2 + - @backstage/plugin-badges-backend@0.2.6-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.35-next.2 + - @backstage/plugin-tech-insights-backend@0.5.17-next.2 + - @backstage/backend-tasks@0.5.8-next.2 + - example-app@0.2.87-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-app-backend@0.3.51-next.2 + - @backstage/plugin-auth-backend@0.18.9-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/plugin-entity-feedback-backend@0.1.9-next.2 + - @backstage/plugin-events-backend@0.2.12-next.2 + - @backstage/plugin-kubernetes-backend@0.11.6-next.2 + - @backstage/plugin-linguist-backend@0.4.3-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/plugin-playlist-backend@0.3.7-next.2 + - @backstage/plugin-proxy-backend@0.3.3-next.2 + - @backstage/plugin-rollbar-backend@0.1.48-next.2 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.4-next.2 + - @backstage/plugin-search-backend@1.4.3-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.7-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.7-next.2 + - @backstage/plugin-search-backend-module-pg@0.5.12-next.2 + - @backstage/plugin-search-backend-module-techdocs@0.1.7-next.2 + - @backstage/plugin-techdocs-backend@1.7.0-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/plugin-devtools-backend@0.1.6-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-adr-backend@0.3.9-next.2 + - @backstage/plugin-azure-devops-backend@0.3.30-next.2 + - @backstage/plugin-azure-sites-backend@0.1.13-next.2 + - @backstage/plugin-explore-backend@0.0.13-next.2 + - @backstage/plugin-graphql-backend@0.1.41-next.2 + - @backstage/plugin-jenkins-backend@0.2.6-next.2 + - @backstage/plugin-kafka-backend@0.2.44-next.2 + - @backstage/plugin-lighthouse-backend@0.2.7-next.2 + - @backstage/plugin-nomad-backend@0.1.5-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.20-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.3.6-next.2 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/plugin-tech-insights-node@0.4.9-next.2 + - @backstage/plugin-todo-backend@0.2.3-next.2 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.3-next.2 + - @backstage/plugin-events-node@0.2.12-next.2 + - @backstage/plugin-search-common@1.2.6-next.1 + +## 0.2.87-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-module-pg@0.5.12-next.1 + - @backstage/plugin-kubernetes-backend@0.11.6-next.1 + - @backstage/plugin-catalog-backend@1.13.0-next.1 + - @backstage/plugin-auth-backend@0.18.9-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/plugin-devtools-backend@0.1.6-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/plugin-techdocs-backend@1.7.0-next.1 + - @backstage/plugin-scaffolder-backend@1.16.6-next.1 + - @backstage/plugin-code-coverage-backend@0.2.17-next.1 + - example-app@0.2.87-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-adr-backend@0.3.9-next.1 + - @backstage/plugin-app-backend@0.3.51-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-azure-devops-backend@0.3.30-next.1 + - @backstage/plugin-azure-sites-backend@0.1.13-next.1 + - @backstage/plugin-badges-backend@0.2.6-next.1 + - @backstage/plugin-entity-feedback-backend@0.1.9-next.1 + - @backstage/plugin-events-backend@0.2.12-next.1 + - @backstage/plugin-explore-backend@0.0.13-next.1 + - @backstage/plugin-graphql-backend@0.1.41-next.1 + - @backstage/plugin-jenkins-backend@0.2.6-next.1 + - @backstage/plugin-kafka-backend@0.2.44-next.1 + - @backstage/plugin-lighthouse-backend@0.2.7-next.1 + - @backstage/plugin-linguist-backend@0.4.3-next.1 + - @backstage/plugin-nomad-backend@0.1.5-next.1 + - @backstage/plugin-permission-backend@0.5.26-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/plugin-playlist-backend@0.3.7-next.1 + - @backstage/plugin-proxy-backend@0.3.3-next.1 + - @backstage/plugin-rollbar-backend@0.1.48-next.1 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.4-next.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.20-next.1 + - @backstage/plugin-search-backend@1.4.3-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.7-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.3.6-next.1 + - @backstage/plugin-search-backend-module-explore@0.1.7-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.7-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.1 + - @backstage/plugin-tech-insights-backend@0.5.17-next.1 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.35-next.1 + - @backstage/plugin-tech-insights-node@0.4.9-next.1 + - @backstage/plugin-todo-backend@0.2.3-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.3-next.1 + - @backstage/plugin-events-node@0.2.12-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-search-common@1.2.6-next.0 + +## 0.2.87-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/plugin-auth-backend@0.18.8-next.0 + - @backstage/plugin-code-coverage-backend@0.2.16-next.0 + - @backstage/plugin-scaffolder-backend@1.16.3-next.0 + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/plugin-linguist-backend@0.4.2-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/plugin-entity-feedback-backend@0.1.8-next.0 + - @backstage/plugin-tech-insights-backend@0.5.16-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/plugin-app-backend@0.3.50-next.0 + - example-app@0.2.87-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-adr-backend@0.3.8-next.0 + - @backstage/plugin-azure-devops-backend@0.3.29-next.0 + - @backstage/plugin-azure-sites-backend@0.1.12-next.0 + - @backstage/plugin-badges-backend@0.2.5-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.2-next.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-devtools-backend@0.1.5-next.0 + - @backstage/plugin-events-backend@0.2.11-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + - @backstage/plugin-explore-backend@0.0.12-next.0 + - @backstage/plugin-graphql-backend@0.1.40-next.0 + - @backstage/plugin-jenkins-backend@0.2.5-next.0 + - @backstage/plugin-kafka-backend@0.2.43-next.0 + - @backstage/plugin-kubernetes-backend@0.11.5-next.0 + - @backstage/plugin-lighthouse-backend@0.2.6-next.0 + - @backstage/plugin-nomad-backend@0.1.4-next.0 + - @backstage/plugin-permission-backend@0.5.25-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-playlist-backend@0.3.6-next.0 + - @backstage/plugin-proxy-backend@0.3.2-next.0 + - @backstage/plugin-rollbar-backend@0.1.47-next.0 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.3-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.19-next.0 + - @backstage/plugin-search-backend@1.4.2-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.3.5-next.0 + - @backstage/plugin-search-backend-module-pg@0.5.11-next.0 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.34-next.0 + - @backstage/plugin-tech-insights-node@0.4.8-next.0 + - @backstage/plugin-techdocs-backend@1.6.7-next.0 + - @backstage/plugin-todo-backend@0.2.2-next.0 + +## 0.2.86 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-module-elasticsearch@1.3.3 + - @backstage/plugin-search-backend-module-pg@0.5.9 + - @backstage/plugin-azure-devops-backend@0.3.27 + - @backstage/plugin-kubernetes-backend@0.11.3 + - @backstage/plugin-lighthouse-backend@0.2.4 + - @backstage/plugin-permission-backend@0.5.23 + - @backstage/plugin-scaffolder-backend@1.16.0 + - @backstage/plugin-devtools-backend@0.1.3 + - @backstage/plugin-techdocs-backend@1.6.5 + - @backstage/backend-common@0.19.2 + - @backstage/plugin-catalog-backend@1.12.0 + - @backstage/plugin-badges-backend@0.2.3 + - @backstage/plugin-events-backend@0.2.9 + - @backstage/plugin-search-backend@1.4.0 + - @backstage/plugin-kafka-backend@0.2.41 + - @backstage/plugin-proxy-backend@0.3.0 + - @backstage/plugin-todo-backend@0.2.0 + - @backstage/plugin-app-backend@0.3.48 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.1 + - @backstage/plugin-auth-backend@0.18.6 + - @backstage/plugin-explore-backend@0.0.10 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.17 + - @backstage/plugin-entity-feedback-backend@0.1.6 + - @backstage/plugin-code-coverage-backend@0.2.14 + - @backstage/plugin-search-backend-node@1.2.4 + - @backstage/plugin-linguist-backend@0.4.0 + - @backstage/plugin-playlist-backend@0.3.4 + - @backstage/plugin-jenkins-backend@0.2.3 + - @backstage/plugin-nomad-backend@0.1.2 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/plugin-events-node@0.2.9 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - example-app@0.2.86 + - @backstage/plugin-adr-backend@0.3.6 + - @backstage/plugin-azure-sites-backend@0.1.10 + - @backstage/plugin-graphql-backend@0.1.38 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/plugin-rollbar-backend@0.1.45 + - @backstage/plugin-tech-insights-backend@0.5.14 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.32 + - @backstage/plugin-tech-insights-node@0.4.6 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-common@1.2.5 + +## 0.2.86-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.18.6-next.2 + - @backstage/plugin-scaffolder-backend@1.15.2-next.2 + - @backstage/plugin-explore-backend@0.0.10-next.2 + - @backstage/plugin-catalog-backend@1.12.0-next.2 + - @backstage/plugin-proxy-backend@0.3.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/plugin-app-backend@0.3.48-next.2 + - @backstage/plugin-linguist-backend@0.4.0-next.2 + - @backstage/plugin-techdocs-backend@1.6.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - example-app@0.2.86-next.2 + - @backstage/plugin-adr-backend@0.3.6-next.2 + - @backstage/plugin-azure-devops-backend@0.3.27-next.2 + - @backstage/plugin-badges-backend@0.2.3-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.2.0-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-devtools-backend@0.1.3-next.2 + - @backstage/plugin-entity-feedback-backend@0.1.6-next.2 + - @backstage/plugin-events-backend@0.2.9-next.2 + - @backstage/plugin-events-node@0.2.9-next.2 + - @backstage/plugin-kafka-backend@0.2.41-next.2 + - @backstage/plugin-kubernetes-backend@0.11.3-next.2 + - @backstage/plugin-lighthouse-backend@0.2.4-next.2 + - @backstage/plugin-permission-backend@0.5.23-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-search-backend@1.4.0-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.3.3-next.2 + - @backstage/plugin-search-backend-module-pg@0.5.9-next.2 + - @backstage/plugin-search-backend-node@1.2.4-next.2 + - @backstage/plugin-todo-backend@0.2.0-next.2 + - @backstage/plugin-tech-insights-backend@0.5.14-next.2 + - @backstage/plugin-tech-insights-node@0.4.6-next.2 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.1-next.2 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.17-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + - @backstage/plugin-azure-sites-backend@0.1.10-next.2 + - @backstage/plugin-code-coverage-backend@0.2.14-next.2 + - @backstage/plugin-graphql-backend@0.1.38-next.2 + - @backstage/plugin-jenkins-backend@0.2.3-next.2 + - @backstage/plugin-nomad-backend@0.1.2-next.2 + - @backstage/plugin-playlist-backend@0.3.4-next.2 + - @backstage/plugin-rollbar-backend@0.1.45-next.2 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.32-next.2 + ## 0.2.86-next.1 ### Patch Changes diff --git a/packages/backend/catalog-info.yaml b/packages/backend/catalog-info.yaml new file mode 100644 index 0000000000..0e95516a41 --- /dev/null +++ b/packages/backend/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: example-backend + title: example-backend +spec: + lifecycle: experimental + type: backstage-backend + owner: maintainers diff --git a/packages/backend/package.json b/packages/backend/package.json index 4f5b896d27..d7f8a9c403 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.86-next.1", + "version": "0.2.87-next.2", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -65,8 +65,11 @@ "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "workspace:^", "@backstage/plugin-scaffolder-backend-module-rails": "workspace:^", "@backstage/plugin-search-backend": "workspace:^", + "@backstage/plugin-search-backend-module-catalog": "workspace:^", "@backstage/plugin-search-backend-module-elasticsearch": "workspace:^", + "@backstage/plugin-search-backend-module-explore": "workspace:^", "@backstage/plugin-search-backend-module-pg": "workspace:^", + "@backstage/plugin-search-backend-module-techdocs": "workspace:^", "@backstage/plugin-search-backend-node": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "@backstage/plugin-tech-insights-backend": "workspace:^", @@ -87,6 +90,7 @@ "express-prom-bundle": "^6.3.6", "express-promise-router": "^4.1.0", "luxon": "^3.0.0", + "mysql2": "^2.2.5", "pg": "^8.3.0", "pg-connection-string": "^2.3.0", "prom-client": "^14.0.1", diff --git a/packages/backend/src/plugins/search.ts b/packages/backend/src/plugins/search.ts index 4290fcd68a..7babfe7e1f 100644 --- a/packages/backend/src/plugins/search.ts +++ b/packages/backend/src/plugins/search.ts @@ -16,8 +16,8 @@ import { useHotCleanup } from '@backstage/backend-common'; import { DefaultAdrCollatorFactory } from '@backstage/plugin-adr-backend'; -import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; -import { ToolDocumentCollatorFactory } from '@backstage/plugin-explore-backend'; +import { DefaultCatalogCollatorFactory } from '@backstage/plugin-search-backend-module-catalog'; +import { ToolDocumentCollatorFactory } from '@backstage/plugin-search-backend-module-explore'; import { createRouter } from '@backstage/plugin-search-backend'; import { ElasticSearchSearchEngine } from '@backstage/plugin-search-backend-module-elasticsearch'; import { PgSearchEngine } from '@backstage/plugin-search-backend-module-pg'; @@ -26,7 +26,7 @@ import { LunrSearchEngine, } from '@backstage/plugin-search-backend-node'; import { SearchEngine } from '@backstage/plugin-search-common'; -import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; +import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-search-backend-module-techdocs'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; @@ -43,6 +43,7 @@ async function createSearchEngine( if (await PgSearchEngine.supported(env.database)) { return await PgSearchEngine.fromConfig(env.config, { database: env.database, + logger: env.logger, }); } diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index c9bf604360..d544c25244 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/catalog-client +## 1.4.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + +## 1.4.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/errors@1.2.1 + ## 1.4.3 ### Patch Changes diff --git a/packages/catalog-client/catalog-info.yaml b/packages/catalog-client/catalog-info.yaml new file mode 100644 index 0000000000..91a4cb5504 --- /dev/null +++ b/packages/catalog-client/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-catalog-client + title: '@backstage/catalog-client' + description: An isomorphic client for the catalog backend +spec: + lifecycle: production + type: backstage-common-library + owner: maintainers diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 99c36cdf2d..91f90e8928 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-client", "description": "An isomorphic client for the catalog backend", - "version": "1.4.3", + "version": "1.4.4-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,6 +23,7 @@ "keywords": [ "backstage" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 8425a83c85..4e07318bf7 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/catalog-model +## 1.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + ## 1.4.1 ### Patch Changes diff --git a/packages/catalog-model/catalog-info.yaml b/packages/catalog-model/catalog-info.yaml new file mode 100644 index 0000000000..7b2bce99b9 --- /dev/null +++ b/packages/catalog-model/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-catalog-model + title: '@backstage/catalog-model' + description: Types and validators that help describe the model of a Backstage Catalog +spec: + lifecycle: production + type: backstage-common-library + owner: maintainers diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 869023127f..536fee0893 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-model", "description": "Types and validators that help describe the model of a Backstage Catalog", - "version": "1.4.1", + "version": "1.4.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,6 +35,7 @@ "keywords": [ "backstage" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/packages/cli-common/catalog-info.yaml b/packages/cli-common/catalog-info.yaml new file mode 100644 index 0000000000..bc1f589fc1 --- /dev/null +++ b/packages/cli-common/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-common + title: '@backstage/cli-common' + description: Common functionality used by cli, backend, and create-app +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/packages/cli-node/CHANGELOG.md b/packages/cli-node/CHANGELOG.md index 2615c2ddd7..8a7af72d8d 100644 --- a/packages/cli-node/CHANGELOG.md +++ b/packages/cli-node/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/cli-node +## 0.1.3 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + ## 0.1.3-next.0 ### Patch Changes diff --git a/packages/cli-node/catalog-info.yaml b/packages/cli-node/catalog-info.yaml new file mode 100644 index 0000000000..3497007bfa --- /dev/null +++ b/packages/cli-node/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-node + title: '@backstage/cli-node' + description: Node.js library for Backstage CLIs +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index f87f70b135..589139a141 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli-node", "description": "Node.js library for Backstage CLIs", - "version": "0.1.3-next.0", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 54660d1773..dd4a35df94 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,77 @@ # @backstage/cli +## 0.22.13-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- ea779492ad88: Updated dependency `run-script-webpack-plugin` to `^0.2.0`. +- Updated dependencies + - @backstage/release-manifests@0.0.10-next.0 + - @backstage/config-loader@1.5.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + - @backstage/eslint-plugin@0.1.3 + - @backstage/types@1.1.0 + +## 0.22.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/config-loader@1.5.0-next.1 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + - @backstage/eslint-plugin@0.1.3 + - @backstage/release-manifests@0.0.9 + - @backstage/types@1.1.0 + +## 0.22.12-next.0 + +### Patch Changes + +- 278d9326eb40: Added the ability to create a plain backend module with the `new` command. +- 71d4368ae5cc: Added support for the `dev/index` entry point for backend plugins and modules. +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/eslint-plugin@0.1.3 + - @backstage/release-manifests@0.0.9 + - @backstage/types@1.1.0 + +## 0.22.10 + +### Patch Changes + +- 3f67cefb4780: Reload the frontend when app config changes +- cebbf8a27f3c: Enable to print the config schema not merged with the `--no-merge` flag +- 5c28ebc79fd6: Updated dependency `esbuild` to `^0.19.0`. +- 971bdd6a4732: Bumped internal `nodemon` dependency. +- Updated dependencies + - @backstage/config-loader@1.4.0 + - @backstage/cli-node@0.1.3 + - @backstage/integration@1.6.0 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/eslint-plugin@0.1.3 + - @backstage/release-manifests@0.0.9 + - @backstage/types@1.1.0 + ## 0.22.10-next.1 ### Patch Changes diff --git a/packages/cli/catalog-info.yaml b/packages/cli/catalog-info.yaml new file mode 100644 index 0000000000..212ba472d4 --- /dev/null +++ b/packages/cli/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli + title: '@backstage/cli' + description: CLI for developing Backstage plugins and apps +spec: + lifecycle: experimental + type: backstage-cli + owner: maintainers diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index e20634df4e..6323eb0035 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -405,6 +405,7 @@ Options: Commands: build [options] lint [options] + fix [options] clean list-deprecations [options] test [options] @@ -431,6 +432,16 @@ Options: -h, --help ``` +### `backstage-cli repo fix` + +``` +Usage: backstage-cli repo fix [options] + +Options: + --check + -h, --help +``` + ### `backstage-cli repo lint` ``` diff --git a/packages/cli/package.json b/packages/cli/package.json index 8fe6a579ea..604832ed89 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.22.10-next.1", + "version": "0.22.13-next.2", "publishConfig": { "access": "public" }, @@ -41,6 +41,7 @@ "@backstage/release-manifests": "workspace:^", "@backstage/types": "workspace:^", "@esbuild-kit/cjs-loader": "^2.4.1", + "@esbuild-kit/esm-loader": "^2.5.5", "@manypkg/get-packages": "^1.1.3", "@octokit/graphql": "^5.0.0", "@octokit/graphql-schema": "^13.7.0", @@ -78,7 +79,7 @@ "cross-spawn": "^7.0.3", "css-loader": "^6.5.1", "diff": "^5.0.0", - "esbuild": "^0.18.0", + "esbuild": "^0.19.0", "esbuild-loader": "^2.18.0", "eslint": "^8.6.0", "eslint-config-prettier": "^8.3.0", @@ -122,7 +123,7 @@ "rollup-plugin-esbuild": "^4.7.2", "rollup-plugin-postcss": "^4.0.0", "rollup-pluginutils": "^2.8.2", - "run-script-webpack-plugin": "^0.1.0", + "run-script-webpack-plugin": "^0.2.0", "semver": "^7.5.3", "style-loader": "^3.3.1", "sucrase": "^3.20.2", @@ -140,6 +141,8 @@ }, "devDependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", "@backstage/config": "workspace:^", "@backstage/core-app-api": "workspace:^", "@backstage/core-components": "workspace:^", @@ -167,7 +170,7 @@ "del": "^7.0.0", "mock-fs": "^5.1.0", "msw": "^1.0.0", - "nodemon": "^2.0.2", + "nodemon": "^3.0.1", "ts-node": "^10.0.0", "type-fest": "^2.19.0" }, diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index a7f65588a5..b1941ddac8 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -67,6 +67,15 @@ export function registerRepoCommand(program: Command) { .option('--fix', 'Attempt to automatically fix violations') .action(lazy(() => import('./repo/lint').then(m => m.command))); + command + .command('fix') + .description('Automatically fix packages in the project') + .option( + '--check', + 'Fail if any packages would have been changed by the command', + ) + .action(lazy(() => import('./repo/fix').then(m => m.command))); + command .command('clean') .description('Delete cache and output directories') diff --git a/packages/cli/src/commands/migrate/packageExports.ts b/packages/cli/src/commands/migrate/packageExports.ts index 82d0b73ac2..6757d01bbe 100644 --- a/packages/cli/src/commands/migrate/packageExports.ts +++ b/packages/cli/src/commands/migrate/packageExports.ts @@ -14,108 +14,21 @@ * limitations under the License. */ -import fs from 'fs-extra'; -import { resolve as resolvePath } from 'path'; -import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node'; - -function trimRelative(path: string): string { - if (path.startsWith('./')) { - return path.slice(2); - } - return path; -} +import { + fixPackageExports, + readFixablePackages, + writeFixedPackages, +} from '../repo/fix'; export async function command() { - const packages = await PackageGraph.listTargetPackages(); - - await Promise.all( - packages.map(async ({ dir, packageJson }) => { - let changed = false; - let newPackageJson = packageJson; - - let { exports: exp } = newPackageJson; - if (!exp) { - return; - } - if (Array.isArray(exp)) { - throw new Error('Unexpected array in package.json exports field'); - } - - // If exports is a string we rewrite it to an object to add package.json - if (typeof exp === 'string') { - changed = true; - exp = { '.': exp }; - newPackageJson.exports = exp; - } else if (typeof exp !== 'object') { - return; - } - - if (!exp['./package.json']) { - changed = true; - exp['./package.json'] = './package.json'; - } - - const existingTypesVersions = JSON.stringify(packageJson.typesVersions); - - const typeEntries: Record = {}; - for (const [path, value] of Object.entries(exp)) { - // Main entry point does not need to be listed - if (path === '.') { - continue; - } - const newPath = trimRelative(path); - - if (typeof value === 'string') { - typeEntries[newPath] = [trimRelative(value)]; - } else if ( - value && - typeof value === 'object' && - !Array.isArray(value) - ) { - if (typeof value.types === 'string') { - typeEntries[newPath] = [trimRelative(value.types)]; - } else if (typeof value.default === 'string') { - typeEntries[newPath] = [trimRelative(value.default)]; - } - } - } - - const typesVersions = { '*': typeEntries }; - if (existingTypesVersions !== JSON.stringify(typesVersions)) { - console.log(`Synchronizing exports in ${packageJson.name}`); - const newPkgEntries = Object.entries(newPackageJson).filter( - ([name]) => name !== 'typesVersions', - ); - newPkgEntries.splice( - newPkgEntries.findIndex(([name]) => name === 'exports') + 1, - 0, - ['typesVersions', typesVersions], - ); - - newPackageJson = Object.fromEntries( - newPkgEntries, - ) as BackstagePackageJson; - changed = true; - } - - // Remove the legacy fields from publishConfig, which are no longer needed - const publishConfig = newPackageJson.publishConfig as - | Record - | undefined; - if (publishConfig) { - for (const field of ['main', 'module', 'browser', 'types']) { - if (publishConfig[field]) { - delete publishConfig[field]; - changed = true; - } - } - } - - if (changed) { - await fs.writeJson(resolvePath(dir, 'package.json'), newPackageJson, { - spaces: 2, - }); - } - }), + console.log( + 'The `migrate package-exports` command is deprecated, use `repo fix` instead.', ); + const packages = await readFixablePackages(); + + for (const pkg of packages) { + fixPackageExports(pkg); + } + + await writeFixedPackages(packages); } diff --git a/packages/cli/src/commands/repo/fix.ts b/packages/cli/src/commands/repo/fix.ts new file mode 100644 index 0000000000..bd5b375ed5 --- /dev/null +++ b/packages/cli/src/commands/repo/fix.ts @@ -0,0 +1,207 @@ +/* + * 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 { + BackstagePackage, + BackstagePackageJson, + PackageGraph, + PackageRoles, +} from '@backstage/cli-node'; +import { OptionValues } from 'commander'; +import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import { paths } from '../../lib/paths'; + +/** + * A mutable object representing a package.json file with potential fixes. + */ +export interface FixablePackage extends BackstagePackage { + changed: boolean; +} + +export async function readFixablePackages(): Promise { + const packages = await PackageGraph.listTargetPackages(); + return packages.map(pkg => ({ ...pkg, changed: false })); +} + +export function printPackageFixHint(packages: FixablePackage[]) { + const changed = packages.filter(pkg => pkg.changed); + if (changed.length > 0) { + const rootPkg = require(paths.resolveTargetRoot('package.json')); + const fixCmd = + rootPkg.scripts?.fix === 'backstage-cli repo fix' + ? 'fix' + : 'backstage-cli repo fix'; + console.log( + `The following packages are out of sync, run 'yarn ${fixCmd}' to fix them:`, + ); + for (const pkg of changed) { + console.log(` ${pkg.packageJson.name}`); + } + return true; + } + return false; +} + +export async function writeFixedPackages( + packages: FixablePackage[], +): Promise { + await Promise.all( + packages.map(async pkg => { + if (!pkg.changed) { + return; + } + + await fs.writeJson( + resolvePath(pkg.dir, 'package.json'), + pkg.packageJson, + { + spaces: 2, + }, + ); + }), + ); +} + +function trimRelative(path: string): string { + if (path.startsWith('./')) { + return path.slice(2); + } + return path; +} + +export function fixPackageExports(pkg: FixablePackage) { + let { exports: exp } = pkg.packageJson; + if (!exp) { + return; + } + if (Array.isArray(exp)) { + throw new Error('Unexpected array in package.json exports field'); + } + + // If exports is a string we rewrite it to an object to add package.json + if (typeof exp === 'string') { + pkg.changed = true; + exp = { '.': exp }; + pkg.packageJson.exports = exp; + } else if (typeof exp !== 'object') { + return; + } + + if (!exp['./package.json']) { + pkg.changed = true; + exp['./package.json'] = './package.json'; + } + + const existingTypesVersions = JSON.stringify(pkg.packageJson.typesVersions); + + const typeEntries: Record = {}; + for (const [path, value] of Object.entries(exp)) { + // Main entry point does not need to be listed + if (path === '.') { + continue; + } + const newPath = trimRelative(path); + + if (typeof value === 'string') { + typeEntries[newPath] = [trimRelative(value)]; + } else if (value && typeof value === 'object' && !Array.isArray(value)) { + if (typeof value.types === 'string') { + typeEntries[newPath] = [trimRelative(value.types)]; + } else if (typeof value.default === 'string') { + typeEntries[newPath] = [trimRelative(value.default)]; + } + } + } + + const typesVersions = { '*': typeEntries }; + if (existingTypesVersions !== JSON.stringify(typesVersions)) { + const newPkgEntries = Object.entries(pkg.packageJson).filter( + ([name]) => name !== 'typesVersions', + ); + newPkgEntries.splice( + newPkgEntries.findIndex(([name]) => name === 'exports') + 1, + 0, + ['typesVersions', typesVersions], + ); + + pkg.packageJson = Object.fromEntries(newPkgEntries) as BackstagePackageJson; + pkg.changed = true; + } + + // Remove the legacy fields from publishConfig, which are no longer needed + const publishConfig = pkg.packageJson.publishConfig as + | Record + | undefined; + if (publishConfig) { + for (const field of ['main', 'module', 'browser', 'types']) { + if (publishConfig[field]) { + delete publishConfig[field]; + pkg.changed = true; + } + } + } +} + +export function fixSideEffects(pkg: FixablePackage) { + const role = PackageRoles.getRoleFromPackage(pkg.packageJson); + if (!role) { + return; + } + + const roleInfo = PackageRoles.getRoleInfo(role); + + // Only web and common packages benefit from being marked as side effect free + if (roleInfo.platform === 'node') { + return; + } + // Bundled packages don't need to mark themselves as having no side effects + if (roleInfo.output.length === 1 && roleInfo.output[0] === 'bundle') { + return; + } + + // Already declared + if ('sideEffects' in pkg.packageJson) { + return; + } + + const pkgEntries = Object.entries(pkg.packageJson); + pkgEntries.splice( + // Place it just above the scripts field + pkgEntries.findIndex(([name]) => name === 'scripts'), + 0, + ['sideEffects', false], + ); + pkg.packageJson = Object.fromEntries(pkgEntries) as BackstagePackageJson; + pkg.changed = true; +} + +export async function command(opts: OptionValues): Promise { + const packages = await readFixablePackages(); + + for (const pkg of packages) { + fixPackageExports(pkg); + fixSideEffects(pkg); + } + + if (opts.check) { + if (printPackageFixHint(packages)) { + process.exit(1); + } + } else { + await writeFixedPackages(packages); + } +} diff --git a/packages/cli/src/commands/start/startBackend.ts b/packages/cli/src/commands/start/startBackend.ts index 3e6a13be96..622eeb9d1b 100644 --- a/packages/cli/src/commands/start/startBackend.ts +++ b/packages/cli/src/commands/start/startBackend.ts @@ -26,7 +26,17 @@ interface StartBackendOptions { } export async function startBackend(options: StartBackendOptions) { - if (process.env.EXPERIMENTAL_BACKEND_START) { + const hasDev = await fs.pathExists(paths.resolveTarget('dev')); + if (hasDev) { + const waitForExit = await startBackendExperimental({ + entry: 'dev/index', + checksEnabled: false, // not supported + inspectEnabled: options.inspectEnabled, + inspectBrkEnabled: options.inspectBrkEnabled, + }); + + await waitForExit(); + } else if (process.env.EXPERIMENTAL_BACKEND_START) { const waitForExit = await startBackendExperimental({ entry: 'src/index', checksEnabled: false, // not supported diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index 8035796cba..99d8e96498 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -157,7 +157,7 @@ export async function makeRollupConfigs( paths.resolveTargetRoot( 'dist-types', relativePath(paths.targetRoot, targetDir), - e.path.replace(/\.ts$/, '.d.ts'), + e.path.replace(/\.(?:ts|tsx)$/, '.d.ts'), ), ]), ); diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts index 7c45528c5c..11e18320f1 100644 --- a/packages/cli/src/lib/bundler/bundle.ts +++ b/packages/cli/src/lib/bundler/bundle.ts @@ -27,6 +27,7 @@ import { createConfig, resolveBaseUrl } from './config'; import { BuildOptions } from './types'; import { resolveBundlingPaths } from './paths'; import chalk from 'chalk'; +import { createDetectedModulesEntryPoint } from './packageDetection'; // TODO(Rugvip): Limits from CRA, we might want to tweak these though. const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; @@ -40,12 +41,19 @@ export async function buildBundle(options: BuildOptions) { const { statsJsonEnabled, schema: configSchema } = options; const paths = resolveBundlingPaths(options); + + const detectedModulesEntryPoint = await createDetectedModulesEntryPoint({ + config: options.fullConfig, + targetPath: paths.targetPath, + }); + const config = await createConfig(paths, { ...options, checksEnabled: false, isDev: false, baseUrl: resolveBaseUrl(options.frontendConfig), getFrontendAppConfigs: () => options.frontendAppConfigs, + additionalEntryPoints: detectedModulesEntryPoint, }); 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 2f09a66e18..c9238e7e48 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -161,7 +161,7 @@ export async function createConfig( }, devtool: isDev ? 'eval-cheap-module-source-map' : 'source-map', context: paths.targetPath, - entry: [paths.targetEntry], + entry: [...(options.additionalEntryPoints ?? []), paths.targetEntry], resolve: { extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx', '.json', '.wasm'], mainFields: ['browser', 'module', 'main'], diff --git a/packages/cli/src/lib/bundler/packageDetection.ts b/packages/cli/src/lib/bundler/packageDetection.ts new file mode 100644 index 0000000000..46cffdceb0 --- /dev/null +++ b/packages/cli/src/lib/bundler/packageDetection.ts @@ -0,0 +1,131 @@ +/* + * 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 { BackstagePackageJson } from '@backstage/cli-node'; +import { Config } from '@backstage/config'; +import chokidar from 'chokidar'; +import fs from 'fs-extra'; +import { join as joinPath, resolve as resolvePath } from 'path'; +import { paths as cliPaths } from '../paths'; + +const DETECTED_MODULES_MODULE_NAME = '__backstage-autodetected-plugins__'; + +interface PackageDetectionConfig { + include?: string[]; + exclude?: string[]; +} + +function readPackageDetectionConfig( + config: Config, +): PackageDetectionConfig | undefined { + const packages = config.getOptional('app.experimental.packages'); + if (packages === undefined || packages === null) { + return undefined; + } + + if (typeof packages === 'string') { + if (packages !== 'all') { + throw new Error( + `Invalid app.experimental.packages mode, got '${packages}', expected 'all'`, + ); + } + return {}; + } + + return { + include: config.getOptionalStringArray('app.experimental.packages.include'), + exclude: config.getOptionalStringArray('app.experimental.packages.exclude'), + }; +} + +async function detectPackages( + targetPath: string, + { include, exclude }: PackageDetectionConfig, +) { + const pkg: BackstagePackageJson = await fs.readJson( + resolvePath(targetPath, 'package.json'), + ); + + return Object.keys(pkg.dependencies ?? {}) + .flatMap(depName => { + const depPackageJson: BackstagePackageJson = require(require.resolve( + `${depName}/package.json`, + { paths: [targetPath] }, + )); + if ( + ['frontend-plugin', 'frontend-plugin-module'].includes( + depPackageJson.backstage?.role ?? '', + ) + ) { + return [depName]; + } + return []; + }) + .filter(name => { + if (exclude?.includes(name)) { + return false; + } + if (include && !include.includes(name)) { + return false; + } + return true; + }); +} + +async function writeDetectedPackagesModule(packageNames: string[]) { + const requirePackageScript = packageNames + ?.map(pkg => `{ name: '${pkg}', default: require('${pkg}').default }`) + .join(','); + + await fs.writeFile( + joinPath( + cliPaths.targetRoot, + 'node_modules', + `${DETECTED_MODULES_MODULE_NAME}.js`, + ), + `window['__@backstage/discovered__'] = { modules: [${requirePackageScript}] };`, + ); +} + +export async function createDetectedModulesEntryPoint(options: { + config: Config; + targetPath: string; + watch?: () => void; +}): Promise { + const { config, watch, targetPath } = options; + + const detectionConfig = readPackageDetectionConfig(config); + if (!detectionConfig) { + return []; + } + + if (watch) { + const watcher = chokidar.watch(resolvePath(targetPath, 'package.json')); + + watcher.on('change', async () => { + await writeDetectedPackagesModule( + await detectPackages(targetPath, detectionConfig), + ); + watch(); + }); + } + + await writeDetectedPackagesModule( + await detectPackages(targetPath, detectionConfig), + ); + + return [DETECTED_MODULES_MODULE_NAME]; +} diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 74c6b2a53c..78f6bb7823 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -14,27 +14,31 @@ * 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 openBrowser from 'react-dev-utils/openBrowser'; -import uniq from 'lodash/uniq'; -import { createConfig, resolveBaseUrl } from './config'; -import { ServeOptions } from './types'; -import { resolveBundlingPaths } from './paths'; -import { paths as libPaths } from '../../lib/paths'; -import { loadCliConfig } from '../config'; -import chalk from 'chalk'; -import { AppConfig } from '@backstage/config'; -import { PackageGraph } from '@backstage/cli-node'; -import { Lockfile } from '../versioning'; 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 { createDetectedModulesEntryPoint } from './packageDetection'; +import { resolveBundlingPaths } from './paths'; +import { ServeOptions } from './types'; export async function serveBundle(options: ServeOptions) { + const paths = resolveBundlingPaths(options); + const targetPkg = await fs.readJson(paths.targetPackageJson); + if (options.verifyVersions) { const lockfile = await Lockfile.load( libPaths.resolveTargetRoot('yarn.lock'), @@ -115,10 +119,16 @@ export async function serveBundle(options: ServeOptions) { Number(url.port) || (url.protocol === 'https:' ? 443 : 80); - const paths = resolveBundlingPaths(options); - const pkgPath = paths.targetPackageJson; - const pkg = await fs.readJson(pkgPath); + const detectedModulesEntryPoint = await createDetectedModulesEntryPoint({ + config: fullConfig, + targetPath: paths.targetPath, + watch() { + server?.invalidate(); + }, + }); + const config = await createConfig(paths, { + ...options, checksEnabled: options.checksEnabled, isDev: true, baseUrl: url, @@ -126,6 +136,7 @@ export async function serveBundle(options: ServeOptions) { getFrontendAppConfigs: () => { return latestFrontendAppConfigs; }, + additionalEntryPoints: detectedModulesEntryPoint, }); const compiler = webpack(config); @@ -160,7 +171,7 @@ export async function serveBundle(options: ServeOptions) { : false, host, port, - proxy: pkg.proxy, + proxy: targetPkg.proxy, // When the dev server is behind a proxy, the host and public hostname differ allowedHosts: [url.hostname], client: { diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 69e75b113d..414414fa81 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -25,6 +25,7 @@ export type BundlingOptions = { getFrontendAppConfigs(): AppConfig[]; baseUrl: URL; parallelism?: number; + additionalEntryPoints?: string[]; }; export type ServeOptions = BundlingPathsOptions & { @@ -41,6 +42,7 @@ export type BuildOptions = BundlingPathsOptions & { schema?: ConfigSchema; frontendConfig: Config; frontendAppConfigs: AppConfig[]; + fullConfig: Config; }; export type BackendBundlingOptions = { diff --git a/packages/cli/src/lib/experimental/startBackendExperimental.ts b/packages/cli/src/lib/experimental/startBackendExperimental.ts index c08fbda0da..f4875ce1e5 100644 --- a/packages/cli/src/lib/experimental/startBackendExperimental.ts +++ b/packages/cli/src/lib/experimental/startBackendExperimental.ts @@ -25,7 +25,12 @@ import debounce from 'lodash/debounce'; import spawn from 'cross-spawn'; import { paths } from '../paths'; -const loaderArgs = ['--require', require.resolve('@esbuild-kit/cjs-loader')]; +const loaderArgs = [ + '--require', + require.resolve('@esbuild-kit/cjs-loader'), + '--loader', + require.resolve('@esbuild-kit/esm-loader'), +]; export async function startBackendExperimental(options: BackendServeOptions) { const envEnv = process.env as { NODE_ENV: string }; diff --git a/packages/cli/src/lib/new/factories/backendModule.test.ts b/packages/cli/src/lib/new/factories/backendModule.test.ts new file mode 100644 index 0000000000..c633b1c1b2 --- /dev/null +++ b/packages/cli/src/lib/new/factories/backendModule.test.ts @@ -0,0 +1,121 @@ +/* + * 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 fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { sep, resolve as resolvePath } from 'path'; +import { paths } from '../../paths'; +import { Task } from '../../tasks'; +import { FactoryRegistry } from '../FactoryRegistry'; +import { createMockOutputStream, mockPaths } from './common/testUtils'; +import { backendModule } from './backendModule'; + +describe('backendModule factory', () => { + beforeEach(() => { + mockPaths({ + targetRoot: '/root', + }); + }); + + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should create a backend plugin', async () => { + mockFs({ + '/root': { + packages: { + backend: { + 'package.json': JSON.stringify({}), + }, + }, + plugins: mockFs.directory(), + }, + [paths.resolveOwn('templates')]: mockFs.load( + paths.resolveOwn('templates'), + ), + }); + + const options = await FactoryRegistry.populateOptions(backendModule, { + id: 'test', + moduleId: 'tester-two', + }); + + let modified = false; + + const [output, mockStream] = createMockOutputStream(); + jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); + jest.spyOn(Task, 'forCommand').mockResolvedValue(); + + await backendModule.create(options, { + private: true, + isMonoRepo: true, + defaultVersion: '1.0.0', + markAsModified: () => { + modified = true; + }, + createTemporaryDirectory: () => fs.mkdtemp('test'), + }); + + expect(modified).toBe(true); + + expect(output).toEqual([ + '', + 'Creating backend module backstage-plugin-test-backend-module-tester-two', + 'Checking Prerequisites:', + `availability plugins${sep}test-backend-module-tester-two`, + 'creating temp dir', + 'Executing Template:', + 'copying .eslintrc.js', + 'templating README.md.hbs', + 'templating package.json.hbs', + 'templating index.ts.hbs', + 'templating module.ts.hbs', + 'Installing:', + `moving plugins${sep}test-backend-module-tester-two`, + 'backend adding dependency', + ]); + + await expect( + fs.readJson('/root/packages/backend/package.json'), + ).resolves.toEqual({ + dependencies: { + 'backstage-plugin-test-backend-module-tester-two': '^1.0.0', + }, + }); + const moduleFile = await fs.readFile( + '/root/plugins/test-backend-module-tester-two/src/module.ts', + 'utf-8', + ); + + expect(moduleFile).toContain( + `const testModuleTesterTwo = createBackendModule({`, + ); + expect(moduleFile).toContain(`pluginId: 'test',`); + expect(moduleFile).toContain(`moduleId: 'tester-two',`); + + expect(Task.forCommand).toHaveBeenCalledTimes(2); + expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { + cwd: resolvePath('/root/plugins/test-backend-module-tester-two'), + optional: true, + }); + expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { + cwd: resolvePath('/root/plugins/test-backend-module-tester-two'), + optional: true, + }); + }); +}); diff --git a/packages/cli/src/lib/new/factories/backendModule.ts b/packages/cli/src/lib/new/factories/backendModule.ts new file mode 100644 index 0000000000..221adcb1f3 --- /dev/null +++ b/packages/cli/src/lib/new/factories/backendModule.ts @@ -0,0 +1,100 @@ +/* + * 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 fs from 'fs-extra'; +import chalk from 'chalk'; +import camelCase from 'lodash/camelCase'; +import { paths } from '../../paths'; +import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; +import { createFactory, CreateContext } from '../types'; +import { addPackageDependency, Task } from '../../tasks'; +import { + moduleIdIdPrompt, + ownerPrompt, + pluginIdPrompt, +} from './common/prompts'; +import { executePluginPackageTemplate } from './common/tasks'; + +type Options = { + id: string; + moduleId: string; + owner?: string; + codeOwnersPath?: string; +}; + +export const backendModule = createFactory({ + name: 'backend-module', + description: 'A new backend module', + optionsDiscovery: async () => ({ + codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), + }), + optionsPrompts: [pluginIdPrompt(), moduleIdIdPrompt(), ownerPrompt()], + async create(options: Options, ctx: CreateContext) { + const { id: pluginId, moduleId } = options; + const dirName = `${pluginId}-backend-module-${moduleId}`; + const name = ctx.scope + ? `@${ctx.scope}/plugin-${dirName}` + : `backstage-plugin-${dirName}`; + + Task.log(); + Task.log(`Creating backend module ${chalk.cyan(name)}`); + + const targetDir = ctx.isMonoRepo + ? paths.resolveTargetRoot('plugins', dirName) + : paths.resolveTargetRoot(`backstage-plugin-${dirName}`); + + const moduleCamelCase = camelCase(moduleId); + const modulePascalCase = + moduleCamelCase[0].toUpperCase() + moduleCamelCase.slice(1); + const moduleVar = `${camelCase(pluginId)}Module${modulePascalCase}`; + await executePluginPackageTemplate(ctx, { + targetDir, + templateName: 'default-backend-module', + values: { + pluginId, + moduleId, + name, + moduleVar, + packageVersion: ctx.defaultVersion, + privatePackage: ctx.private, + npmRegistry: ctx.npmRegistry, + }, + }); + + if (await fs.pathExists(paths.resolveTargetRoot('packages/backend'))) { + await Task.forItem('backend', 'adding dependency', async () => { + await addPackageDependency( + paths.resolveTargetRoot('packages/backend/package.json'), + { + dependencies: { + [name]: `^${ctx.defaultVersion}`, + }, + }, + ); + }); + } + + if (options.owner) { + await addCodeownersEntry(`/plugins/${dirName}`, options.owner); + } + + await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); + await Task.forCommand('yarn lint --fix', { + cwd: targetDir, + optional: true, + }); + }, +}); diff --git a/packages/cli/src/lib/new/factories/common/prompts.ts b/packages/cli/src/lib/new/factories/common/prompts.ts index 9c7672ddfb..428ba8f9d3 100644 --- a/packages/cli/src/lib/new/factories/common/prompts.ts +++ b/packages/cli/src/lib/new/factories/common/prompts.ts @@ -33,6 +33,22 @@ export function pluginIdPrompt(): Prompt<{ id: string }> { }; } +export function moduleIdIdPrompt(): Prompt<{ moduleId: string }> { + return { + type: 'input', + name: 'moduleId', + message: 'Enter the ID of the module [required]', + validate: (value: string) => { + if (!value) { + return 'Please enter the ID of the module'; + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return 'Module IDs must be lowercase and contain only letters, digits, and dashes.'; + } + return true; + }, + }; +} + export function ownerPrompt(): Prompt<{ owner?: string; codeOwnersPath?: string; diff --git a/packages/cli/src/lib/new/factories/index.ts b/packages/cli/src/lib/new/factories/index.ts index 232e762b91..a4bcb2b721 100644 --- a/packages/cli/src/lib/new/factories/index.ts +++ b/packages/cli/src/lib/new/factories/index.ts @@ -16,6 +16,7 @@ export { frontendPlugin } from './frontendPlugin'; export { backendPlugin } from './backendPlugin'; +export { backendModule } from './backendModule'; export { webLibraryPackage } from './webLibraryPackage'; export { pluginCommon } from './pluginCommon'; export { pluginNode } from './pluginNode'; diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index bf89842d5f..96b42fcfb7 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -35,6 +35,8 @@ leaving any imports in place. */ import { version as backendCommon } from '../../../../packages/backend-common/package.json'; +import { version as backendPluginApi } from '../../../../packages/backend-plugin-api/package.json'; +import { version as backendTestUtils } from '../../../../packages/backend-test-utils/package.json'; import { version as cli } from '../../../../packages/cli/package.json'; import { version as config } from '../../../../packages/config/package.json'; import { version as coreAppApi } from '../../../../packages/core-app-api/package.json'; @@ -47,6 +49,8 @@ import { version as scaffolderBackend } from '../../../../plugins/scaffolder-bac export const packageVersions: Record = { '@backstage/backend-common': backendCommon, + '@backstage/backend-plugin-api': backendPluginApi, + '@backstage/backend-test-utils': backendTestUtils, '@backstage/cli': cli, '@backstage/config': config, '@backstage/core-app-api': coreAppApi, diff --git a/packages/cli/templates/default-backend-module/.eslintrc.js b/packages/cli/templates/default-backend-module/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli/templates/default-backend-module/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli/templates/default-backend-module/README.md.hbs b/packages/cli/templates/default-backend-module/README.md.hbs new file mode 100644 index 0000000000..ad7669beda --- /dev/null +++ b/packages/cli/templates/default-backend-module/README.md.hbs @@ -0,0 +1,5 @@ +# {{name}} + +The {{moduleId}} backend module for the {{pluginId}} plugin. + +_This plugin was created through the Backstage CLI_ diff --git a/packages/cli/templates/default-backend-module/package.json.hbs b/packages/cli/templates/default-backend-module/package.json.hbs new file mode 100644 index 0000000000..07eee73d50 --- /dev/null +++ b/packages/cli/templates/default-backend-module/package.json.hbs @@ -0,0 +1,42 @@ +{ + "name": "{{name}}", + "description": "The {{moduleId}} backend module for the {{pluginId}} plugin.", + "version": "{{packageVersion}}", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", +{{#if privatePackage}} + "private": {{privatePackage}}, +{{/if}} + "publishConfig": { +{{#if npmRegistry}} + "registry": "{{npmRegistry}}", +{{/if}} + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "{{versionQuery '@backstage/backend-common'}}", + "@backstage/backend-plugin-api": "{{versionQuery '@backstage/backend-plugin-api'}}" + }, + "devDependencies": { + "@backstage/backend-test-utils": "{{versionQuery '@backstage/backend-test-utils'}}", + "@backstage/cli": "{{versionQuery '@backstage/cli'}}" + }, + "files": [ + "dist" + ] +} diff --git a/packages/cli/templates/default-backend-module/src/index.ts.hbs b/packages/cli/templates/default-backend-module/src/index.ts.hbs new file mode 100644 index 0000000000..0cf1f00d42 --- /dev/null +++ b/packages/cli/templates/default-backend-module/src/index.ts.hbs @@ -0,0 +1,8 @@ +/***/ +/** + * The {{moduleId}} backend module for the {{pluginId}} plugin. + * + * @packageDocumentation + */ + +export { {{moduleVar}} } from './module'; diff --git a/packages/cli/templates/default-backend-module/src/module.ts.hbs b/packages/cli/templates/default-backend-module/src/module.ts.hbs new file mode 100644 index 0000000000..8c895a49c0 --- /dev/null +++ b/packages/cli/templates/default-backend-module/src/module.ts.hbs @@ -0,0 +1,14 @@ +import { coreServices, createBackendModule } from '@backstage/backend-plugin-api'; + +export const {{moduleVar}} = createBackendModule({ + pluginId: '{{pluginId}}', + moduleId: '{{moduleId}}', + register(reg) { + reg.registerInit({ + deps: { logger: coreServices.logger }, + async init({ logger }) { + logger.info('Hello World!') + }, + }); + }, +}); diff --git a/packages/cli/templates/default-backend-module/tsconfig.json b/packages/cli/templates/default-backend-module/tsconfig.json new file mode 100644 index 0000000000..5ae9aeb62d --- /dev/null +++ b/packages/cli/templates/default-backend-module/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "dist-types", + "rootDir": "." + } +} 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 844a951d05..992599ef6d 100644 --- a/packages/cli/templates/default-common-plugin-package/package.json.hbs +++ b/packages/cli/templates/default-common-plugin-package/package.json.hbs @@ -20,6 +20,7 @@ "backstage": { "role": "common-library" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index 463fc6ea90..011a50ba37 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -18,6 +18,7 @@ "backstage": { "role": "frontend-plugin" }, + "sideEffects": false, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", @@ -47,7 +48,6 @@ "@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '5.10.1'}}", "@testing-library/react": "{{versionQuery '@testing-library/react' '12.1.3'}}", "@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '14.0.0'}}", - "@types/node": "{{versionQuery '@types/node' '16.11.26'}}", "msw": "{{versionQuery 'msw' '1.0.0'}}" }, "files": [ 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 e0ee408eda..230ecc5f03 100644 --- a/packages/cli/templates/default-react-plugin-package/package.json.hbs +++ b/packages/cli/templates/default-react-plugin-package/package.json.hbs @@ -19,6 +19,7 @@ "backstage": { "role": "web-library" }, + "sideEffects": false, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", diff --git a/packages/cli/templates/web-library-package/package.json.hbs b/packages/cli/templates/web-library-package/package.json.hbs index 957efef167..f9f703f523 100644 --- a/packages/cli/templates/web-library-package/package.json.hbs +++ b/packages/cli/templates/web-library-package/package.json.hbs @@ -18,6 +18,7 @@ "backstage": { "role": "web-library" }, + "sideEffects": false, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", diff --git a/packages/codemods/catalog-info.yaml b/packages/codemods/catalog-info.yaml new file mode 100644 index 0000000000..8e5739bd1a --- /dev/null +++ b/packages/codemods/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-codemods + title: '@backstage/codemods' + description: A collection of codemods for Backstage projects +spec: + lifecycle: experimental + type: backstage-cli + owner: maintainers diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index b02d08495a..aab170e602 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,128 @@ # @backstage/config-loader +## 1.5.0-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.5.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.5.0-next.0 + +### Minor Changes + +- 9606ba0939e6: Deep visibility now also applies to values that are not covered by the configuration schema. + + For example, given the following configuration schema: + + ```ts + // plugins/a/config.schema.ts + export interface Config { + /** @deepVisibility frontend */ + a?: unknown; + } + + // plugins/a/config.schema.ts + export interface Config { + a?: { + b?: string; + }; + } + ``` + + All values under `a` are now visible to the frontend, while previously only `a` and `a/b` would've been visible. + +### Patch Changes + +- f9657b891b00: Do not unnecessarily notify subscribers when no-op updates to config happen +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.4.0 + +### Minor Changes + +- 2f1859585998: Loading invalid TypeScript configuration schemas will now throw an error rather than silently being ignored. + + In particular this includes defining any additional types other than `Config` in the schema file, or use of unsupported types such as `Record` or `Partial`. + +- cd514545d1d0: Adds a new `deepVisibility` schema keyword that sets child visibility recursively to the defined value, respecting preexisting values or child `deepVisibility`. + + Example usage: + + ```ts + export interface Config { + /** + * Enforces a default of `secret` instead of `backend` for this object. + * @deepVisibility secret + */ + mySecretProperty: { + type: 'object'; + properties: { + secretValue: { + type: 'string'; + }; + + verySecretProperty: { + type: 'string'; + }; + }; + }; + } + ``` + + Example of a schema that would not be allowed: + + ```ts + export interface Config { + /** + * Set the top level property to secret, enforcing a default of `secret` instead of `backend` for this object. + * @deepVisibility secret + */ + mySecretProperty: { + type: 'object'; + properties: { + frontendUrl: { + /** + * We can NOT override the visibility to reveal a property to the front end. + * @visibility frontend + */ + type: 'string'; + }; + + verySecretProperty: { + type: 'string'; + }; + }; + }; + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + ## 1.4.0-next.1 ### Minor Changes diff --git a/packages/config-loader/catalog-info.yaml b/packages/config-loader/catalog-info.yaml new file mode 100644 index 0000000000..4dfe9c1e59 --- /dev/null +++ b/packages/config-loader/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-config-loader + title: '@backstage/config-loader' + description: Config loading functionality used by Backstage backend, and CLI +spec: + lifecycle: production + type: backstage-node-library + owner: maintainers diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 5e82c0bb76..56a8a0db81 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "1.4.0-next.1", + "version": "1.5.0-next.2", "publishConfig": { "access": "public", "main": "dist/index.cjs.js", @@ -56,7 +56,6 @@ "@backstage/cli": "workspace:^", "@types/json-schema-merge-allof": "^0.6.0", "@types/mock-fs": "^4.10.0", - "@types/node": "^16.11.26", "@types/yup": "^0.29.13", "mock-fs": "^5.1.0", "msw": "^1.0.0", diff --git a/packages/config-loader/src/schema/compile.test.ts b/packages/config-loader/src/schema/compile.test.ts index 914a813d9f..ff0a9543aa 100644 --- a/packages/config-loader/src/schema/compile.test.ts +++ b/packages/config-loader/src/schema/compile.test.ts @@ -39,6 +39,7 @@ describe('compileConfigSchemas', () => { }, ], visibilityByDataPath: new Map(), + deepVisibilityByDataPath: new Map(), visibilityBySchemaPath: new Map(), deprecationByDataPath: new Map(), }); @@ -53,6 +54,7 @@ describe('compileConfigSchemas', () => { }, ], visibilityByDataPath: new Map(), + deepVisibilityByDataPath: new Map(), visibilityBySchemaPath: new Map(), deprecationByDataPath: new Map(), }); @@ -106,6 +108,7 @@ describe('compileConfigSchemas', () => { '/d/0': 'frontend', }), ), + deepVisibilityByDataPath: new Map(), visibilityBySchemaPath: new Map( Object.entries({ '/properties/a': 'frontend', @@ -166,6 +169,7 @@ describe('compileConfigSchemas', () => { '/b': 'deprecation reason for b', }), ), + deepVisibilityByDataPath: new Map(), visibilityByDataPath: new Map(), visibilityBySchemaPath: new Map(), }); @@ -221,6 +225,7 @@ describe('compileConfigSchemas', () => { '//circleci/api/headers/Circle-Token': 'secret', }), ), + deepVisibilityByDataPath: new Map(), visibilityBySchemaPath: new Map( Object.entries({ '/properties//circleci/api/properties/headers/properties/Circle-Token': @@ -277,18 +282,18 @@ describe('deepVisibility', () => { ).toEqual({ visibilityByDataPath: new Map( Object.entries({ - '': 'secret', '/b': 'secret', - '/d': 'secret', - '/d/0': 'secret', + }), + ), + deepVisibilityByDataPath: new Map( + Object.entries({ + '': 'secret', }), ), visibilityBySchemaPath: new Map( Object.entries({ '': 'secret', '/properties/b': 'secret', - '/properties/d': 'secret', - '/properties/d/items': 'secret', }), ), deprecationByDataPath: new Map(), @@ -339,26 +344,6 @@ describe('deepVisibility', () => { it('should throw when children have a different deepVisibility', () => { expect(() => compileConfigSchemas([ - { - path: 'a1', - value: { - type: 'object', - properties: { - a: { - type: 'object', - properties: { - a: { type: 'string' }, - }, - }, - b: { type: 'string', visibility: 'backend' }, - c: { type: 'string' }, - d: { - type: 'array', - items: { type: 'string' }, - }, - }, - }, - }, { path: 'a2', value: { @@ -368,7 +353,7 @@ describe('deepVisibility', () => { a: { type: 'object', properties: { - a: { type: 'string', visibility: 'frontend' }, + a: { type: 'string', deepVisibility: 'frontend' }, }, }, b: { type: 'string', visibility: 'secret' }, @@ -386,6 +371,28 @@ describe('deepVisibility', () => { ); }); + it('should throw when the same schema node has a conflicting deepVisibility', () => { + expect(() => + compileConfigSchemas([ + { + path: 'a2', + value: { + type: 'object', + properties: { + a: { + type: 'string', + deepVisibility: 'secret', + visibility: 'frontend', + }, + }, + }, + }, + ]), + ).toThrow( + `Config schema visibility is both 'frontend' and 'secret' for /properties/a`, + ); + }); + it('should throw when ancestor and children have a different deepVisibility', () => { expect(() => compileConfigSchemas([ diff --git a/packages/config-loader/src/schema/compile.ts b/packages/config-loader/src/schema/compile.ts index 5d448a590a..fd39b0def9 100644 --- a/packages/config-loader/src/schema/compile.ts +++ b/packages/config-loader/src/schema/compile.ts @@ -28,6 +28,9 @@ import { import { SchemaObject } from 'json-schema-traverse'; import { normalizeAjvPath } from './utils'; +// Used to keep track of the internal deepVisibility inherited through the schema. +const inheritedVisibility = Symbol('inherited-visibility'); + /** * This takes a collection of Backstage configuration schemas from various * sources and compiles them down into a single schema validation function. @@ -45,6 +48,7 @@ export function compileConfigSchemas( // output during validation. We work around this by having this extra piece // of state that we reset before each validation. const visibilityByDataPath = new Map(); + const deepVisibilityByDataPath = new Map(); const deprecationByDataPath = new Map(); const ajv = new Ajv({ @@ -87,6 +91,18 @@ export function compileConfigSchemas( */ enum: ['frontend', 'secret'], }, + compile(visibility: 'frontend' | 'secret') { + return (_data, context) => { + if (context?.instancePath === undefined) { + return false; + } + if (visibility) { + const normalizedPath = normalizeAjvPath(context.instancePath); + deepVisibilityByDataPath.set(normalizedPath, visibility); + } + return true; + }; + }, }) .removeKeyword('deprecated') // remove `deprecated` keyword so that we can implement our own compiler .addKeyword({ @@ -118,37 +134,34 @@ export function compileConfigSchemas( traverse( merged, ( - schema: SchemaObject, + schema: SchemaObject & { [inheritedVisibility]?: ConfigVisibility }, jsonPtr: string, _1: any, _2: any, _3?: any, - parentSchema?: SchemaObject, + parentSchema?: SchemaObject & { + [inheritedVisibility]?: ConfigVisibility; + }, ) => { // Inherit parent deepVisibility if we don't define one ourselves. - if (parentSchema?.deepVisibility) { - schema.deepVisibility ??= parentSchema?.deepVisibility; - } + // This is used to detect situations where conflicting deep visibilities are set. + schema[inheritedVisibility] ??= + schema?.deepVisibility ?? parentSchema?.[inheritedVisibility]; - // Apply deep visibility to self. - if (schema?.deepVisibility) { - // This runs before we compile the AJV so visibilityByDataPath has the - // correct data. - schema.visibility ??= schema.deepVisibility; - - if (parentSchema) { - /** - * Validate that we're not trying to override a child's visibility - * by setting the parent deep visibility. - */ - const values = [schema.visibility, parentSchema.visibility]; - const hasFrontend = values.some(e => e === 'frontend'); - const hasSecret = values.some(e => e === 'secret'); - if (hasFrontend && hasSecret) { - throw new Error( - `Config schema visibility is both 'frontend' and 'secret' for ${jsonPtr}`, - ); - } + if (schema[inheritedVisibility]) { + // Validate that we're not trying to set a conflicting visibility. This can be done + // either by setting a conflicting visibility directly or deep visibility + const values = [ + schema.visibility, + schema[inheritedVisibility], + parentSchema?.[inheritedVisibility], + ]; + const hasFrontend = values.some(e => e === 'frontend'); + const hasSecret = values.some(e => e === 'secret'); + if (hasFrontend && hasSecret) { + throw new Error( + `Config schema visibility is both 'frontend' and 'secret' for ${jsonPtr}`, + ); } } @@ -171,12 +184,16 @@ export function compileConfigSchemas( if (schema.visibility && schema.visibility !== 'backend') { visibilityBySchemaPath.set(normalizeAjvPath(path), schema.visibility); } + if (schema.deepVisibility) { + visibilityBySchemaPath.set(normalizeAjvPath(path), schema.deepVisibility); + } }); return configs => { const config = ConfigReader.fromConfigs(configs).get(); visibilityByDataPath.clear(); + deepVisibilityByDataPath.clear(); const valid = validate(config); @@ -184,6 +201,7 @@ export function compileConfigSchemas( return { errors: validate.errors ?? [], visibilityByDataPath: new Map(visibilityByDataPath), + deepVisibilityByDataPath: new Map(deepVisibilityByDataPath), visibilityBySchemaPath, deprecationByDataPath, }; @@ -191,6 +209,7 @@ export function compileConfigSchemas( return { visibilityByDataPath: new Map(visibilityByDataPath), + deepVisibilityByDataPath: new Map(deepVisibilityByDataPath), visibilityBySchemaPath, deprecationByDataPath, }; diff --git a/packages/config-loader/src/schema/filtering.test.ts b/packages/config-loader/src/schema/filtering.test.ts index 3a5de7e2c0..4beed367ea 100644 --- a/packages/config-loader/src/schema/filtering.test.ts +++ b/packages/config-loader/src/schema/filtering.test.ts @@ -30,6 +30,8 @@ const data = { b: { s: true, }, + df: { a: 1, b: [{ c: 1 }, '1'], d: { e: 'x' } }, + ds: { a: 1, b: [{ c: 1 }, '1'], d: { e: 'x' } }, }, arrF: [{ never: 'here' }], arrB: [{ never: 'here' }], @@ -64,6 +66,13 @@ const visibility = new Map( }), ); +const deepVisibility = new Map( + Object.entries({ + '/obj/df': 'frontend', + '/obj/ds': 'secret', + }), +); + const deprecations = new Map( Object.entries({ '/arr': 'deprecated array', @@ -92,6 +101,14 @@ describe('filterByVisibility', () => { 'objArr[1].s', 'obj.f', 'obj.b.s', + 'obj.df.a', + 'obj.df.b[0].c', + 'obj.df.b[1]', + 'obj.df.d.e', + 'obj.ds.a', + 'obj.ds.b[0].c', + 'obj.ds.b[1]', + 'obj.ds.d.e', 'arrF[0].never', 'arrB[0].never', 'arrS[0].never', @@ -107,7 +124,7 @@ describe('filterByVisibility', () => { data: { arr: ['f'], objArr: [{ f: 1 }, { f: 4 }], - obj: { f: 'a' }, + obj: { f: 'a', df: { a: 1, b: [{ c: 1 }, '1'], d: { e: 'x' } } }, arrF: [], objF: {}, arrU: ['f', 'b'], @@ -121,6 +138,10 @@ describe('filterByVisibility', () => { 'objArr[1].b', 'objArr[1].s', 'obj.b.s', + 'obj.ds.a', + 'obj.ds.b[0].c', + 'obj.ds.b[1]', + 'obj.ds.d.e', 'arrF[0].never', 'arrB[0].never', 'arrS[0].never', @@ -137,7 +158,7 @@ describe('filterByVisibility', () => { arr: ['b'], arrU: ['t'], objArr: [{ b: 2 }, { b: 5 }], - obj: { b: {} }, + obj: { b: {}, df: {}, ds: {} }, arrF: [{ never: 'here' }], arrB: [{ never: 'here' }], arrS: [{ never: 'here' }], @@ -156,6 +177,14 @@ describe('filterByVisibility', () => { 'objArr[1].s', 'obj.f', 'obj.b.s', + 'obj.df.a', + 'obj.df.b[0].c', + 'obj.df.b[1]', + 'obj.df.d.e', + 'obj.ds.a', + 'obj.ds.b[0].c', + 'obj.ds.b[1]', + 'obj.ds.d.e', ], }, ], @@ -165,7 +194,10 @@ describe('filterByVisibility', () => { data: { arr: ['s'], objArr: [{ s: 3 }, { s: 6 }], - obj: { b: { s: true } }, + obj: { + b: { s: true }, + ds: { a: 1, b: [{ c: 1 }, '1'], d: { e: 'x' } }, + }, arrS: [], objS: {}, }, @@ -180,6 +212,10 @@ describe('filterByVisibility', () => { 'objArr[1].f', 'objArr[1].b', 'obj.f', + 'obj.df.a', + 'obj.df.b[0].c', + 'obj.df.b[1]', + 'obj.df.d.e', 'arrF[0].never', 'arrB[0].never', 'arrS[0].never', @@ -196,6 +232,7 @@ describe('filterByVisibility', () => { data, filter, visibility, + deepVisibility, deprecations, undefined, true, @@ -210,6 +247,7 @@ describe('filterByVisibility', () => { data, [], visibility, + deepVisibility, deprecations, undefined, true, diff --git a/packages/config-loader/src/schema/filtering.ts b/packages/config-loader/src/schema/filtering.ts index 1c2d0c122a..1b7bbf43c1 100644 --- a/packages/config-loader/src/schema/filtering.ts +++ b/packages/config-loader/src/schema/filtering.ts @@ -31,6 +31,7 @@ export function filterByVisibility( data: JsonObject, includeVisibilities: ConfigVisibility[], visibilityByDataPath: Map, + deepVisibilityByDataPath: Map, deprecationByDataPath: Map, transformFunc?: TransformFunc, withFilteredKeys?: boolean, @@ -47,11 +48,17 @@ export function filterByVisibility( jsonVal: JsonValue, visibilityPath: string, // Matches the format we get from ajv filterPath: string, // Matches the format of the ConfigReader + inheritedVisibility: ConfigVisibility, ): JsonValue | undefined { const visibility = - visibilityByDataPath.get(visibilityPath) ?? DEFAULT_CONFIG_VISIBILITY; + visibilityByDataPath.get(visibilityPath) ?? inheritedVisibility; const isVisible = includeVisibilities.includes(visibility); + // If a deep visibility is set for our current path, then we that as our + // default visibility for all children until we encounter a different deep visibility + const newInheritedVisibility = + deepVisibilityByDataPath.get(visibilityPath) ?? inheritedVisibility; + // deprecated keys are added regardless of visibility indicator const deprecation = deprecationByDataPath.get(visibilityPath); if (deprecation) { @@ -84,7 +91,12 @@ export function filterByVisibility( path = `${visibilityPath}/${index}`; } - const out = transform(value, path, `${filterPath}[${index}]`); + const out = transform( + value, + path, + `${filterPath}[${index}]`, + newInheritedVisibility, + ); if (out !== undefined) { arr.push(out); @@ -108,6 +120,7 @@ export function filterByVisibility( value, `${visibilityPath}/${key}`, filterPath ? `${filterPath}.${key}` : key, + newInheritedVisibility, ); if (out !== undefined) { outObj[key] = out; @@ -124,7 +137,8 @@ export function filterByVisibility( return { filteredKeys: withFilteredKeys ? filteredKeys : undefined, deprecatedKeys: withDeprecatedKeys ? deprecatedKeys : undefined, - data: (transform(data, '', '') as JsonObject) ?? {}, + data: + (transform(data, '', '', DEFAULT_CONFIG_VISIBILITY) as JsonObject) ?? {}, }; } diff --git a/packages/config-loader/src/schema/load.ts b/packages/config-loader/src/schema/load.ts index 8ef971184f..885da44116 100644 --- a/packages/config-loader/src/schema/load.ts +++ b/packages/config-loader/src/schema/load.ts @@ -122,6 +122,7 @@ export async function loadConfigSchema( data, visibility, result.visibilityByDataPath, + result.deepVisibilityByDataPath, result.deprecationByDataPath, valueTransform, withFilteredKeys, @@ -135,6 +136,7 @@ export async function loadConfigSchema( data, Array.from(CONFIG_VISIBILITIES), result.visibilityByDataPath, + result.deepVisibilityByDataPath, result.deprecationByDataPath, valueTransform, withFilteredKeys, diff --git a/packages/config-loader/src/schema/types.ts b/packages/config-loader/src/schema/types.ts index 95576cf5aa..dc8da64038 100644 --- a/packages/config-loader/src/schema/types.ts +++ b/packages/config-loader/src/schema/types.ts @@ -75,6 +75,13 @@ type ValidationResult = { */ visibilityByDataPath: Map; + /** + * The configuration deep visibilities that were discovered during validation. + * + * The path in the key uses the form `////` + */ + deepVisibilityByDataPath: Map; + /** * The configuration visibilities that were discovered during validation. * diff --git a/packages/config-loader/src/sources/ObservableConfigProxy.test.ts b/packages/config-loader/src/sources/ObservableConfigProxy.test.ts index d1fccf3e80..f072536489 100644 --- a/packages/config-loader/src/sources/ObservableConfigProxy.test.ts +++ b/packages/config-loader/src/sources/ObservableConfigProxy.test.ts @@ -25,21 +25,17 @@ describe('ObservableConfigProxy', () => { const sub = config.subscribe(fn); expect(config.getOptionalNumber('x')).toBe(undefined); - config.setConfig(new ConfigReader({})); - expect(fn).toHaveBeenCalledTimes(1); - expect(config.getOptionalNumber('x')).toBe(undefined); - config.setConfig(new ConfigReader({ x: 1 })); - expect(fn).toHaveBeenCalledTimes(2); + expect(fn).toHaveBeenCalledTimes(1); expect(config.getOptionalNumber('x')).toBe(1); config.setConfig(new ConfigReader({ x: 3 })); - expect(fn).toHaveBeenCalledTimes(3); + expect(fn).toHaveBeenCalledTimes(2); sub.unsubscribe(); expect(config.getOptionalNumber('x')).toBe(3); config.setConfig(new ConfigReader({ x: 5 })); - expect(fn).toHaveBeenCalledTimes(3); + expect(fn).toHaveBeenCalledTimes(2); expect(config.getOptionalNumber('x')).toBe(5); }); @@ -58,18 +54,10 @@ describe('ObservableConfigProxy', () => { expect(config2.getOptionalNumber('x')).toBe(undefined); expect(config3.getOptionalNumber('x')).toBe(undefined); - config1.setConfig(new ConfigReader({})); + config1.setConfig(new ConfigReader({ x: 1, a: { x: 2, b: { x: 3 } } })); expect(fn1).toHaveBeenCalledTimes(1); expect(fn2).toHaveBeenCalledTimes(1); expect(fn3).toHaveBeenCalledTimes(1); - expect(config1.getOptionalNumber('x')).toBe(undefined); - expect(config2.getOptionalNumber('x')).toBe(undefined); - expect(config3.getOptionalNumber('x')).toBe(undefined); - - config1.setConfig(new ConfigReader({ x: 1, a: { x: 2, b: { x: 3 } } })); - expect(fn1).toHaveBeenCalledTimes(2); - expect(fn2).toHaveBeenCalledTimes(2); - expect(fn3).toHaveBeenCalledTimes(2); expect(config1.getNumber('x')).toBe(1); expect(config2.getNumber('x')).toBe(2); expect(config3.getNumber('x')).toBe(3); @@ -79,9 +67,9 @@ describe('ObservableConfigProxy', () => { sub3.unsubscribe(); config1.setConfig(new ConfigReader({ x: 4, a: { x: 5, b: { x: 6 } } })); - expect(fn1).toHaveBeenCalledTimes(2); - expect(fn2).toHaveBeenCalledTimes(2); - expect(fn3).toHaveBeenCalledTimes(2); + expect(fn1).toHaveBeenCalledTimes(1); + expect(fn2).toHaveBeenCalledTimes(1); + expect(fn3).toHaveBeenCalledTimes(1); expect(config1.getNumber('x')).toBe(4); expect(config2.getNumber('x')).toBe(5); expect(config3.getNumber('x')).toBe(6); @@ -140,4 +128,43 @@ describe('ObservableConfigProxy', () => { 'Only the root config can be closed', ); }); + + it('should only notify subscribers when the config data actually changes (ignoring key order)', () => { + const config = ObservableConfigProxy.create(new AbortController()); + const fn = jest.fn(); + config.subscribe(fn); + + expect(config.getOptionalNumber('a')).toBe(undefined); + + config.setConfig(new ConfigReader({})); + expect(fn).toHaveBeenCalledTimes(0); + expect(config.getOptionalNumber('a')).toBe(undefined); + + config.setConfig( + new ConfigReader({ + a: 1, + b: 1, + }), + ); + expect(fn).toHaveBeenCalledTimes(1); + expect(config.getOptionalNumber('a')).toBe(1); + + config.setConfig( + new ConfigReader({ + b: 1, + a: 1, + }), + ); + expect(fn).toHaveBeenCalledTimes(1); + expect(config.getOptionalNumber('a')).toBe(1); + + config.setConfig( + new ConfigReader({ + b: 1, + a: 2, + }), + ); + expect(fn).toHaveBeenCalledTimes(2); + expect(config.getOptionalNumber('a')).toBe(2); + }); }); diff --git a/packages/config-loader/src/sources/ObservableConfigProxy.ts b/packages/config-loader/src/sources/ObservableConfigProxy.ts index 59be836160..3d2084beac 100644 --- a/packages/config-loader/src/sources/ObservableConfigProxy.ts +++ b/packages/config-loader/src/sources/ObservableConfigProxy.ts @@ -16,6 +16,7 @@ import { Config, ConfigReader } from '@backstage/config'; import { JsonValue } from '@backstage/types'; +import isEqual from 'lodash/isEqual'; export class ObservableConfigProxy implements Config { private config: Config = new ConfigReader({}); @@ -40,12 +41,22 @@ export class ObservableConfigProxy implements Config { if (this.parent) { throw new Error('immutable'); } + + // We only notify subscribers if the data contents of the config actually + // changed. If they didn't, there's no point in callers trying to re-read + // them. However we still want to replace the local config object, since its + // runtime implementation could be entirely different. + const changed = !isEqual(this.config.get(), config.get()); + this.config = config; - for (const subscriber of this.subscribers) { - try { - subscriber(); - } catch (error) { - console.error(`Config subscriber threw error, ${error}`); + + if (changed) { + for (const subscriber of this.subscribers) { + try { + subscriber(); + } catch (error) { + console.error(`Config subscriber threw error, ${error}`); + } } } } diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md index 9f2f38c8ee..5960c5da49 100644 --- a/packages/config/CHANGELOG.md +++ b/packages/config/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/config +## 1.1.0-next.1 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.1.0-next.0 + +### Minor Changes + +- 62f448edb0b5: Added a `readDurationFromConfig` function + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + ## 1.0.8 ### Patch Changes diff --git a/packages/config/api-report.md b/packages/config/api-report.md index df9baeb4cd..ea9617b2a3 100644 --- a/packages/config/api-report.md +++ b/packages/config/api-report.md @@ -3,6 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { Config as Config_2 } from '@backstage/config'; +import { HumanDuration } from '@backstage/types'; import type { JsonArray as JsonArray_2 } from '@backstage/types'; import { JsonObject as JsonObject_2 } from '@backstage/types'; import type { JsonPrimitive as JsonPrimitive_2 } from '@backstage/types'; @@ -80,4 +82,12 @@ export type JsonPrimitive = JsonPrimitive_2; // @public @deprecated export type JsonValue = JsonValue_2; + +// @public +export function readDurationFromConfig( + config: Config_2, + options?: { + key?: string; + }, +): HumanDuration; ``` diff --git a/packages/config/catalog-info.yaml b/packages/config/catalog-info.yaml new file mode 100644 index 0000000000..c077d9d47b --- /dev/null +++ b/packages/config/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-config + title: '@backstage/config' + description: Config API used by Backstage core, backend, and CLI +spec: + lifecycle: production + type: backstage-common-library + owner: maintainers diff --git a/packages/config/package.json b/packages/config/package.json index 4470e9719e..359791a969 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config", "description": "Config API used by Backstage core, backend, and CLI", - "version": "1.0.8", + "version": "1.1.0-next.1", "publishConfig": { "access": "public", "main": "dist/index.cjs.js", @@ -23,6 +23,7 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", @@ -32,13 +33,13 @@ "clean": "backstage-cli package clean" }, "dependencies": { + "@backstage/errors": "workspace:^", "@backstage/types": "workspace:^", "lodash": "^4.17.21" }, "devDependencies": { "@backstage/cli": "workspace:^", - "@backstage/test-utils": "workspace:^", - "@types/node": "^16.0.0" + "@backstage/test-utils": "workspace:^" }, "files": [ "dist" diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index c8800d0ad2..c6254e97ba 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -26,5 +26,6 @@ export type { JsonPrimitive, JsonValue, } from './deprecatedTypes'; +export { readDurationFromConfig } from './readDurationFromConfig'; export { ConfigReader } from './reader'; export type { AppConfig, Config } from './types'; diff --git a/packages/config/src/readDurationFromConfig.test.ts b/packages/config/src/readDurationFromConfig.test.ts new file mode 100644 index 0000000000..3da790ddcf --- /dev/null +++ b/packages/config/src/readDurationFromConfig.test.ts @@ -0,0 +1,128 @@ +/* + * 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 { + readDurationFromConfig, + propsOfHumanDuration, +} from './readDurationFromConfig'; +import { ConfigReader } from './reader'; + +describe('readDurationFromConfig', () => { + it('reads all known keys', () => { + const config = new ConfigReader({ + milliseconds: 1, + seconds: 2, + minutes: 3, + hours: 4, + days: 5, + weeks: 6, + months: 7, + years: 8, + }); + expect(readDurationFromConfig(config)).toEqual({ + milliseconds: 1, + seconds: 2, + minutes: 3, + hours: 4, + days: 5, + weeks: 6, + months: 7, + years: 8, + }); + }); + + it('reads all known keys, for a subkey', () => { + const config = new ConfigReader({ + sub: { + key: { + milliseconds: 1, + seconds: 2, + minutes: 3, + hours: 4, + days: 5, + weeks: 6, + months: 7, + years: 8, + }, + }, + }); + expect(readDurationFromConfig(config, { key: 'sub.key' })).toEqual({ + milliseconds: 1, + seconds: 2, + minutes: 3, + hours: 4, + days: 5, + weeks: 6, + months: 7, + years: 8, + }); + }); + + it('rejects wrong type of target, for a subkey', () => { + const config = new ConfigReader({ + sub: { key: 7 }, + }); + expect(() => readDurationFromConfig(config, { key: 'sub.key' })).toThrow( + "Failed to read duration from config, TypeError: Invalid type in config for key 'sub.key' in 'mock-config', got number, wanted object", + ); + }); + + it('rejects no keys', () => { + const config = new ConfigReader({}); + expect(() => readDurationFromConfig(config)).toThrow( + `Failed to read duration from config, Error: Needs one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'`, + ); + }); + + it('rejects no keys, for a subkey', () => { + const config = new ConfigReader({ sub: { key: {} } }); + expect(() => readDurationFromConfig(config, { key: 'sub.key' })).toThrow( + `Failed to read duration from config at 'sub.key', Error: Needs one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'`, + ); + }); + + it('rejects unknown keys', () => { + const config = new ConfigReader({ + minutes: 3, + invalid: 'value', + }); + expect(() => readDurationFromConfig(config)).toThrow( + `Failed to read duration from config, Error: Unknown property 'invalid'; expected one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'`, + ); + }); + + it.each(propsOfHumanDuration)('rejects non-number %p', prop => { + const config = new ConfigReader({ + [prop]: 'value', + }); + expect(() => readDurationFromConfig(config)).toThrow( + `Failed to read duration from config, Error: Unable to convert config value for key '${prop}' in 'mock-config' to a number`, + ); + }); + + it.each(propsOfHumanDuration)('rejects non-number %p, for a subkey', prop => { + const config = new ConfigReader({ + sub: { + key: { + [prop]: 'value', + }, + }, + }); + expect(() => readDurationFromConfig(config, { key: 'sub.key' })).toThrow( + `Failed to read duration from config, Error: Unable to convert config value for key 'sub.key.${prop}' in 'mock-config' to a number`, + ); + }); +}); diff --git a/packages/config/src/readDurationFromConfig.ts b/packages/config/src/readDurationFromConfig.ts new file mode 100644 index 0000000000..2f59e9a976 --- /dev/null +++ b/packages/config/src/readDurationFromConfig.ts @@ -0,0 +1,101 @@ +/* + * 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 { Config } from '@backstage/config'; +import { InputError } from '@backstage/errors'; +import { HumanDuration } from '@backstage/types'; + +export const propsOfHumanDuration = [ + 'years', + 'months', + 'weeks', + 'days', + 'hours', + 'minutes', + 'seconds', + 'milliseconds', +]; + +/** + * Reads a duration from a config object. + * + * @public + * @remarks + * + * This does not support optionality; if you want to support optional durations, + * you need to first check the presence of the target with `config.has(...)` and + * then call this function. + * + * @param config - A configuration object + * @param key - If specified, read the duration from the given subkey + * under the config object + * @returns A duration object + */ +export function readDurationFromConfig( + config: Config, + options?: { + key?: string; + }, +): HumanDuration { + let root: Config; + let found = false; + const result: Record = {}; + + try { + root = options?.key ? config.getConfig(options.key) : config; + for (const prop of propsOfHumanDuration) { + const value = root.getOptionalNumber(prop); + if (value !== undefined) { + result[prop] = value; + found = true; + } + } + } catch (error) { + // This needs no contextual key prefix since the config reader adds it to + // its own errors + throw new InputError(`Failed to read duration from config, ${error}`); + } + + try { + if (!found) { + const good = propsOfHumanDuration.map(p => `'${p}'`).join(', '); + throw new Error(`Needs one or more of ${good}`); + } + + const invalidProps = root + .keys() + .filter(prop => !propsOfHumanDuration.includes(prop)); + if (invalidProps.length) { + const what = invalidProps.length === 1 ? 'property' : 'properties'; + const bad = invalidProps.map(p => `'${p}'`).join(', '); + const good = propsOfHumanDuration.map(p => `'${p}'`).join(', '); + throw new Error( + `Unknown ${what} ${bad}; expected one or more of ${good}`, + ); + } + } catch (error) { + // For our own errors, even though we don't know where we are anchored in + // the config hierarchy, we try to be helpful and at least add the subkey if + // we have it + let prefix = 'Failed to read duration from config'; + if (options?.key) { + prefix += ` at '${options.key}'`; + } + throw new InputError(`${prefix}, ${error}`); + } + + return result as HumanDuration; +} diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index 1de645fb9e..3cd9fe309e 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,56 @@ # @backstage/core-app-api +## 1.10.0-next.2 + +### Minor Changes + +- 6e30769cc627: Introduced experimental support for internationalization. + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## 1.10.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## 1.10.0-next.0 + +### Minor Changes + +- 18619f793c94: Fixed two bugs in how the `OAuth2Session` type represents the underlying data. The `expiresAt` and `backstageIdentity` are now both optional, since that's what they are in practice. This is not considered a breaking change since it was effectively a bug in the modelling of the state that this type represents, and the type was not used in any other external contract. +- 18619f793c94: The `OAuth` class which is used by all OAuth providers will now consider both the session expiration of both the Backstage identity as well as the upstream identity provider, and refresh the session with either of them is about to expire. + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## 1.9.1 + +### Patch Changes + +- 9ae4e7e63836: Fixed a bug that could cause `navigate` analytics events to be misattributed to the plugin mounted on the root route (e.g. the `home` plugin at `/`) when the route that was navigated to wasn't associated with a routable extension. +- Updated dependencies + - @backstage/core-plugin-api@1.5.3 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + ## 1.9.1-next.0 ### Patch Changes diff --git a/packages/core-app-api/alpha-api-report.md b/packages/core-app-api/alpha-api-report.md new file mode 100644 index 0000000000..f35adfdbaa --- /dev/null +++ b/packages/core-app-api/alpha-api-report.md @@ -0,0 +1,85 @@ +## API Report File for "@backstage/core-app-api" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AppTranslationApi } from '@backstage/core-plugin-api/alpha'; +import { i18n } from 'i18next'; +import { TranslationRef } from '@backstage/core-plugin-api/alpha'; + +// @alpha (undocumented) +export class AppTranslationApiImpl implements AppTranslationApi { + // (undocumented) + addLazyResources>( + translationRef: TranslationRef, + initResources?: Record< + string, + () => Promise<{ + messages: TranslationMessages; + }> + >, + ): void; + // (undocumented) + addResources>( + translationRef: TranslationRef, + initResources?: Record< + string, + TranslationMessages> + >, + ): void; + // (undocumented) + addResourcesByRef>( + translationRef: TranslationRef, + ): void; + // (undocumented) + static create(options?: ExperimentalI18n): AppTranslationApiImpl; + // (undocumented) + getI18n(): i18n; + // (undocumented) + initMessages(options?: ExperimentalI18n): void; +} + +// @alpha (undocumented) +export function createTranslationResource(options: { + ref: T; + messages?: Record>; + lazyMessages: Record< + string, + () => Promise<{ + messages: TranslationMessages; + }> + >; +}): { + ref: T; + messages?: Record> | undefined; + lazyMessages: Record< + string, + () => Promise<{ + messages: TranslationMessages; + }> + >; +}; + +// @alpha (undocumented) +export type ExperimentalI18n = { + supportedLanguages: string[]; + fallbackLanguage?: string | string[]; + messages?: Array<{ + ref: TranslationRef; + messages?: Record>; + lazyMessages: Record< + string, + () => Promise<{ + messages: TranslationMessages; + }> + >; + }>; +}; + +// @alpha (undocumented) +export type TranslationMessages = T extends TranslationRef + ? Partial + : never; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 5746598ffd..e53d2eed83 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -56,6 +56,7 @@ import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; import PropTypes from 'prop-types'; +import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { SessionApi } from '@backstage/core-plugin-api'; @@ -63,6 +64,7 @@ import { SessionState } from '@backstage/core-plugin-api'; import { StorageApi } from '@backstage/core-plugin-api'; import { StorageValueSnapshot } from '@backstage/core-plugin-api'; import { SubRouteRef } from '@backstage/core-plugin-api'; +import { TranslationRef } from '@backstage/core-plugin-api/alpha'; // @public export class AlertApiForwarder implements AlertApi { @@ -113,7 +115,7 @@ export type ApiFactoryScope = 'default' | 'app' | 'static'; // @public export const ApiProvider: { - (props: PropsWithChildren): JSX.Element; + (props: PropsWithChildren): React_2.JSX.Element; propTypes: { apis: PropTypes.Validator< NonNullable< @@ -219,6 +221,20 @@ export type AppOptions = { themes: (Partial & Omit)[]; configLoader?: AppConfigLoader; bindRoutes?(context: { bind: AppRouteBinder }): void; + __experimentalI18n?: { + supportedLanguages: string[]; + fallbackLanguage?: string | string[]; + messages?: Array<{ + ref: TranslationRef; + messages?: Record>; + lazyMessages: Record< + string, + () => Promise<{ + messages: TranslationMessages; + }> + >; + }>; + }; }; // @public @@ -235,7 +251,7 @@ export type AppRouteBinder = < ) => void; // @public -export function AppRouter(props: AppRouterProps): JSX.Element; +export function AppRouter(props: AppRouterProps): React_2.JSX.Element; // @public export interface AppRouterProps { @@ -368,7 +384,9 @@ export type ErrorBoundaryFallbackProps = PropsWithChildren<{ }>; // @public -export const FeatureFlagged: (props: FeatureFlaggedProps) => JSX.Element; +export const FeatureFlagged: ( + props: FeatureFlaggedProps, +) => React_2.JSX.Element; // @public export type FeatureFlaggedProps = { @@ -535,10 +553,10 @@ export type OAuth2Session = { idToken: string; accessToken: string; scopes: Set; - expiresAt: Date; + expiresAt?: Date; }; profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; + backstageIdentity?: BackstageIdentityResponse; }; // @public diff --git a/packages/core-app-api/catalog-info.yaml b/packages/core-app-api/catalog-info.yaml new file mode 100644 index 0000000000..628c5a36d6 --- /dev/null +++ b/packages/core-app-api/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-core-app-api + title: '@backstage/core-app-api' + description: Core app API used by Backstage apps +spec: + lifecycle: production + type: backstage-web-library + owner: maintainers diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 15dca326e0..0dd87d1c5f 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,11 +1,24 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "1.9.1-next.0", + "version": "1.10.0-next.2", "publishConfig": { - "access": "public", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "web-library" @@ -22,6 +35,7 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", @@ -39,7 +53,10 @@ "@types/prop-types": "^15.7.3", "@types/react": "^16.13.1 || ^17.0.0", "history": "^5.0.0", + "i18next": "^22.4.15", + "i18next-browser-languagedetector": "^7.0.2", "prop-types": "^15.7.2", + "react-i18next": "^12.3.1", "react-use": "^17.2.4", "zen-observable": "^0.10.0", "zod": "^3.21.4" @@ -57,7 +74,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", "@types/zen-observable": "^0.8.0", "msw": "^1.0.0", "react-router-beta": "npm:react-router@6.0.0-beta.0", diff --git a/packages/core-app-api/src/alpha.ts b/packages/core-app-api/src/alpha.ts new file mode 100644 index 0000000000..288676f509 --- /dev/null +++ b/packages/core-app-api/src/alpha.ts @@ -0,0 +1,17 @@ +/* + * 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 './apis/implementations/AppTranslationApi'; +export * from './app/TranslationResource'; diff --git a/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslation.test.ts b/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslation.test.ts new file mode 100644 index 0000000000..b6eba8a1c7 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslation.test.ts @@ -0,0 +1,181 @@ +/* + * 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 { createTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { AppTranslationApiImpl } from './AppTranslationImpl'; +import i18next from 'i18next'; + +jest.mock('i18next', () => ({ + createInstance: jest.fn(() => ({ + use: jest.fn(() => ({ + init: jest.fn(), + use: jest.fn(), + addResourceBundle: jest.fn(), + reloadResources: jest.fn(), + emit: jest.fn(), + services: { + languageUtils: { + getFallbackCodes: jest.fn().mockReturnValue(['en']), + }, + }, + options: { + fallbackLng: 'en', + supportedLngs: ['zh', 'en'], + }, + })), + })), +})); + +describe('AppTranslationApiImpl', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should create i18n instance and init with options', () => { + const i18nMock = i18next.createInstance() as any; + const useReturnMock = i18nMock.use(); + jest.spyOn(i18nMock, 'use').mockReturnValue(useReturnMock); + jest.spyOn(i18next, 'createInstance').mockReturnValue(i18nMock); + + const instance = AppTranslationApiImpl.create({ + supportedLanguages: ['en', 'zh'], + }); + + expect(i18next.createInstance).toHaveBeenCalled(); + expect(i18nMock.use).toHaveBeenCalled(); + expect(useReturnMock.init).toHaveBeenCalledWith({ + fallbackLng: 'en', + supportedLngs: ['en', 'zh'], + interpolation: { + escapeValue: false, + }, + react: { + bindI18n: 'loaded languageChanged', + }, + }); + expect(instance).toBeInstanceOf(AppTranslationApiImpl); + }); + + it('should init messages correctly', () => { + const useResourcesMock = jest.spyOn( + AppTranslationApiImpl.prototype, + 'addResources', + ); + const useLazyResourcesMock = jest.spyOn( + AppTranslationApiImpl.prototype, + 'addLazyResources', + ); + const ref = createTranslationRef({ + id: 'ref-id', + messages: { + key1: '', + }, + }); + + const options = { + supportedLanguages: ['en'], + messages: [ + { + ref, + messages: { + en: { key1: 'value1' }, + }, + lazyMessages: { + en: () => Promise.resolve({ key2: 'value2' }), + } as any, + }, + ], + }; + + const instance = AppTranslationApiImpl.create({ + supportedLanguages: ['en'], + }); + instance.initMessages(options); + + expect(useResourcesMock).toHaveBeenCalledWith( + options.messages[0].ref, + options.messages[0].messages, + ); + expect(useLazyResourcesMock).toHaveBeenCalledWith( + options.messages[0].ref, + options.messages[0].lazyMessages, + ); + }); + + it('should useResources correctly', () => { + const i18nMock = i18next.createInstance() as any; + const useReturnMock = i18nMock.use(); + jest.spyOn(i18nMock, 'use').mockReturnValue(useReturnMock); + jest.spyOn(i18next, 'createInstance').mockReturnValue(i18nMock); + + const ref = createTranslationRef({ + id: 'ref-id', + messages: { + key1: 'value1', + }, + resources: { + en: { + key1: 'value2', + }, + }, + }); + + const instance = AppTranslationApiImpl.create({ + supportedLanguages: ['en'], + }); + instance.addResources(ref); + + expect(useReturnMock.addResourceBundle).toHaveBeenCalledWith( + 'en', + 'ref-id', + { key1: 'value2' }, + true, + false, + ); + }); + + it('should useLazyResources correctly', () => { + const i18nMock = i18next.createInstance() as any; + const useReturnMock = i18nMock.use(); + + jest.spyOn(i18nMock, 'use').mockReturnValue(useReturnMock); + jest.spyOn(i18next, 'createInstance').mockReturnValue(i18nMock); + + const ref = createTranslationRef({ + id: 'ref-id', + messages: { + key1: 'value1', + }, + lazyResources: { + en: () => Promise.resolve({ messages: { key1: 'value2' } }), + }, + }); + + const instance = AppTranslationApiImpl.create({ + supportedLanguages: ['en'], + }); + instance.addLazyResources(ref); + setTimeout(() => { + expect(useReturnMock.addResourceBundle).toHaveBeenCalledWith( + 'en', + 'ref-id', + { key1: 'value2' }, + true, + false, + ); + }); + }); +}); diff --git a/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts b/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts new file mode 100644 index 0000000000..69820e7b67 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts @@ -0,0 +1,192 @@ +/* + * 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 { + AppTranslationApi, + TranslationRef, +} from '@backstage/core-plugin-api/alpha'; +import i18next, { type i18n } from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +import { TranslationMessages } from '../../../alpha'; + +/** @alpha */ +export type ExperimentalI18n = { + supportedLanguages: string[]; + fallbackLanguage?: string | string[]; + messages?: Array<{ + ref: TranslationRef; + messages?: Record>; + lazyMessages: Record< + string, + () => Promise<{ messages: TranslationMessages }> + >; + }>; +}; + +/** @alpha */ +export class AppTranslationApiImpl implements AppTranslationApi { + static create(options?: ExperimentalI18n) { + const i18n = i18next.createInstance().use(initReactI18next); + + i18n.use(LanguageDetector); + + i18n.init({ + fallbackLng: options?.fallbackLanguage || 'en', + supportedLngs: options?.supportedLanguages || ['en'], + interpolation: { + escapeValue: false, + }, + react: { + bindI18n: 'loaded languageChanged', + }, + }); + + return new AppTranslationApiImpl(i18n, options); + } + + private readonly cache = new WeakSet(); + private readonly lazyCache = new WeakMap>(); + + getI18n() { + return this.i18n; + } + + initMessages(options?: ExperimentalI18n) { + if (options?.messages?.length) { + options.messages.forEach(appMessage => { + if (appMessage.messages) { + this.addResources(appMessage.ref, appMessage.messages); + } + + if (appMessage.lazyMessages) { + this.addLazyResources(appMessage.ref, appMessage.lazyMessages); + } + }); + } + } + + addResourcesByRef>( + translationRef: TranslationRef, + ): void { + this.addResources(translationRef); + this.addLazyResources(translationRef); + } + + addResources>( + translationRef: TranslationRef, + initResources?: Record< + string, + TranslationMessages> + >, + ) { + const resources = initResources || translationRef.getResources(); + if (!resources || this.cache.has(translationRef)) { + return; + } + this.cache.add(translationRef); + Object.entries(resources).forEach(([language, messages]) => { + this.i18n.addResourceBundle( + language, + translationRef.getId(), + messages, + true, + false, + ); + }); + } + + addLazyResources>( + translationRef: TranslationRef, + initResources?: Record< + string, + () => Promise<{ messages: TranslationMessages }> + >, + ) { + let cache = this.lazyCache.get(translationRef); + + if (!cache) { + cache = new Set(); + this.lazyCache.set(translationRef, cache); + } + + const { + language: currentLanguage, + services, + options, + addResourceBundle, + reloadResources, + } = this.i18n; + + if (cache.has(currentLanguage)) { + return; + } + + const namespace = translationRef.getId(); + const lazyResources = initResources || translationRef.getLazyResources(); + + Promise.allSettled((options.supportedLngs || []).map(addLanguage)).then( + results => { + if (results.some(result => result.status === 'fulfilled')) { + this.i18n.emit('loaded'); + } + }, + ); + + async function addLanguage(language: string) { + if (cache!.has(language)) { + return; + } + + cache!.add(language); + + let loadBackend: Promise | undefined; + + if (services.backendConnector?.backend) { + loadBackend = reloadResources([language], [namespace]); + } + + const loadLazyResources = lazyResources?.[language]; + + if (!loadLazyResources) { + await loadBackend; + return; + } + + const [result] = await Promise.allSettled([ + loadLazyResources(), + loadBackend, + ]); + + if (result.status === 'rejected') { + throw result.reason; + } + + addResourceBundle( + language, + namespace, + result.value.messages, + true, + false, + ); + } + } + + private constructor(private readonly i18n: i18n, options?: ExperimentalI18n) { + this.initMessages(options); + } +} diff --git a/packages/core-app-api/src/apis/implementations/AppTranslationApi/index.ts b/packages/core-app-api/src/apis/implementations/AppTranslationApi/index.ts new file mode 100644 index 0000000000..91d76413d8 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/AppTranslationApi/index.ts @@ -0,0 +1,17 @@ +/* + * 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 './AppTranslationImpl'; diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts index 6f941379a7..8ca818ebd8 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts @@ -76,6 +76,37 @@ describe('OAuth2', () => { ); }); + it('should forward backstage identity', async () => { + getSession = jest.fn().mockResolvedValue({ + providerInfo: { accessToken: 'access-token', expiresAt: theFuture }, + backstageIdentity: { + token: 'a.b.c', + expiresAt: theFuture, + identity: { + type: 'user', + userEntityRef: 'user:default/mock', + ownershipEntityRefs: [], + }, + }, + }); + const oauth2 = OAuth2.create({ + configApi: configApi, + scopeTransform: scopes => scopes.map(scope => `my-prefix/${scope}`), + oauthRequestApi: new MockOAuthApi(), + discoveryApi: UrlPatternDiscovery.compile('http://example.com'), + }); + + await expect(oauth2.getBackstageIdentity()).resolves.toEqual({ + token: 'a.b.c', + expiresAt: theFuture, + identity: { + type: 'user', + userEntityRef: 'user:default/mock', + ownershipEntityRefs: [], + }, + }); + }); + it('should get refreshed id token', async () => { getSession = jest.fn().mockResolvedValue({ providerInfo: { idToken: 'id-token', expiresAt: theFuture }, diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts index fd40a9c8fb..6be7cfc89d 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts @@ -30,6 +30,7 @@ import { SessionState, SessionApi, BackstageIdentityApi, + BackstageUserIdentity, } from '@backstage/core-plugin-api'; import { Observable } from '@backstage/types'; import { OAuth2Session } from './types'; @@ -49,10 +50,14 @@ export type OAuth2Response = { accessToken: string; idToken: string; scope: string; - expiresInSeconds: number; + expiresInSeconds?: number; }; profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; + backstageIdentity: { + token: string; + expiresInSeconds?: number; + identity: BackstageUserIdentity; + }; }; const DEFAULT_PROVIDER = { @@ -92,8 +97,11 @@ export default class OAuth2 environment, provider, oauthRequestApi: oauthRequestApi, - sessionTransform(res: OAuth2Response): OAuth2Session { - return { + sessionTransform({ + backstageIdentity, + ...res + }: OAuth2Response): OAuth2Session { + const session: OAuth2Session = { ...res, providerInfo: { idToken: res.providerInfo.idToken, @@ -102,11 +110,26 @@ export default class OAuth2 scopeTransform, res.providerInfo.scope, ), - expiresAt: new Date( - Date.now() + res.providerInfo.expiresInSeconds * 1000, - ), + expiresAt: res.providerInfo.expiresInSeconds + ? new Date(Date.now() + res.providerInfo.expiresInSeconds * 1000) + : undefined, }, }; + if (backstageIdentity) { + // TODO(Rugvip): This fallback can be removed a few releases after 1.18. It's there to avoid + // breaking deployments that update their frontend before updating their backend. + const expInSec = + backstageIdentity.expiresInSeconds ?? + res.providerInfo.expiresInSeconds; + session.backstageIdentity = { + token: backstageIdentity.token, + identity: backstageIdentity.identity, + expiresAt: expInSec + ? new Date(Date.now() + expInSec * 1000) + : undefined, + }; + } + return session; }, popupOptions, }); @@ -116,9 +139,21 @@ export default class OAuth2 defaultScopes: new Set(defaultScopes), sessionScopes: (session: OAuth2Session) => session.providerInfo.scopes, sessionShouldRefresh: (session: OAuth2Session) => { - const expiresInSec = - (session.providerInfo.expiresAt.getTime() - Date.now()) / 1000; - return expiresInSec < 60 * 5; + // TODO(Rugvip): Optimize to use separate checks for provider vs backstage session expiration + let min = Infinity; + if (session.providerInfo?.expiresAt) { + min = Math.min( + min, + (session.providerInfo.expiresAt.getTime() - Date.now()) / 1000, + ); + } + if (session.backstageIdentity?.expiresAt) { + min = Math.min( + min, + (session.backstageIdentity.expiresAt.getTime() - Date.now()) / 1000, + ); + } + return min < 60 * 5; }, }); diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts index bd3bb95ba6..e51de63dbe 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts @@ -31,8 +31,8 @@ export type OAuth2Session = { idToken: string; accessToken: string; scopes: Set; - expiresAt: Date; + expiresAt?: Date; }; profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; + backstageIdentity?: BackstageIdentityResponse; }; diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index db0d3ea9af..1b68cfd6ca 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -260,6 +260,46 @@ describe('Integration Test', () => { expect(screen.getByText('extLink4: ')).toBeInTheDocument(); }); + it('runs success with __experimentalI18n', async () => { + const app = new AppManager({ + apis: [noOpAnalyticsApi], + defaultApis: [], + themes, + icons, + plugins: [], + components, + configLoader: async () => [], + bindRoutes: ({ bind }) => { + bind(plugin1.externalRoutes, { + extRouteRef1: plugin1RouteRef, + extRouteRef2: plugin2RouteRef, + }); + }, + __experimentalI18n: { + supportedLanguages: ['en'], + }, + }); + + const Provider = app.getProvider(); + const Router = app.getRouter(); + + await renderWithEffects( + + + + } /> + } /> + + + , + ); + + expect(screen.getByText('extLink1: /')).toBeInTheDocument(); + expect(screen.getByText('extLink2: /foo')).toBeInTheDocument(); + expect(screen.getByText('extLink3: ')).toBeInTheDocument(); + expect(screen.getByText('extLink4: ')).toBeInTheDocument(); + }); + it('should wait for the config to load before calling feature flags', async () => { const storageFlags = new LocalStorageFeatureFlags(); jest.spyOn(storageFlags, 'registerFlag'); diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 470f7f8d75..de078c0ffc 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -42,6 +42,10 @@ import { BackstagePlugin, FeatureFlag, } from '@backstage/core-plugin-api'; +import { + AppTranslationApi, + appTranslationApiRef, +} from '@backstage/core-plugin-api/alpha'; import { ApiFactoryRegistry, ApiResolver } from '../apis/system'; import { childDiscoverer, @@ -75,6 +79,8 @@ import { resolveRouteBindings } from './resolveRouteBindings'; import { isReactRouterBeta } from './isReactRouterBeta'; import { InternalAppContext } from './InternalAppContext'; import { AppRouter, getBasePath } from './AppRouter'; +import { AppTranslationProvider } from './AppTranslationProvider'; +import { AppTranslationApiImpl } from '../apis/implementations/AppTranslationApi'; type CompatiblePlugin = | BackstagePlugin @@ -209,6 +215,7 @@ export class AppManager implements BackstageApp { private readonly configLoader?: AppConfigLoader; private readonly defaultApis: Iterable; private readonly bindRoutes: AppOptions['bindRoutes']; + private readonly appTranslationApi: AppTranslationApi; private readonly appIdentityProxy = new AppIdentityProxy(); private readonly apiFactoryRegistry: ApiFactoryRegistry; @@ -224,6 +231,9 @@ export class AppManager implements BackstageApp { this.defaultApis = options.defaultApis ?? []; this.bindRoutes = options.bindRoutes; this.apiFactoryRegistry = new ApiFactoryRegistry(); + this.appTranslationApi = AppTranslationApiImpl.create( + options.__experimentalI18n, + ); } getPlugins(): BackstagePlugin[] { @@ -379,24 +389,26 @@ export class AppManager implements BackstageApp { return ( - - - + + - {children} - - - + + {children} + + + + ); @@ -447,6 +459,11 @@ export class AppManager implements BackstageApp { deps: {}, factory: () => this.appIdentityProxy, }); + this.apiFactoryRegistry.register('static', { + api: appTranslationApiRef, + deps: {}, + factory: () => this.appTranslationApi, + }); // It's possible to replace the feature flag API, but since we must have at least // one implementation we add it here directly instead of through the defaultApis. diff --git a/packages/core-app-api/src/app/AppTranslationProvider.tsx b/packages/core-app-api/src/app/AppTranslationProvider.tsx new file mode 100644 index 0000000000..3d5e7e57e1 --- /dev/null +++ b/packages/core-app-api/src/app/AppTranslationProvider.tsx @@ -0,0 +1,27 @@ +/* + * 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 React, { PropsWithChildren } from 'react'; +import { useApi } from '@backstage/core-plugin-api'; +import { appTranslationApiRef } from '@backstage/core-plugin-api/alpha'; +import { I18nextProvider } from 'react-i18next'; + +/** @alpha */ +export function AppTranslationProvider({ children }: PropsWithChildren<{}>) { + const appTranslationAPi = useApi(appTranslationApiRef); + const i18n = appTranslationAPi.getI18n(); + return {children}; +} diff --git a/packages/core-app-api/src/app/TranslationResource.ts b/packages/core-app-api/src/app/TranslationResource.ts new file mode 100644 index 0000000000..65f1c4622c --- /dev/null +++ b/packages/core-app-api/src/app/TranslationResource.ts @@ -0,0 +1,34 @@ +/* + * 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 { TranslationRef } from '@backstage/core-plugin-api/alpha'; + +/** @alpha */ +export type TranslationMessages = T extends TranslationRef + ? Partial + : never; + +/** @alpha */ +export function createTranslationResource(options: { + ref: T; + messages?: Record>; + lazyMessages: Record< + string, + () => Promise<{ messages: TranslationMessages }> + >; +}) { + return options; +} diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 7f38e2d006..3d4b9cf8c5 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -27,6 +27,7 @@ import { FeatureFlag, } from '@backstage/core-plugin-api'; import { AppConfig } from '@backstage/config'; +import { TranslationRef } from '@backstage/core-plugin-api/alpha'; /** * Props for the `BootErrorPage` component of {@link AppComponents}. @@ -177,6 +178,16 @@ export type AppRouteBinder = < >, ) => void; +/** + * TODO: To be remove when TranslationMessages in packages/core-app-api/src/app/TranslationResource.ts + * come to be public + * + * @ignore + * */ +type TranslationMessages = T extends TranslationRef + ? Partial + : never; + /** * The options accepted by {@link createSpecializedApp}. * @@ -278,6 +289,23 @@ export type AppOptions = { * ``` */ bindRoutes?(context: { bind: AppRouteBinder }): void; + + /** + * TODO: Change to ExperimentalI18n type when packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts + * become to public + */ + __experimentalI18n?: { + supportedLanguages: string[]; + fallbackLanguage?: string | string[]; + messages?: Array<{ + ref: TranslationRef; + messages?: Record>; + lazyMessages: Record< + string, + () => Promise<{ messages: TranslationMessages }> + >; + }>; + }; }; /** diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 4ebd92ceb6..8fb94136e0 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/core-components +## 0.13.5-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- db92d1244897: Updated dependency `rc-progress` to `3.5.1`. +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/version-bridge@1.0.4 + +## 0.13.5-next.1 + +### Patch Changes + +- 0c9907645aab: Fixed an issue causing `StructuredMetadataTable` to crash in case metadata contained `null` values. +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/version-bridge@1.0.4 + +## 0.13.5-next.0 + +### Patch Changes + +- 47782f4bfa5b: Add loading indicator to Table +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/version-bridge@1.0.4 + +## 0.13.4 + +### Patch Changes + +- 3d86be999fdf: Prefer simple `theme.spacing` without string interpolation +- Updated dependencies + - @backstage/core-plugin-api@1.5.3 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/version-bridge@1.0.4 + ## 0.13.4-next.0 ### Patch Changes diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index df34abdd03..54c26039a3 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -54,7 +54,9 @@ import { TooltipProps } from '@material-ui/core/Tooltip'; import { WithStyles } from '@material-ui/core/styles'; // @public -export function AlertDisplay(props: AlertDisplayProps): JSX.Element | null; +export function AlertDisplay( + props: AlertDisplayProps, +): React_2.JSX.Element | null; // @public export type AlertDisplayProps = { @@ -66,7 +68,7 @@ export type AlertDisplayProps = { }; // @public -export function Avatar(props: AvatarProps): JSX.Element; +export function Avatar(props: AvatarProps): React_2.JSX.Element; // @public (undocumented) export type AvatarClassKey = 'avatar'; @@ -94,7 +96,7 @@ export type BackstageOverrides = Overrides & { export type BoldHeaderClassKey = 'root' | 'title' | 'subheader'; // @public -export function BottomLink(props: BottomLinkProps): JSX.Element; +export function BottomLink(props: BottomLinkProps): React_2.JSX.Element; // @public (undocumented) export type BottomLinkClassKey = 'root' | 'boxTitle' | 'arrow'; @@ -109,7 +111,7 @@ export type BottomLinkProps = { // Warning: (ae-forgotten-export) The symbol "Props_19" needs to be exported by the entry point index.d.ts // // @public -export function Breadcrumbs(props: Props_19): JSX.Element; +export function Breadcrumbs(props: Props_19): React_2.JSX.Element; // @public (undocumented) export type BreadcrumbsClickableTextClassKey = 'root'; @@ -120,7 +122,7 @@ export type BreadcrumbsStyledBoxClassKey = 'root'; // Warning: (ae-forgotten-export) The symbol "IconComponentProps" needs to be exported by the entry point index.d.ts // // @public -export function BrokenImageIcon(props: IconComponentProps): JSX.Element; +export function BrokenImageIcon(props: IconComponentProps): React_2.JSX.Element; // @public @deprecated (undocumented) export const Button: (props: LinkButtonProps) => JSX.Element; @@ -134,22 +136,24 @@ export type CardActionsTopRightClassKey = 'root'; // Warning: (ae-forgotten-export) The symbol "CardTabProps" needs to be exported by the entry point index.d.ts // // @public -export function CardTab(props: PropsWithChildren): JSX.Element; +export function CardTab( + props: PropsWithChildren, +): React_2.JSX.Element; // @public (undocumented) export type CardTabClassKey = 'root' | 'selected'; // @public (undocumented) -export function CatalogIcon(props: IconComponentProps): JSX.Element; +export function CatalogIcon(props: IconComponentProps): React_2.JSX.Element; // @public (undocumented) -export function ChatIcon(props: IconComponentProps): JSX.Element; +export function ChatIcon(props: IconComponentProps): React_2.JSX.Element; // @public (undocumented) export type ClosedDropdownClassKey = 'icon'; // @public -export function CodeSnippet(props: CodeSnippetProps): JSX.Element; +export function CodeSnippet(props: CodeSnippetProps): React_2.JSX.Element; // @public export interface CodeSnippetProps { @@ -164,14 +168,16 @@ export interface CodeSnippetProps { // Warning: (ae-forgotten-export) The symbol "Props_13" needs to be exported by the entry point index.d.ts // // @public -export function Content(props: PropsWithChildren): JSX.Element; +export function Content( + props: PropsWithChildren, +): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "ContentHeaderProps" needs to be exported by the entry point index.d.ts // // @public export function ContentHeader( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // @public (undocumented) export type ContentHeaderClassKey = @@ -182,7 +188,7 @@ export type ContentHeaderClassKey = | 'title'; // @public -export function CopyTextButton(props: CopyTextButtonProps): JSX.Element; +export function CopyTextButton(props: CopyTextButtonProps): React_2.JSX.Element; // @public export interface CopyTextButtonProps { @@ -193,7 +199,9 @@ export interface CopyTextButtonProps { } // @public -export function CreateButton(props: CreateButtonProps): JSX.Element | null; +export function CreateButton( + props: CreateButtonProps, +): React_2.JSX.Element | null; // @public export type CreateButtonProps = { @@ -204,12 +212,12 @@ export type CreateButtonProps = { export type CustomProviderClassKey = 'form' | 'button'; // @public (undocumented) -export function DashboardIcon(props: IconComponentProps): JSX.Element; +export function DashboardIcon(props: IconComponentProps): React_2.JSX.Element; // @public export function DependencyGraph( props: DependencyGraphProps, -): JSX.Element; +): React_2.JSX.Element; // @public (undocumented) export type DependencyGraphDefaultLabelClassKey = 'text'; @@ -301,7 +309,7 @@ export namespace DependencyGraphTypes { // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export const DismissableBanner: (props: Props) => JSX.Element; +export const DismissableBanner: (props: Props) => React_2.JSX.Element; // @public (undocumented) export type DismissableBannerClassKey = @@ -317,15 +325,15 @@ export type DismissableBannerClassKey = export type DismissbleBannerClassKey = DismissableBannerClassKey; // @public (undocumented) -export function DocsIcon(props: IconComponentProps): JSX.Element; +export function DocsIcon(props: IconComponentProps): React_2.JSX.Element; // @public (undocumented) -export function EmailIcon(props: IconComponentProps): JSX.Element; +export function EmailIcon(props: IconComponentProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props_2" needs to be exported by the entry point index.d.ts // // @public -export function EmptyState(props: Props_2): JSX.Element; +export function EmptyState(props: Props_2): React_2.JSX.Element; // @public (undocumented) export type EmptyStateClassKey = 'root' | 'action' | 'imageContainer'; @@ -347,7 +355,7 @@ export type ErrorBoundaryProps = { // Warning: (ae-forgotten-export) The symbol "IErrorPageProps" needs to be exported by the entry point index.d.ts // // @public -export function ErrorPage(props: IErrorPageProps): JSX.Element; +export function ErrorPage(props: IErrorPageProps): React_2.JSX.Element; // @public (undocumented) export type ErrorPageClassKey = 'container' | 'title' | 'subtitle'; @@ -355,7 +363,7 @@ export type ErrorPageClassKey = 'container' | 'title' | 'subtitle'; // @public export function ErrorPanel( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // @public (undocumented) export type ErrorPanelClassKey = 'text' | 'divider'; @@ -382,18 +390,18 @@ export type FeatureCalloutCircleClassKey = // @public export function FeatureCalloutCircular( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // @public (undocumented) export type FiltersContainerClassKey = 'root' | 'title'; // @public -export function Gauge(props: GaugeProps): JSX.Element; +export function Gauge(props: GaugeProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props_10" needs to be exported by the entry point index.d.ts // // @public -export function GaugeCard(props: Props_10): JSX.Element; +export function GaugeCard(props: Props_10): React_2.JSX.Element; // @public (undocumented) export type GaugeCardClassKey = 'root'; @@ -429,18 +437,20 @@ export type GaugePropsGetColorOptions = { }; // @public (undocumented) -export function GitHubIcon(props: IconComponentProps): JSX.Element; +export function GitHubIcon(props: IconComponentProps): React_2.JSX.Element; // @public (undocumented) -export function GroupIcon(props: IconComponentProps): JSX.Element; +export function GroupIcon(props: IconComponentProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props_14" needs to be exported by the entry point index.d.ts // // @public -export function Header(props: PropsWithChildren): JSX.Element; +export function Header(props: PropsWithChildren): React_2.JSX.Element; // @public (undocumented) -export function HeaderActionMenu(props: HeaderActionMenuProps): JSX.Element; +export function HeaderActionMenu( + props: HeaderActionMenuProps, +): React_2.JSX.Element; // @public (undocumented) export type HeaderActionMenuItem = { @@ -471,7 +481,7 @@ export type HeaderClassKey = // Warning: (ae-forgotten-export) The symbol "Props_5" needs to be exported by the entry point index.d.ts // // @public -export function HeaderIconLinkRow(props: Props_5): JSX.Element; +export function HeaderIconLinkRow(props: Props_5): React_2.JSX.Element; // @public (undocumented) export type HeaderIconLinkRowClassKey = 'links'; @@ -479,7 +489,7 @@ export type HeaderIconLinkRowClassKey = 'links'; // Warning: (ae-forgotten-export) The symbol "HeaderLabelProps" needs to be exported by the entry point index.d.ts // // @public -export function HeaderLabel(props: HeaderLabelProps): JSX.Element; +export function HeaderLabel(props: HeaderLabelProps): React_2.JSX.Element; // @public (undocumented) export type HeaderLabelClassKey = 'root' | 'label' | 'value'; @@ -487,7 +497,7 @@ export type HeaderLabelClassKey = 'root' | 'label' | 'value'; // Warning: (ae-forgotten-export) The symbol "HeaderTabsProps" needs to be exported by the entry point index.d.ts // // @public -export function HeaderTabs(props: HeaderTabsProps): JSX.Element; +export function HeaderTabs(props: HeaderTabsProps): React_2.JSX.Element; // @public (undocumented) export type HeaderTabsClassKey = @@ -497,14 +507,14 @@ export type HeaderTabsClassKey = | 'tabRoot'; // @public (undocumented) -export function HelpIcon(props: IconComponentProps): JSX.Element; +export function HelpIcon(props: IconComponentProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props_6" needs to be exported by the entry point index.d.ts // // @public export function HorizontalScrollGrid( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // @public (undocumented) export type HorizontalScrollGridClassKey = @@ -563,16 +573,16 @@ export type InfoCardVariants = 'flex' | 'fullHeight' | 'gridItem'; // Warning: (ae-forgotten-export) The symbol "IntroCardProps" needs to be exported by the entry point index.d.ts // // @public -export function IntroCard(props: IntroCardProps): JSX.Element; +export function IntroCard(props: IntroCardProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "ItemCardProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ItemCard" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @deprecated -export function ItemCard(props: ItemCardProps): JSX.Element; +export function ItemCard(props: ItemCardProps): React_2.JSX.Element; // @public -export function ItemCardGrid(props: ItemCardGridProps): JSX.Element; +export function ItemCardGrid(props: ItemCardGridProps): React_2.JSX.Element; // @public (undocumented) export type ItemCardGridClassKey = 'root'; @@ -585,7 +595,7 @@ export type ItemCardGridProps = Partial> & { }; // @public -export function ItemCardHeader(props: ItemCardHeaderProps): JSX.Element; +export function ItemCardHeader(props: ItemCardHeaderProps): React_2.JSX.Element; // @public (undocumented) export type ItemCardHeaderClassKey = 'root'; @@ -603,7 +613,7 @@ export type ItemCardHeaderProps = Partial> & { // Warning: (ae-missing-release-tag) "Lifecycle" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function Lifecycle(props: Props_7): JSX.Element; +export function Lifecycle(props: Props_7): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "LifecycleClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -614,7 +624,7 @@ export type LifecycleClassKey = 'alpha' | 'beta'; // Warning: (ae-missing-release-tag) "LinearGauge" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function LinearGauge(props: Props_11): JSX.Element | null; +export function LinearGauge(props: Props_11): React_2.JSX.Element | null; // Warning: (ae-missing-release-tag) "Link" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -644,7 +654,7 @@ export type LinkProps = Omit & export type LoginRequestListItemClassKey = 'root'; // @public -export function LogViewer(props: LogViewerProps): JSX.Element; +export function LogViewer(props: LogViewerProps): React_2.JSX.Element; // @public export type LogViewerClassKey = @@ -691,7 +701,7 @@ export interface LogViewerProps { // Warning: (ae-missing-release-tag) "MarkdownContent" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function MarkdownContent(props: Props_8): JSX.Element; +export function MarkdownContent(props: Props_8): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "MarkdownContentClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -727,7 +737,9 @@ export type MicDropClassKey = 'micDrop'; // Warning: (ae-missing-release-tag) "MissingAnnotationEmptyState" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function MissingAnnotationEmptyState(props: Props_3): JSX.Element; +export function MissingAnnotationEmptyState( + props: Props_3, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "MissingAnnotationEmptyStateClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -735,7 +747,9 @@ export function MissingAnnotationEmptyState(props: Props_3): JSX.Element; export type MissingAnnotationEmptyStateClassKey = 'code'; // @public -export const MobileSidebar: (props: MobileSidebarProps) => JSX.Element | null; +export const MobileSidebar: ( + props: MobileSidebarProps, +) => React_2.JSX.Element | null; // @public export type MobileSidebarProps = { @@ -745,7 +759,7 @@ export type MobileSidebarProps = { // Warning: (ae-missing-release-tag) "OAuthRequestDialog" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function OAuthRequestDialog(_props: {}): JSX.Element; +export function OAuthRequestDialog(_props: {}): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "OAuthRequestDialogClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -765,7 +779,7 @@ export type OpenedDropdownClassKey = 'icon'; // Warning: (ae-missing-release-tag) "OverflowTooltip" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function OverflowTooltip(props: Props_9): JSX.Element; +export function OverflowTooltip(props: Props_9): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "OverflowTooltipClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -776,7 +790,7 @@ export type OverflowTooltipClassKey = 'container'; // Warning: (ae-missing-release-tag) "Page" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function Page(props: Props_16): JSX.Element; +export function Page(props: Props_16): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "PageClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -789,19 +803,19 @@ export type PageClassKey = 'root'; // @public (undocumented) export function PageWithHeader( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "Progress" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function Progress( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // @public export const ProxiedSignInPage: ( props: ProxiedSignInPageProps, -) => JSX.Element | null; +) => React_2.JSX.Element | null; // @public export type ProxiedSignInPageProps = SignInPageProps & { @@ -812,7 +826,7 @@ export type ProxiedSignInPageProps = SignInPageProps & { // Warning: (ae-missing-release-tag) "ResponseErrorPanel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function ResponseErrorPanel(props: ErrorPanelProps): JSX.Element; +export function ResponseErrorPanel(props: ErrorPanelProps): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "ResponseErrorPanelClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -822,12 +836,14 @@ export type ResponseErrorPanelClassKey = 'text' | 'divider'; // Warning: (ae-missing-release-tag) "RoutedTabs" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function RoutedTabs(props: { routes: SubRoute_2[] }): JSX.Element; +export function RoutedTabs(props: { + routes: SubRoute_2[]; +}): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "SelectProps" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export function Select(props: SelectProps): JSX.Element; +export function Select(props: SelectProps): React_2.JSX.Element; // @public (undocumented) export type SelectClassKey = @@ -851,7 +867,7 @@ export type SelectItem = { }; // @public -export const Sidebar: (props: SidebarProps) => JSX.Element; +export const Sidebar: (props: SidebarProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SIDEBAR_INTRO_LOCAL_STORAGE" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -905,10 +921,10 @@ export const SidebarDivider: React_2.ComponentType< export type SidebarDividerClassKey = 'root'; // @public -export const SidebarExpandButton: () => JSX.Element | null; +export const SidebarExpandButton: () => React_2.JSX.Element | null; // @public -export const SidebarGroup: (props: SidebarGroupProps) => JSX.Element; +export const SidebarGroup: (props: SidebarGroupProps) => React_2.JSX.Element; // @public export interface SidebarGroupProps extends BottomNavigationActionProps { @@ -920,7 +936,7 @@ export interface SidebarGroupProps extends BottomNavigationActionProps { // Warning: (ae-missing-release-tag) "SidebarIntro" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SidebarIntro(_props: {}): JSX.Element | null; +export function SidebarIntro(_props: {}): React_2.JSX.Element | null; // @public (undocumented) export type SidebarIntroClassKey = @@ -967,7 +983,7 @@ export type SidebarOpenState = { export function SidebarOpenStateProvider(props: { children: ReactNode; value: SidebarOpenState; -}): JSX.Element; +}): React_2.JSX.Element; // @public (undocumented) export type SidebarOptions = { @@ -978,7 +994,7 @@ export type SidebarOptions = { // Warning: (ae-missing-release-tag) "SidebarPage" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SidebarPage(props: SidebarPageProps): JSX.Element; +export function SidebarPage(props: SidebarPageProps): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SidebarPageClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1011,7 +1027,7 @@ export type SidebarPinStateContextType = { export function SidebarPinStateProvider(props: { children: ReactNode; value: SidebarPinStateContextType; -}): JSX.Element; +}): React_2.JSX.Element; // @public (undocumented) export type SidebarProps = { @@ -1036,7 +1052,9 @@ export const SidebarScrollWrapper: React_2.ComponentType< // Warning: (ae-missing-release-tag) "SidebarSearchField" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SidebarSearchField(props: SidebarSearchFieldProps): JSX.Element; +export function SidebarSearchField( + props: SidebarSearchFieldProps, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SidebarSpace" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1067,12 +1085,14 @@ export const SidebarSpacer: React_2.ComponentType< export type SidebarSpacerClassKey = 'root'; // @public -export const SidebarSubmenu: (props: SidebarSubmenuProps) => JSX.Element; +export const SidebarSubmenu: ( + props: SidebarSubmenuProps, +) => React_2.JSX.Element; // @public export const SidebarSubmenuItem: ( props: SidebarSubmenuItemProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type SidebarSubmenuItemDropdownItem = { @@ -1100,7 +1120,7 @@ export type SidebarSubmenuProps = { // Warning: (ae-missing-release-tag) "SignInPage" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SignInPage(props: Props_17): JSX.Element; +export function SignInPage(props: Props_17): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SignInPageClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1123,7 +1143,7 @@ export type SignInProviderConfig = { // @public (undocumented) export function SimpleStepper( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SimpleStepperFooterClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1136,7 +1156,7 @@ export type SimpleStepperFooterClassKey = 'root'; // @public (undocumented) export function SimpleStepperStep( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SimpleStepperStepClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1146,7 +1166,9 @@ export type SimpleStepperStepClassKey = 'end'; // Warning: (ae-missing-release-tag) "StatusAborted" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusAborted(props: PropsWithChildren<{}>): JSX.Element; +export function StatusAborted( + props: PropsWithChildren<{}>, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StatusClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1163,33 +1185,39 @@ export type StatusClassKey = // Warning: (ae-missing-release-tag) "StatusError" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusError(props: PropsWithChildren<{}>): JSX.Element; +export function StatusError(props: PropsWithChildren<{}>): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StatusOK" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusOK(props: PropsWithChildren<{}>): JSX.Element; +export function StatusOK(props: PropsWithChildren<{}>): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StatusPending" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusPending(props: PropsWithChildren<{}>): JSX.Element; +export function StatusPending( + props: PropsWithChildren<{}>, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StatusRunning" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusRunning(props: PropsWithChildren<{}>): JSX.Element; +export function StatusRunning( + props: PropsWithChildren<{}>, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StatusWarning" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusWarning(props: PropsWithChildren<{}>): JSX.Element; +export function StatusWarning( + props: PropsWithChildren<{}>, +): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props_12" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "StructuredMetadataTable" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StructuredMetadataTable(props: Props_12): JSX.Element; +export function StructuredMetadataTable(props: Props_12): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StructuredMetadataTableListClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1211,7 +1239,7 @@ export type SubmenuOptions = { // Warning: (ae-missing-release-tag) "SubvalueCell" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SubvalueCell(props: SubvalueCellProps): JSX.Element; +export function SubvalueCell(props: SubvalueCellProps): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SubvalueCellClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1222,7 +1250,7 @@ export type SubvalueCellClassKey = 'value' | 'subvalue'; // Warning: (ae-missing-release-tag) "SupportButton" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SupportButton(props: SupportButtonProps): JSX.Element; +export function SupportButton(props: SupportButtonProps): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SupportButtonClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1272,7 +1300,9 @@ export type Tab = { // Warning: (ae-missing-release-tag) "TabbedCard" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function TabbedCard(props: PropsWithChildren): JSX.Element; +export function TabbedCard( + props: PropsWithChildren, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "TabbedCardClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1283,7 +1313,7 @@ export type TabbedCardClassKey = 'root' | 'indicator'; // Warning: (ae-missing-release-tag) "TabbedLayout" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function TabbedLayout(props: PropsWithChildren<{}>): JSX.Element; +export function TabbedLayout(props: PropsWithChildren<{}>): React_2.JSX.Element; // @public (undocumented) export namespace TabbedLayout { @@ -1296,7 +1326,9 @@ export namespace TabbedLayout { // Warning: (ae-missing-release-tag) "Table" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function Table(props: TableProps): JSX.Element; +export function Table( + props: TableProps, +): React_2.JSX.Element; // @public (undocumented) export namespace Table { @@ -1356,6 +1388,8 @@ export interface TableProps // (undocumented) initialState?: TableState; // (undocumented) + isLoading?: boolean; + // (undocumented) onStateChange?: (state: TableState) => any; // (undocumented) subtitle?: string; @@ -1383,7 +1417,7 @@ export function TrendLine( Pick & { title?: string; }, -): JSX.Element | null; +): React_2.JSX.Element | null; // @public export function useContent(): { @@ -1401,7 +1435,7 @@ export function useQueryParamState( ): [T | undefined, SetQueryParams]; // @public (undocumented) -export function UserIcon(props: IconComponentProps): JSX.Element; +export function UserIcon(props: IconComponentProps): React_2.JSX.Element; // @public export class UserIdentity implements IdentityApi { @@ -1447,13 +1481,13 @@ export const useSidebarPinState: () => SidebarPinState; export function useSupportConfig(): SupportConfig; // @public (undocumented) -export function WarningIcon(props: IconComponentProps): JSX.Element; +export function WarningIcon(props: IconComponentProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "WarningProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "WarningPanel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function WarningPanel(props: WarningProps): JSX.Element; +export function WarningPanel(props: WarningProps): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "WarningPanelClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/packages/core-components/catalog-info.yaml b/packages/core-components/catalog-info.yaml new file mode 100644 index 0000000000..ba189b8c39 --- /dev/null +++ b/packages/core-components/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-core-components + title: '@backstage/core-components' + description: Core components used by Backstage plugins and apps +spec: + lifecycle: experimental + type: backstage-web-library + owner: maintainers diff --git a/packages/core-components/package.json b/packages/core-components/package.json index c9b1584b85..e2e534da56 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.13.4-next.0", + "version": "0.13.5-next.2", "publishConfig": { "access": "public", "main": "dist/index.esm.js", @@ -22,6 +22,7 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", @@ -60,7 +61,7 @@ "pluralize": "^8.0.0", "prop-types": "^15.7.2", "qs": "^6.9.4", - "rc-progress": "3.4.2", + "rc-progress": "3.5.1", "react-helmet": "6.1.0", "react-hook-form": "^7.12.2", "react-markdown": "^8.0.0", @@ -95,7 +96,6 @@ "@types/d3-zoom": "^3.0.1", "@types/dagre": "^0.7.44", "@types/google-protobuf": "^3.7.2", - "@types/node": "^16.11.26", "@types/react-helmet": "^6.1.0", "@types/react-syntax-highlighter": "^15.0.0", "@types/react-virtualized-auto-sizer": "^1.0.1", diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx index adb9eeb760..cbeb249815 100644 --- a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import Box from '@material-ui/core/Box'; import IconButton from '@material-ui/core/IconButton'; import CopyIcon from '@material-ui/icons/FileCopy'; @@ -69,7 +70,7 @@ export function RealLogViewer(props: RealLogViewerProps) { return ( - {({ height, width }) => ( + {({ height, width }: { height?: number; width?: number }) => ( diff --git a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx index 7ebe15c931..db7c53897d 100644 --- a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx +++ b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx @@ -21,7 +21,7 @@ import { StructuredMetadataTable } from './StructuredMetadataTable'; describe('', () => { it('renders without exploding', () => { - const metadata = { hello: 'world' }; + const metadata = { hello: 'world', foo: null }; const { getByText } = render( , ); diff --git a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx index 6114054bbe..2b18f6ad19 100644 --- a/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx +++ b/packages/core-components/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx @@ -113,7 +113,7 @@ function toValue( return {value}; } - if (typeof value === 'object' && !Array.isArray(value)) { + if (value !== null && typeof value === 'object' && !Array.isArray(value)) { return renderMap(value, options, nested); } diff --git a/packages/core-components/src/components/Table/Table.stories.tsx b/packages/core-components/src/components/Table/Table.stories.tsx index 807d58057d..b38941012c 100644 --- a/packages/core-components/src/components/Table/Table.stories.tsx +++ b/packages/core-components/src/components/Table/Table.stories.tsx @@ -89,6 +89,43 @@ export const DefaultTable = () => { ); }; +export const LoadingTable = () => { + const classes = useStyles(); + const columns: TableColumn[] = [ + { + title: 'Column 1', + field: 'col1', + highlight: true, + }, + { + title: 'Column 2', + field: 'col2', + }, + { + title: 'Numeric value', + field: 'number', + type: 'numeric', + }, + { + title: 'A Date', + field: 'date', + type: 'date', + }, + ]; + + return ( +
+ + + ); +}; + export const EmptyTable = () => { const classes = useStyles(); const columns: TableColumn[] = [ diff --git a/packages/core-components/src/components/Table/Table.test.tsx b/packages/core-components/src/components/Table/Table.test.tsx index ffdca26d98..43c8cfe41d 100644 --- a/packages/core-components/src/components/Table/Table.test.tsx +++ b/packages/core-components/src/components/Table/Table.test.tsx @@ -48,6 +48,11 @@ describe('
', () => { expect(rendered.getByText('second value, second row')).toBeInTheDocument(); }); + it('renders loading without exploding', async () => { + const rendered = await renderInTestApp(
); + expect(rendered.getByTestId('loading-indicator')).toBeInTheDocument(); + }); + describe('with style rows', () => { describe('with CSS Properties object', () => { const styledColumn2 = { diff --git a/packages/core-components/src/components/Table/Table.tsx b/packages/core-components/src/components/Table/Table.tsx index 5cea3dd6b6..9f2b04c214 100644 --- a/packages/core-components/src/components/Table/Table.tsx +++ b/packages/core-components/src/components/Table/Table.tsx @@ -53,6 +53,7 @@ import React, { import { SelectProps } from '../Select/Select'; import { Filter, Filters, SelectedFilters, Without } from './Filters'; +import CircularProgress from '@material-ui/core/CircularProgress'; // Material-table is not using the standard icons available in in material-ui. https://github.com/mbrn/material-table/issues/51 const tableIcons: Icons = { @@ -236,6 +237,7 @@ export interface TableProps filters?: TableFilter[]; initialState?: TableState; emptyContent?: ReactNode; + isLoading?: boolean; onStateChange?: (state: TableState) => any; } @@ -309,6 +311,7 @@ export function Table(props: TableProps) { emptyContent, onStateChange, components, + isLoading: isLoading, ...restProps } = props; const tableClasses = useTableStyles(); @@ -470,6 +473,28 @@ export function Table(props: TableProps) { const columnCount = columns.length; const Body = useCallback( bodyProps => { + if (isLoading) { + return ( + + + + + + ); + } + if (emptyContent && hasNoRows) { return ( @@ -482,7 +507,7 @@ export function Table(props: TableProps) { return ; }, - [hasNoRows, emptyContent, columnCount], + [hasNoRows, emptyContent, columnCount, isLoading], ); return ( diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index 62b7d0fbdb..33060653c1 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/core-plugin-api +## 1.6.0-next.2 + +### Minor Changes + +- 6e30769cc627: Introduced experimental support for internationalization. + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## 1.6.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## 1.6.0-next.0 + +### Minor Changes + +- 18619f793c94: Added the optional `expiresAt` field that may now be part of a `BackstageIdentityResponse`. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + ## 1.5.3 ### Patch Changes diff --git a/packages/core-plugin-api/alpha-api-report.md b/packages/core-plugin-api/alpha-api-report.md index 379e186df8..57cf4a2cb4 100644 --- a/packages/core-plugin-api/alpha-api-report.md +++ b/packages/core-plugin-api/alpha-api-report.md @@ -3,9 +3,29 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { i18n } from 'i18next'; import { ReactNode } from 'react'; +// @alpha (undocumented) +export type AppTranslationApi = { + getI18n(): i18n; + addResourcesByRef>( + translationRef: TranslationRef, + ): void; +}; + +// @alpha (undocumented) +export const appTranslationApiRef: ApiRef; + +// @alpha (undocumented) +export const createTranslationRef: < + Messages extends Record = {}, +>( + config: TranslationRefConfig, +) => TranslationRef; + // @alpha export interface PluginOptionsProviderProps { // (undocumented) @@ -17,10 +37,92 @@ export interface PluginOptionsProviderProps { // @alpha export const PluginProvider: (props: PluginOptionsProviderProps) => JSX.Element; +// @alpha (undocumented) +export type TranslationOptions< + Messages extends Record = Record, +> = Messages; + +// @alpha (undocumented) +export interface TranslationRef< + Messages extends Record = Record, +> { + // (undocumented) + getDefaultMessages(): Messages; + // (undocumented) + getId(): string; + // (undocumented) + getLazyResources(): + | Record< + string, + () => Promise<{ + messages: Messages; + }> + > + | undefined; + // (undocumented) + getResources(): Record | undefined; +} + +// @alpha (undocumented) +export interface TranslationRefConfig< + Messages extends Record, +> { + // (undocumented) + id: string; + // (undocumented) + lazyResources?: Record< + string, + () => Promise<{ + messages: Messages; + }> + >; + // (undocumented) + messages: Messages; + // (undocumented) + resources?: Record; +} + +// @alpha (undocumented) +export class TranslationRefImpl> + implements TranslationRef +{ + // (undocumented) + static create>( + config: TranslationRefConfig, + ): TranslationRefImpl; + // (undocumented) + getDefaultMessages(): Messages; + // (undocumented) + getId(): string; + // (undocumented) + getLazyResources(): + | Record< + string, + () => Promise<{ + messages: Messages; + }> + > + | undefined; + // (undocumented) + getResources(): Record | undefined; + // (undocumented) + toString(): string; +} + // @alpha export function usePluginOptions< TPluginOptions extends {} = {}, >(): TPluginOptions; +// @alpha (undocumented) +export const useTranslationRef: < + Messages extends Record, +>( + translationRef: TranslationRef, +) => ( + key: Tkey, + options?: TranslationOptions, +) => Messages[Tkey]; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 5577d94359..930f1f979d 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -45,7 +45,7 @@ export const analyticsApiRef: ApiRef; export const AnalyticsContext: (options: { attributes: Partial; children: ReactNode; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public export type AnalyticsContextValue = CommonAnalyticsContext & { @@ -214,6 +214,7 @@ export type BackstageIdentityApi = { // @public export type BackstageIdentityResponse = { token: string; + expiresAt?: Date; identity: BackstageUserIdentity; }; @@ -782,7 +783,9 @@ export function withApis( ): ( WrappedComponent: React_2.ComponentType, ) => { - (props: React_2.PropsWithChildren>): JSX.Element; + ( + props: React_2.PropsWithChildren>, + ): React_2.JSX.Element; displayName: string; }; ``` diff --git a/packages/core-plugin-api/catalog-info.yaml b/packages/core-plugin-api/catalog-info.yaml new file mode 100644 index 0000000000..2f0fa2d4eb --- /dev/null +++ b/packages/core-plugin-api/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-core-plugin-api + title: '@backstage/core-plugin-api' + description: Core API used by Backstage plugins +spec: + lifecycle: production + type: backstage-web-library + owner: maintainers diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 138ad24249..38af96585c 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-plugin-api", "description": "Core API used by Backstage plugins", - "version": "1.5.3", + "version": "1.6.0-next.2", "publishConfig": { "access": "public" }, @@ -35,6 +35,7 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", @@ -50,7 +51,9 @@ "@backstage/version-bridge": "workspace:^", "@types/react": "^16.13.1 || ^17.0.0", "history": "^5.0.0", + "i18next": "^22.4.15", "prop-types": "^15.7.2", + "react-i18next": "^12.3.1", "zen-observable": "^0.10.0" }, "peerDependencies": { @@ -67,7 +70,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", "@types/prop-types": "^15.7.3", "@types/zen-observable": "^0.8.0", "msw": "^1.0.0" diff --git a/packages/core-plugin-api/src/alpha.ts b/packages/core-plugin-api/src/alpha.ts index 09983a48e9..505fd9b818 100644 --- a/packages/core-plugin-api/src/alpha.ts +++ b/packages/core-plugin-api/src/alpha.ts @@ -15,3 +15,5 @@ */ export * from './plugin-options'; +export * from './translation'; +export * from './apis/alpha'; diff --git a/packages/core-plugin-api/src/apis/alpha.ts b/packages/core-plugin-api/src/apis/alpha.ts new file mode 100644 index 0000000000..b22d3df574 --- /dev/null +++ b/packages/core-plugin-api/src/apis/alpha.ts @@ -0,0 +1,16 @@ +/* + * 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 './definitions/alpha'; diff --git a/packages/core-plugin-api/src/apis/definitions/AppTranslationApi.ts b/packages/core-plugin-api/src/apis/definitions/AppTranslationApi.ts new file mode 100644 index 0000000000..463aee02bf --- /dev/null +++ b/packages/core-plugin-api/src/apis/definitions/AppTranslationApi.ts @@ -0,0 +1,35 @@ +/* + * 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 { type i18n } from 'i18next'; +import { TranslationRef } from '../../translation'; +import { ApiRef, createApiRef } from '@backstage/core-plugin-api'; + +/** @alpha */ +export type AppTranslationApi = { + getI18n(): i18n; + + addResourcesByRef>( + translationRef: TranslationRef, + ): void; +}; + +/** + * @alpha + */ +export const appTranslationApiRef: ApiRef = createApiRef({ + id: 'core.apptranslation', +}); diff --git a/packages/core-plugin-api/src/apis/definitions/alpha.ts b/packages/core-plugin-api/src/apis/definitions/alpha.ts new file mode 100644 index 0000000000..691af96e32 --- /dev/null +++ b/packages/core-plugin-api/src/apis/definitions/alpha.ts @@ -0,0 +1,16 @@ +/* + * 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 './AppTranslationApi'; diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index 62d0d5c810..d9aec2ca5a 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -226,6 +226,11 @@ export type BackstageIdentityResponse = { */ token: string; + /** + * The time at which the token expires. If not set, it can be assumed that the token does not expire. + */ + expiresAt?: Date; + /** * Identity information derived from the token. */ diff --git a/packages/core-plugin-api/src/translation/TranslationRef.test.ts b/packages/core-plugin-api/src/translation/TranslationRef.test.ts new file mode 100644 index 0000000000..155624a122 --- /dev/null +++ b/packages/core-plugin-api/src/translation/TranslationRef.test.ts @@ -0,0 +1,60 @@ +/* + * 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 { TranslationRefImpl, createTranslationRef } from './TranslationRef'; + +describe('TranslationRefImpl', () => { + it('should create a TranslationRef instance', () => { + const config = { + id: 'testId', + messages: { key: 'value' }, + }; + + const translationRef = TranslationRefImpl.create(config); + + expect(translationRef.getId()).toBe('testId'); + expect(translationRef.getDefaultMessages()).toEqual({ key: 'value' }); + }); + + it('should create a TranslationRef instance using the factory function', () => { + const config = { + id: 'testId', + messages: { key: 'value' }, + }; + + const translationRef = createTranslationRef(config); + + expect(translationRef.getId()).toBe('testId'); + expect(translationRef.getDefaultMessages()).toEqual({ key: 'value' }); + }); + + it('should get lazy resources', async () => { + const config = { + id: 'testId', + messages: { key: 'value' }, + lazyResources: { + en: () => Promise.resolve({ messages: { key: 'value' } }), + }, + }; + + const translationRef = TranslationRefImpl.create(config); + + const lazyResources = translationRef.getLazyResources(); + + const messages = await lazyResources?.en(); + + expect(messages).toEqual({ messages: { key: 'value' } }); + }); +}); diff --git a/packages/core-plugin-api/src/translation/TranslationRef.ts b/packages/core-plugin-api/src/translation/TranslationRef.ts new file mode 100644 index 0000000000..d01f7ca261 --- /dev/null +++ b/packages/core-plugin-api/src/translation/TranslationRef.ts @@ -0,0 +1,61 @@ +/* + * 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 { TranslationRef, TranslationRefConfig } from './types'; + +/** @alpha */ +export class TranslationRefImpl> + implements TranslationRef +{ + static create>( + config: TranslationRefConfig, + ) { + return new TranslationRefImpl(config); + } + + getId() { + return this.config.id; + } + + getDefaultMessages(): Messages { + return this.config.messages; + } + + getLazyResources(): + | Record Promise<{ messages: Messages }>> + | undefined { + return this.config.lazyResources; + } + + getResources(): Record | undefined { + return this.config.resources; + } + + toString() { + return `TranslationRef(${this.getId()})`; + } + + private constructor( + private readonly config: TranslationRefConfig, + ) {} +} + +/** @alpha */ +export const createTranslationRef = < + Messages extends Record = {}, +>( + config: TranslationRefConfig, +): TranslationRef => TranslationRefImpl.create(config); diff --git a/packages/core-plugin-api/src/translation/index.ts b/packages/core-plugin-api/src/translation/index.ts new file mode 100644 index 0000000000..9eaef57206 --- /dev/null +++ b/packages/core-plugin-api/src/translation/index.ts @@ -0,0 +1,19 @@ +/* + * 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 './TranslationRef'; +export * from './types'; +export * from './useTranslationRef'; diff --git a/packages/core-plugin-api/src/translation/types.ts b/packages/core-plugin-api/src/translation/types.ts new file mode 100644 index 0000000000..4811c82eb7 --- /dev/null +++ b/packages/core-plugin-api/src/translation/types.ts @@ -0,0 +1,45 @@ +/* + * 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. + */ + +/** @alpha */ +export interface TranslationRefConfig< + Messages extends Record, +> { + id: string; + messages: Messages; + lazyResources?: Record Promise<{ messages: Messages }>>; + resources?: Record; +} + +/** @alpha */ +export interface TranslationRef< + Messages extends Record = Record, +> { + getId(): string; + + getDefaultMessages(): Messages; + + getResources(): Record | undefined; + + getLazyResources(): + | Record Promise<{ messages: Messages }>> + | undefined; +} + +/** @alpha */ +export type TranslationOptions< + Messages extends Record = Record, +> = Messages; diff --git a/packages/core-plugin-api/src/translation/useTranslationRef.test.ts b/packages/core-plugin-api/src/translation/useTranslationRef.test.ts new file mode 100644 index 0000000000..372d72eb23 --- /dev/null +++ b/packages/core-plugin-api/src/translation/useTranslationRef.test.ts @@ -0,0 +1,68 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useTranslation } from 'react-i18next'; +import { useApi } from '../apis'; +import { useTranslationRef } from './useTranslationRef'; +import { createTranslationRef } from './TranslationRef'; + +jest.mock('../apis', () => ({ + ...jest.requireActual('../apis'), + useApi: jest.fn(), +})); + +jest.mock('react-i18next', () => ({ + useTranslation: jest.fn(), +})); + +describe('useTranslationRef', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return correct t', () => { + const translationRef = createTranslationRef({ + id: 'ref-id', + messages: { + key1: 'default1', + key2: 'default2', + }, + }); + + const tMock = jest.fn(); + tMock.mockReturnValue('translatedValue'); + + const i18nMock = { + language: 'en', + t: tMock, + }; + + (useApi as jest.Mock).mockReturnValue({ + addResourcesByRef: jest.fn(), + }); + + (useTranslation as jest.Mock).mockReturnValue(i18nMock); + + const { result } = renderHook(() => useTranslationRef(translationRef)); + + const t = result.current; + + t('key1', { condition: 'v1' }); + expect(tMock).toHaveBeenCalledWith('key1', 'default1', { condition: 'v1' }); + t('key2'); + expect(tMock).toHaveBeenCalledWith('key2', 'default2', undefined); + }); +}); diff --git a/packages/core-plugin-api/src/translation/useTranslationRef.ts b/packages/core-plugin-api/src/translation/useTranslationRef.ts new file mode 100644 index 0000000000..083bcd1f0e --- /dev/null +++ b/packages/core-plugin-api/src/translation/useTranslationRef.ts @@ -0,0 +1,41 @@ +/* + * 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 { useTranslation } from 'react-i18next'; + +import { TranslationOptions, TranslationRef } from './types'; +import { useApi } from '../apis'; +import { appTranslationApiRef } from '../apis/alpha'; + +/** @alpha */ +export const useTranslationRef = < + Messages extends Record, +>( + translationRef: TranslationRef, +) => { + const appTranslationApi = useApi(appTranslationApiRef); + + appTranslationApi.addResourcesByRef(translationRef); + + const { t } = useTranslation(translationRef.getId()); + + const defaulteMessage = translationRef.getDefaultMessages(); + + return ( + key: Tkey, + options?: TranslationOptions, + ): Messages[Tkey] => t(key as string, defaulteMessage[key], options); +}; diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index eb2f048d62..3fd4a7cc1c 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/create-app +## 0.5.5-next.2 + +### Patch Changes + +- 8db70ecbb325: Updated Dockerfile to include `apt-get update` when installing the SQLite dependency +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/cli-common@0.1.12 + +## 0.5.5-next.1 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.12 + +## 0.5.5-next.0 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.12 + +## 0.5.4 + +### Patch Changes + +- b441642fbe0d: Bumped create-app version. +- 572abc7edf55: Bumped create-app version. +- 74f77f151a96: Bumped create-app version. +- 5cc0ac5ef3d1: Bump to a newer version of the `concurrently` library +- 46c9a798e41d: Updated the `app-config.yaml` template to use `proxy.endpoints`. +- 971bdd6a4732: Bumped internal `nodemon` dependency. +- Updated dependencies + - @backstage/cli-common@0.1.12 + +## 0.5.4-next.2 + +### Patch Changes + +- Bumped create-app version. + ## 0.5.4-next.1 ### Patch Changes diff --git a/packages/create-app/catalog-info.yaml b/packages/create-app/catalog-info.yaml new file mode 100644 index 0000000000..7da321f091 --- /dev/null +++ b/packages/create-app/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-create-app + title: '@backstage/create-app' + description: A CLI that helps you create your own Backstage app +spec: + lifecycle: experimental + type: backstage-cli + owner: maintainers diff --git a/packages/create-app/package.json b/packages/create-app/package.json index de1a9791ae..bcbcaa321d 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.4-next.1", + "version": "0.5.5-next.2", "publishConfig": { "access": "public" }, @@ -49,7 +49,7 @@ "@types/node": "^16.11.26", "@types/recursive-readdir": "^2.2.0", "mock-fs": "^5.1.1", - "nodemon": "^2.0.2", + "nodemon": "^3.0.1", "ts-node": "^10.0.0" }, "nodemonConfig": { diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index f158a2d1f1..1943b0c3cc 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -31,6 +31,7 @@ import { promisify } from 'util'; import os from 'os'; const TASK_NAME_MAX_LENGTH = 14; +const TEN_MINUTES_MS = 1000 * 60 * 10; const exec = promisify(execCb); export type GitConfig = { @@ -222,7 +223,13 @@ export async function buildAppTask(appDir: string) { }); }; - await runCmd('yarn install'); + const installTimeout = setTimeout(() => { + Task.error( + "\n⏱️ It's taking a long time to install dependencies, you may want to exit (Ctrl-C) and run 'yarn install' and 'yarn tsc' manually", + ); + }, TEN_MINUTES_MS); + + await runCmd('yarn install').finally(() => clearTimeout(installTimeout)); await runCmd('yarn tsc'); } 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 194549f079..08d2abff59 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -46,9 +46,10 @@ integrations: proxy: ### Example for how to add a proxy endpoint for the frontend. ### A typical reason to do this is to handle HTTPS and CORS for internal services. - # '/test': - # target: 'https://example.com' - # changeOrigin: true + # endpoints: + # '/test': + # target: 'https://example.com' + # changeOrigin: true # Reference documentation http://backstage.io/docs/features/techdocs/configuration # Note: After experimenting with basic setup, use CI/CD to generate docs diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 0682114a35..a4fcb556ea 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -17,6 +17,7 @@ "clean": "backstage-cli repo clean", "test": "backstage-cli repo test", "test:all": "backstage-cli repo test --coverage", + "fix": "backstage-cli repo fix", "lint": "backstage-cli repo lint --since origin/{{defaultBranch}}", "lint:all": "backstage-cli repo lint", "prettier:check": "prettier --check .", @@ -31,7 +32,7 @@ "devDependencies": { "@backstage/cli": "^{{version '@backstage/cli'}}", "@spotify/prettier-config": "^12.0.0", - "concurrently": "^6.0.0", + "concurrently": "^8.0.0", "lerna": "^4.0.0", "node-gyp": "^9.0.0", "prettier": "^2.3.2", 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 02535e217c..6793a91dd0 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 @@ -58,7 +58,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@testing-library/dom": "^8.0.0", - "@types/node": "^16.11.26", "@types/react-dom": "*", "cross-env": "^7.0.0", "cypress": "^9.7.0", diff --git a/packages/create-app/templates/default-app/packages/backend/Dockerfile b/packages/create-app/templates/default-app/packages/backend/Dockerfile index d065e4a248..a60fee7a9c 100644 --- a/packages/create-app/templates/default-app/packages/backend/Dockerfile +++ b/packages/create-app/templates/default-app/packages/backend/Dockerfile @@ -22,6 +22,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ # in which case you should also move better-sqlite3 to "devDependencies" in package.json. RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && \ apt-get install -y --no-install-recommends libsqlite3-dev # From here on we use the least-privileged `node` user to run the backend. diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 132f330d8f..f9f376bc36 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,75 @@ # @backstage/dev-utils +## 1.0.21-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/integration-react@1.1.19-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/app-defaults@1.4.3-next.2 + - @backstage/core-app-api@1.10.0-next.2 + - @backstage/test-utils@1.4.3-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## 1.0.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/app-defaults@1.4.3-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-app-api@1.10.0-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/test-utils@1.4.3-next.1 + - @backstage/theme@0.4.1 + +## 1.0.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/app-defaults@1.4.3-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/test-utils@1.4.3-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 1.0.18 + +### Patch Changes + +- 254ad469f053: Removed deprecated calls to `app.getProvider()` and `app.getRouter()` in `DevAppBuilder` +- Updated dependencies + - @backstage/core-app-api@1.9.1 + - @backstage/integration-react@1.1.16 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/app-defaults@1.4.2 + - @backstage/core-plugin-api@1.5.3 + - @backstage/test-utils@1.4.2 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## 1.0.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/integration-react@1.1.16-next.1 + ## 1.0.18-next.1 ### Patch Changes diff --git a/packages/dev-utils/catalog-info.yaml b/packages/dev-utils/catalog-info.yaml new file mode 100644 index 0000000000..c67eecec83 --- /dev/null +++ b/packages/dev-utils/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-dev-utils + title: '@backstage/dev-utils' + description: Utilities for developing Backstage plugins. +spec: + lifecycle: production + type: backstage-web-library + owner: maintainers diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 730d8ffe4e..ef059cba25 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "1.0.18-next.1", + "version": "1.0.21-next.2", "publishConfig": { "access": "public", "main": "dist/index.esm.js", @@ -22,6 +22,7 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", @@ -57,8 +58,7 @@ "react-router-dom": "6.0.0-beta.0 || ^6.3.0" }, "devDependencies": { - "@backstage/cli": "workspace:^", - "@types/node": "^16.0.0" + "@backstage/cli": "workspace:^" }, "files": [ "dist" diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index 36e87855ed..5f2b21b8d2 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -15,7 +15,7 @@ */ import { createApp } from '@backstage/app-defaults'; -import { FlatRoutes } from '@backstage/core-app-api'; +import { AppRouter, FlatRoutes } from '@backstage/core-app-api'; import { AlertDisplay, OAuthRequestDialog, @@ -195,35 +195,30 @@ export class DevAppBuilder { }, }); - const AppProvider = app.getProvider(); - const AppRouter = app.getRouter(); + const DevApp = ( + <> + + + {this.rootChildren} + + + + + {this.sidebarItems} + + + + + + {this.routes} + } /> + + + + + ); - const DevApp = () => { - return ( - - - - {this.rootChildren} - - - - - {this.sidebarItems} - - - - - - {this.routes} - } /> - - - - - ); - }; - - return DevApp; + return app.createRoot(DevApp); } /** diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md index a334051aae..2c8e1300ac 100644 --- a/packages/e2e-test/CHANGELOG.md +++ b/packages/e2e-test/CHANGELOG.md @@ -1,5 +1,48 @@ # e2e-test +## 0.2.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.5-next.2 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + +## 0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.5-next.1 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + +## 0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.5-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.4 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + +## 0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.4-next.2 + ## 0.2.6-next.1 ### Patch Changes diff --git a/packages/e2e-test/catalog-info.yaml b/packages/e2e-test/catalog-info.yaml new file mode 100644 index 0000000000..55bce03e8b --- /dev/null +++ b/packages/e2e-test/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: e2e-test + title: e2e-test + description: E2E test for verifying Backstage packages +spec: + lifecycle: experimental + type: backstage-cli + owner: maintainers diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 3708f1bae4..4cd3988745 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.6-next.1", + "version": "0.2.7-next.2", "private": true, "backstage": { "role": "cli" @@ -35,7 +35,8 @@ "cross-fetch": "^3.1.5", "fs-extra": "10.1.0", "handlebars": "^4.7.3", - "pgtools": "^0.3.0", + "mysql2": "^2.2.5", + "pgtools": "^1.0.0", "puppeteer": "^17.0.0", "tree-kill": "^1.2.2" }, @@ -44,7 +45,7 @@ "@types/fs-extra": "^9.0.1", "@types/node": "^16.11.26", "@types/puppeteer": "^5.4.4", - "nodemon": "^2.0.2", + "nodemon": "^3.0.1", "ts-node": "^10.0.0" }, "nodemonConfig": { diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index b3fb8d9a3c..20b2753f6b 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -31,7 +31,10 @@ import { waitForExit, print, } from '../lib/helpers'; + +import mysql from 'mysql2/promise'; import pgtools from 'pgtools'; + import { findPaths } from '@backstage/cli-common'; // eslint-disable-next-line no-restricted-syntax @@ -66,9 +69,12 @@ export async function run() { print('Starting the app'); await testAppServe(pluginId, appDir); - if (Boolean(process.env.POSTGRES_USER)) { - print('Testing the PostgreSQL backend startup'); - await preCleanPostgres(); + if ( + Boolean(process.env.POSTGRES_USER) || + Boolean(process.env.MYSQL_CONNECTION) + ) { + print('Testing the database backend startup'); + await preCleanDatabase(); const appConfig = path.resolve(appDir, 'app-config.yaml'); const productionConfig = path.resolve(appDir, 'app-config.production.yaml'); await testBackendStart( @@ -79,7 +85,7 @@ export async function run() { productionConfig, ); } - print('Testing the SQLite backend startup'); + print('Testing the Database backend startup'); await testBackendStart(appDir); if (process.env.CI) { @@ -427,24 +433,39 @@ async function testAppServe(pluginId: string, appDir: string) { } /** Drops PG databases */ -async function dropDB(database: string) { - const config = { - host: process.env.POSTGRES_HOST, - port: process.env.POSTGRES_PORT, - user: process.env.POSTGRES_USER, - password: process.env.POSTGRES_PASSWORD, - }; - +async function dropDB(database: string, client: string) { try { - await pgtools.dropdb(config, database); + if (client === 'postgres') { + const config = { + host: process.env.POSTGRES_HOST, + port: process.env.POSTGRES_PORT, + user: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD, + }; + await pgtools.dropdb(config, database); + } else if (client === 'mysql') { + const connectionString = process.env.MYSQL_CONNECTION ?? ''; + const connection = await mysql.createConnection(connectionString); + await connection.execute('DROP DATABASE ?', [database]); + } } catch (_) { - /* do nothing*/ + /* do nothing */ } } /** Clean remnants from prior e2e runs */ -async function preCleanPostgres() { +async function preCleanDatabase() { print('Dropping old DBs'); + if (Boolean(process.env.POSTGRES_HOST)) { + await dropClientDatabases('postgres'); + } + if (Boolean(process.env.MYSQL_CONNECTION)) { + await dropClientDatabases('mysql'); + } + print('Dropped DBs'); +} + +async function dropClientDatabases(client: string) { await Promise.all( [ 'catalog', @@ -454,9 +475,8 @@ async function preCleanPostgres() { 'proxy', 'techdocs', 'search', - ].map(name => dropDB(`backstage_plugin_${name}`)), + ].map(name => dropDB(`backstage_plugin_${name}`, client)), ); - print('Created DBs'); } /** diff --git a/packages/errors/catalog-info.yaml b/packages/errors/catalog-info.yaml new file mode 100644 index 0000000000..ee97045b20 --- /dev/null +++ b/packages/errors/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-errors + title: '@backstage/errors' + description: Common utilities for error handling within Backstage +spec: + lifecycle: production + type: backstage-common-library + owner: maintainers diff --git a/packages/errors/package.json b/packages/errors/package.json index 05d04031ca..1afeb662ca 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -23,6 +23,7 @@ "keywords": [ "backstage" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/packages/frontend-app-api/.eslintrc.js b/packages/frontend-app-api/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/frontend-app-api/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/frontend-app-api/CHANGELOG.md b/packages/frontend-app-api/CHANGELOG.md new file mode 100644 index 0000000000..6cb9af631d --- /dev/null +++ b/packages/frontend-app-api/CHANGELOG.md @@ -0,0 +1,22 @@ +# @backstage/frontend-app-api + +## 0.0.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-graphiql@0.2.54-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/frontend-plugin-api@0.0.1-next.0 + - @backstage/types@1.1.0 + +## 0.0.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-graphiql@0.2.54-next.1 + - @backstage/frontend-plugin-api@0.0.0 + - @backstage/types@1.1.0 diff --git a/packages/frontend-app-api/README.md b/packages/frontend-app-api/README.md new file mode 100644 index 0000000000..8b2fe5dc23 --- /dev/null +++ b/packages/frontend-app-api/README.md @@ -0,0 +1,9 @@ +# @backstage/frontend-app-api + +**This package is EXPERIMENTAL, we recommend against using it for production deployments** + +This package provides the core API used by Backstage frontend apps. It implements the design outlined in [RFC: Frontend System Evolution](https://github.com/backstage/backstage/issues/18372). + +## Documentation + +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md new file mode 100644 index 0000000000..bf7db4451b --- /dev/null +++ b/packages/frontend-app-api/api-report.md @@ -0,0 +1,14 @@ +## API Report File for "@backstage/frontend-app-api" + +> 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'; + +// @public (undocumented) +export function createApp(options: { plugins: BackstagePlugin[] }): { + createRoot(): JSX.Element; +}; +``` diff --git a/packages/frontend-app-api/catalog-info.yaml b/packages/frontend-app-api/catalog-info.yaml new file mode 100644 index 0000000000..332650f083 --- /dev/null +++ b/packages/frontend-app-api/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-frontend-app-api + title: '@backstage/frontend-app-api' +spec: + lifecycle: experimental + type: backstage-web-library + owner: maintainers diff --git a/packages/frontend-app-api/config.d.ts b/packages/frontend-app-api/config.d.ts new file mode 100644 index 0000000000..81c24d1185 --- /dev/null +++ b/packages/frontend-app-api/config.d.ts @@ -0,0 +1,35 @@ +/* + * 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 interface Config { + app?: { + /** + * @deepVisibility frontend + */ + extensions?: + | string + | { + [extensionId: string]: + | boolean + | string + | { + at?: string; + extension?: string; + config?: unknown; + }; + }; + }; +} diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json new file mode 100644 index 0000000000..451a1c0c5a --- /dev/null +++ b/packages/frontend-app-api/package.json @@ -0,0 +1,47 @@ +{ + "name": "@backstage/frontend-app-api", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "web-library" + }, + "sideEffects": false, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@types/react": "^16.13.1 || ^17.0.0" + }, + "configSchema": "config.d.ts", + "files": [ + "config.d.ts", + "dist" + ], + "dependencies": { + "@backstage/config": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/plugin-graphiql": "workspace:^", + "@backstage/types": "workspace:^", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0", + "react-router-dom": "6.0.0-beta.0 || ^6.3.0" + } +} diff --git a/packages/frontend-app-api/src/createApp.tsx b/packages/frontend-app-api/src/createApp.tsx new file mode 100644 index 0000000000..c50b507642 --- /dev/null +++ b/packages/frontend-app-api/src/createApp.tsx @@ -0,0 +1,172 @@ +/* + * 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 React from 'react'; +import { ConfigReader } from '@backstage/config'; +import { + BackstagePlugin, + coreExtensionData, +} from '@backstage/frontend-plugin-api'; +import { CoreRouter } from './extensions/CoreRouter'; +import { + createExtensionInstance, + ExtensionInstance, +} from './createExtensionInstance'; +import { + ExtensionInstanceParameters, + mergeExtensionParameters, + readAppExtensionParameters, +} from './wiring/parameters'; +import { RoutingProvider } from './routing/RoutingContext'; +import { RouteRef } from '@backstage/core-plugin-api'; +import { getAvailablePlugins } from './wiring/discovery'; + +/** @public */ +export function createApp(options: { plugins: BackstagePlugin[] }): { + createRoot(): JSX.Element; +} { + const appConfig = ConfigReader.fromConfigs(process.env.APP_CONFIG as any); + + const builtinExtensions = [CoreRouter]; + const discoveredPlugins = getAvailablePlugins(); + + // pull in default extension instance from discovered packages + // apply config to adjust default extension instances and add more + const extensionParams = mergeExtensionParameters({ + sources: [...options.plugins, ...discoveredPlugins], + builtinExtensions, + parameters: readAppExtensionParameters(appConfig), + }); + + // TODO: validate the config of all extension instances + // We do it at this point to ensure that merging (if any) of config has already happened + + // Create attachment map so that we can look attachments up during instance creation + const attachmentMap = new Map< + string, + Map + >(); + for (const instanceParams of extensionParams) { + const [extensionId, pointId = 'default'] = instanceParams.at.split('/'); + + let pointMap = attachmentMap.get(extensionId); + if (!pointMap) { + pointMap = new Map(); + attachmentMap.set(extensionId, pointMap); + } + + let instances = pointMap.get(pointId); + if (!instances) { + instances = []; + pointMap.set(pointId, instances); + } + + instances.push(instanceParams); + } + + const instances = new Map(); + + function createInstance( + instanceParams: ExtensionInstanceParameters, + ): ExtensionInstance { + const existingInstance = instances.get(instanceParams.extension.id); + if (existingInstance) { + return existingInstance; + } + + const attachments = new Map( + Array.from( + attachmentMap.get(instanceParams.extension.id)?.entries() ?? [], + ).map(([inputName, attachmentConfigs]) => [ + inputName, + attachmentConfigs.map(createInstance), + ]), + ); + + const newInstance = createExtensionInstance({ + extension: instanceParams.extension, + source: instanceParams.source, + config: instanceParams.config, + attachments, + }); + + instances.set(instanceParams.extension.id, newInstance); + + return newInstance; + } + + const rootConfigs = attachmentMap.get('root')?.get('default') ?? []; + const rootInstances = rootConfigs.map(instanceParams => + createInstance(instanceParams), + ); + + const routePaths = extractRouteInfoFromInstanceTree(rootInstances); + + return { + createRoot() { + const rootComponents = rootInstances.map( + e => + e.data.get( + coreExtensionData.reactComponent.id, + ) as typeof coreExtensionData.reactComponent.T, + ); + return ( + + {rootComponents.map((Component, i) => ( + + ))} + + ); + }, + }; +} + +/** @internal */ +export function extractRouteInfoFromInstanceTree( + roots: ExtensionInstance[], +): Map { + const results = new Map(); + + function visit(current: ExtensionInstance, basePath: string) { + const routePath = current.data.get(coreExtensionData.routePath.id) ?? ''; + const routeRef = current.data.get( + coreExtensionData.routeRef.id, + ) as RouteRef; + + // TODO: join paths in a more robust way + const fullPath = basePath + routePath; + if (routeRef) { + const routeRefId = (routeRef as any).id; // TODO: properly + if (routeRefId !== current.id) { + throw new Error( + `Route ref '${routeRefId}' must have the same ID as extension '${current.id}'`, + ); + } + results.set(routeRef, fullPath); + } + + for (const children of current.attachments.values()) { + for (const child of children) { + visit(child, fullPath); + } + } + } + + for (const root of roots) { + visit(root, ''); + } + return results; +} diff --git a/packages/frontend-app-api/src/createExtensionInstance.ts b/packages/frontend-app-api/src/createExtensionInstance.ts new file mode 100644 index 0000000000..d767f21d58 --- /dev/null +++ b/packages/frontend-app-api/src/createExtensionInstance.ts @@ -0,0 +1,90 @@ +/* + * 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 { BackstagePlugin, Extension } from '@backstage/frontend-plugin-api'; +import mapValues from 'lodash/mapValues'; + +/** @internal */ +export interface ExtensionInstance { + readonly id: string; + /** + * Maps extension data ref IDs to extensions produced. + */ + readonly data: Map; + /** + * Maps input names to the actual instances given to them. + */ + readonly attachments: Map; + readonly $$type: 'extension-instance'; +} + +/** @internal */ +export function createExtensionInstance(options: { + extension: Extension; + config: unknown; + source?: BackstagePlugin; + attachments: Map; +}): ExtensionInstance { + const { extension, config, source, attachments } = options; + const extensionData = new Map(); + + let parsedConfig: unknown; + try { + parsedConfig = extension.configSchema?.parse(config ?? {}); + } catch (e) { + throw new Error( + `Invalid configuration for extension instance '${extension.id}', ${e}`, + ); + } + + try { + extension.factory({ + source, + config: parsedConfig, + bind: namedOutputs => { + for (const [name, output] of Object.entries(namedOutputs)) { + const ref = extension.output[name]; + if (!ref) { + throw new Error( + `Extension instance '${extension.id}' tried to bind unknown output '${name}'`, + ); + } + extensionData.set(ref.id, output); + } + }, + inputs: mapValues( + extension.inputs, + ({ extensionData: pointData }, inputName) => { + // TODO: validation + return (attachments.get(inputName) ?? []).map(attachment => + mapValues(pointData, ref => attachment.data.get(ref.id)), + ); + }, + ), + }); + } catch (e) { + throw new Error( + `Failed to instantiate extension instance '${extension.id}', ${e}`, + ); + } + + return { + id: options.extension.id, + data: extensionData, + attachments, + $$type: 'extension-instance', + }; +} diff --git a/packages/frontend-app-api/src/extensions/CoreRouter.tsx b/packages/frontend-app-api/src/extensions/CoreRouter.tsx new file mode 100644 index 0000000000..05f84aba6d --- /dev/null +++ b/packages/frontend-app-api/src/extensions/CoreRouter.tsx @@ -0,0 +1,58 @@ +/* + * 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 React from 'react'; +import { + createExtension, + coreExtensionData, +} from '@backstage/frontend-plugin-api'; +import { BrowserRouter, useRoutes } from 'react-router-dom'; + +export const CoreRouter = createExtension({ + id: 'core.router', + at: 'root', + inputs: { + routes: { + extensionData: { + path: coreExtensionData.routePath, + ref: coreExtensionData.routeRef, + component: coreExtensionData.reactComponent, + }, + }, + }, + output: { + component: coreExtensionData.reactComponent, + }, + factory({ bind, inputs }) { + const Routes = () => { + const element = useRoutes( + inputs.routes.map(route => ({ + path: route.path, + element: , + })), + ); + + return element; + }; + bind({ + component: () => ( + + + + ), + }); + }, +}); diff --git a/packages/frontend-app-api/src/index.ts b/packages/frontend-app-api/src/index.ts new file mode 100644 index 0000000000..9674eb345d --- /dev/null +++ b/packages/frontend-app-api/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. + */ + +/** + * Core API used by Backstage frontend apps. + * + * @packageDocumentation + */ + +export { createApp } from './createApp'; diff --git a/packages/frontend-app-api/src/routing/RoutingContext.tsx b/packages/frontend-app-api/src/routing/RoutingContext.tsx new file mode 100644 index 0000000000..237c8aa5df --- /dev/null +++ b/packages/frontend-app-api/src/routing/RoutingContext.tsx @@ -0,0 +1,52 @@ +/* + * 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 } from '@backstage/core-plugin-api'; +import React, { createContext, ReactNode } from 'react'; + +export interface RoutingContextType { + resolve( + routeRef: RouteRef, + options: { pathname: string }, + ): (() => string) | undefined; +} + +export const RoutingContext = createContext({ + resolve: () => () => '', +}); + +export class RouteResolver { + constructor(private readonly routePaths: Map) {} + + resolve(anyRouteRef: RouteRef<{}>): (() => string) | undefined { + const basePath = this.routePaths.get(anyRouteRef); + if (!basePath) { + return undefined; + } + return () => basePath; + } +} + +export function RoutingProvider(props: { + routePaths: Map; + children?: ReactNode; +}) { + return ( + + {props.children} + + ); +} diff --git a/packages/frontend-app-api/src/setupTests.ts b/packages/frontend-app-api/src/setupTests.ts new file mode 100644 index 0000000000..c30f1d15cb --- /dev/null +++ b/packages/frontend-app-api/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 '@testing-library/jest-dom'; diff --git a/packages/frontend-app-api/src/wiring/discovery.test.ts b/packages/frontend-app-api/src/wiring/discovery.test.ts new file mode 100644 index 0000000000..5f1c4df718 --- /dev/null +++ b/packages/frontend-app-api/src/wiring/discovery.test.ts @@ -0,0 +1,81 @@ +/* + * 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 { createPlugin } from '@backstage/frontend-plugin-api'; +import { getAvailablePlugins } from './discovery'; + +const globalSpy = jest.fn(); +Object.defineProperty(global, '__@backstage/discovered__', { + get: globalSpy, +}); + +describe('getAvailablePlugins', () => { + afterEach(jest.resetAllMocks); + + it('should discover nothing with undefined global', () => { + expect(getAvailablePlugins()).toEqual([]); + }); + + it('should discover nothing with empty global', () => { + globalSpy.mockReturnValue({ + modules: [], + }); + expect(getAvailablePlugins()).toEqual([]); + }); + + it('should discover a plugin', () => { + const testPlugin = createPlugin({ id: 'test' }); + globalSpy.mockReturnValue({ + modules: [{ default: testPlugin }], + }); + expect(getAvailablePlugins()).toEqual([testPlugin]); + }); + + it('should ignore garbage', () => { + globalSpy.mockReturnValueOnce({ modules: [{ default: null }] }); + expect(getAvailablePlugins()).toEqual([]); + globalSpy.mockReturnValueOnce({ modules: [{ default: undefined }] }); + expect(getAvailablePlugins()).toEqual([]); + globalSpy.mockReturnValueOnce({ modules: [{ default: Symbol() }] }); + expect(getAvailablePlugins()).toEqual([]); + globalSpy.mockReturnValueOnce({ modules: [{ default: () => {} }] }); + expect(getAvailablePlugins()).toEqual([]); + globalSpy.mockReturnValueOnce({ modules: [{ default: 0 }] }); + expect(getAvailablePlugins()).toEqual([]); + globalSpy.mockReturnValueOnce({ modules: [{ default: false }] }); + expect(getAvailablePlugins()).toEqual([]); + globalSpy.mockReturnValueOnce({ modules: [{ default: true }] }); + expect(getAvailablePlugins()).toEqual([]); + }); + + it('should discover multiple plugins', () => { + const test1Plugin = createPlugin({ id: 'test1' }); + const test2Plugin = createPlugin({ id: 'test2' }); + const test3Plugin = createPlugin({ id: 'test3' }); + globalSpy.mockReturnValue({ + modules: [ + { default: test1Plugin }, + { default: test2Plugin }, + { default: test3Plugin }, + ], + }); + expect(getAvailablePlugins()).toEqual([ + test1Plugin, + test2Plugin, + test3Plugin, + ]); + }); +}); diff --git a/packages/frontend-app-api/src/wiring/discovery.ts b/packages/frontend-app-api/src/wiring/discovery.ts new file mode 100644 index 0000000000..d872ae4441 --- /dev/null +++ b/packages/frontend-app-api/src/wiring/discovery.ts @@ -0,0 +1,41 @@ +/* + * 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 { BackstagePlugin } from '@backstage/frontend-plugin-api'; + +interface DiscoveryGlobal { + modules: Array<{ name: string; default: unknown }>; +} + +/** + * @public + */ +export function getAvailablePlugins(): BackstagePlugin[] { + const discovered = ( + window as { '__@backstage/discovered__'?: DiscoveryGlobal } + )['__@backstage/discovered__']; + + return ( + discovered?.modules.map(m => m.default).filter(isBackstagePlugin) ?? [] + ); +} + +function isBackstagePlugin(obj: unknown): obj is BackstagePlugin { + if (obj !== null && typeof obj === 'object' && '$$type' in obj) { + return obj.$$type === 'backstage-plugin'; + } + return false; +} diff --git a/packages/frontend-app-api/src/wiring/parameters.test.ts b/packages/frontend-app-api/src/wiring/parameters.test.ts new file mode 100644 index 0000000000..10b0e2fc6f --- /dev/null +++ b/packages/frontend-app-api/src/wiring/parameters.test.ts @@ -0,0 +1,375 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { createPlugin, Extension } from '@backstage/frontend-plugin-api'; +import { JsonValue } from '@backstage/types'; +import { + expandShorthandExtensionParameters, + mergeExtensionParameters, + readAppExtensionParameters, +} from './parameters'; + +function makeExt(id: string, status: 'disabled' | 'enabled' = 'enabled') { + return { + id, + at: 'root', + disabled: status === 'disabled', + } as Extension; +} + +describe('mergeExtensionParameters', () => { + it('should filter out disabled extension instances', () => { + expect( + mergeExtensionParameters({ + sources: [], + builtinExtensions: [makeExt('a', 'disabled')], + parameters: [], + }), + ).toEqual([]); + }); + + it('should pass through extension instances', () => { + const a = makeExt('a'); + const b = makeExt('b'); + expect( + mergeExtensionParameters({ + sources: [], + builtinExtensions: [a, b], + parameters: [], + }), + ).toEqual([ + { extension: a, at: 'root' }, + { extension: b, at: 'root' }, + ]); + }); + + it('should override attachment points', () => { + const a = makeExt('a'); + const b = makeExt('b'); + const pluginA = createPlugin({ id: 'test', extensions: [a] }); + expect( + mergeExtensionParameters({ + sources: [pluginA], + builtinExtensions: [b], + parameters: [ + { + id: 'b', + at: 'derp', + }, + ], + }), + ).toEqual([ + { extension: a, at: 'root', source: pluginA }, + { extension: b, at: 'derp' }, + ]); + }); + + it('should fully override configuration and duplicate', () => { + const a = makeExt('a'); + const b = makeExt('b'); + const plugin = createPlugin({ id: 'test', extensions: [a, b] }); + expect( + mergeExtensionParameters({ + sources: [plugin], + builtinExtensions: [], + parameters: [ + { + id: 'a', + config: { foo: { bar: 1 } }, + }, + { + id: 'b', + config: { foo: { bar: 2 } }, + }, + { + id: 'b', + config: { foo: { qux: 3 } }, + }, + ], + }), + ).toEqual([ + { extension: a, at: 'root', source: plugin, config: { foo: { bar: 1 } } }, + { extension: b, at: 'root', source: plugin, config: { foo: { qux: 3 } } }, + ]); + }); + + it('should place enabled instances in the order that they were enabled', () => { + const a = makeExt('a', 'disabled'); + const b = makeExt('b', 'disabled'); + expect( + mergeExtensionParameters({ + sources: [createPlugin({ id: 'empty', extensions: [] })], + builtinExtensions: [a, b], + parameters: [ + { + id: 'b', + disabled: false, + }, + { + id: 'a', + disabled: false, + }, + ], + }), + ).toEqual([ + { extension: b, at: 'root' }, + { extension: a, at: 'root' }, + ]); + }); +}); + +describe('readAppExtensionParameters', () => { + it('should disable extension with shorthand notation', () => { + expect( + readAppExtensionParameters( + new ConfigReader({ app: { extensions: [{ 'core.router': false }] } }), + ), + ).toEqual([ + { + id: 'core.router', + disabled: true, + }, + ]); + expect( + readAppExtensionParameters( + new ConfigReader({ + app: { extensions: [{ 'core.router': { disabled: true } }] }, + }), + ), + ).toEqual([ + { + at: undefined, + config: undefined, + disabled: true, + id: 'core.router', + }, + ]); + }); + + it('should enable extension with shorthand notation', () => { + expect( + readAppExtensionParameters( + new ConfigReader({ app: { extensions: ['core.router'] } }), + ), + ).toEqual([ + { + id: 'core.router', + disabled: false, + }, + ]); + expect( + readAppExtensionParameters( + new ConfigReader({ app: { extensions: [{ 'core.router': true }] } }), + ), + ).toEqual([ + { + id: 'core.router', + disabled: false, + }, + ]); + expect( + readAppExtensionParameters( + new ConfigReader({ + app: { extensions: [{ 'core.router': { disabled: false } }] }, + }), + ), + ).toEqual([ + { + id: 'core.router', + disabled: false, + }, + ]); + }); + + it('should not allow string keys', () => { + expect(() => + readAppExtensionParameters( + new ConfigReader({ + app: { + extensions: [{ 'core.router': 'some-string' }], + }, + }), + ), + ).toThrow( + 'Invalid extension configuration at app.extensions[0][core.router], value must be a boolean or object', + ); + }); + + it('should not allow invalid keys', () => { + expect(() => + readAppExtensionParameters( + new ConfigReader({ + app: { + extensions: [ + { + 'core.router/routes': { + extension: 'example-package#MyPage', + config: { foo: 'bar' }, + }, + }, + ], + }, + }), + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[0], extension ID must not contain slashes; got 'core.router/routes', did you mean 'core.router'?"`, + ); + }); +}); + +describe('expandShorthandExtensionParameters', () => { + const run = (value: JsonValue) => { + return expandShorthandExtensionParameters(value, 1); + }; + + it('rejects unknown keys', () => { + expect(() => run(null)).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1], must be a string or an object"`, + ); + expect(() => run(1)).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1], must be a string or an object"`, + ); + expect(() => run([])).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1], must be a string or an object"`, + ); + }); + + it('rejects the wrong number of keys', () => { + expect(() => run({})).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1], must have exactly one key, got none"`, + ); + expect(() => run({ a: {}, b: {} })).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1], must have exactly one key, got 'a', 'b'"`, + ); + }); + + it('rejects unknown values', () => { + expect(() => run({ a: 1 })).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1][a], value must be a boolean or object"`, + ); + expect(() => run({ a: [] })).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1][a], value must be a boolean or object"`, + ); + }); + + it('supports string key', () => { + expect(run('core.router')).toEqual({ + id: 'core.router', + disabled: false, + }); + expect(() => run('')).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1], extension ID must not be empty or contain whitespace"`, + ); + expect(() => run(' a')).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1], extension ID must not be empty or contain whitespace"`, + ); + expect(() => run('core.router/routes')).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1], extension ID must not contain slashes; got 'core.router/routes', did you mean 'core.router'?"`, + ); + }); + + it('supports null value', () => { + // this is the result of typing: + // - core.router: + // The missing value is interpreted as null by the yaml parser so we deal with that + expect(run({ 'core.router': null })).toEqual({ + id: 'core.router', + disabled: false, + }); + }); + + it('supports boolean value', () => { + expect(run({ 'core.router': true })).toEqual({ + id: 'core.router', + disabled: false, + }); + expect(run({ 'core.router': false })).toEqual({ + id: 'core.router', + disabled: true, + }); + }); + + it('should not support string values', () => { + expect(() => + run({ 'core.router': 'example-package#MyRouter' }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1][core.router], value must be a boolean or object"`, + ); + }); + + it('supports object id only in the key', () => { + expect(() => + run({ 'core.router': { id: 'some.id' } }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'at', 'disabled', 'config'"`, + ); + }); + + it('supports object at', () => { + expect(run({ 'core.router': { at: 'other.root/inputs' } })).toEqual({ + id: 'core.router', + at: 'other.root/inputs', + }); + expect(() => + run({ + 'core.router': { + id: 'other-id', + }, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'at', 'disabled', 'config'"`, + ); + }); + + it('supports object disabled', () => { + expect(run({ 'core.router': { disabled: true } })).toEqual({ + id: 'core.router', + disabled: true, + }); + expect(run({ 'core.router': { disabled: false } })).toEqual({ + id: 'core.router', + disabled: false, + }); + expect(() => + run({ 'core.router': { disabled: 0 } }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1][core.router].disabled, must be a boolean"`, + ); + }); + + it('supports object config', () => { + expect( + run({ 'core.router': { config: { disableRedirects: true } } }), + ).toEqual({ + id: 'core.router', + config: { disableRedirects: true }, + }); + expect(() => + run({ 'core.router': { config: 0 } }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1][core.router].config, must be an object"`, + ); + }); + + it('rejects unknown object keys', () => { + expect(() => + run({ 'core.router': { foo: { settings: true } } }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1][core.router].foo, unknown parameter; expected one of 'at', 'disabled', 'config'"`, + ); + }); +}); diff --git a/packages/frontend-app-api/src/wiring/parameters.ts b/packages/frontend-app-api/src/wiring/parameters.ts new file mode 100644 index 0000000000..1b2831aaca --- /dev/null +++ b/packages/frontend-app-api/src/wiring/parameters.ts @@ -0,0 +1,258 @@ +/* + * 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 { Config } from '@backstage/config'; +import { BackstagePlugin, Extension } from '@backstage/frontend-plugin-api'; +import { JsonValue } from '@backstage/types'; + +export interface ExtensionParameters { + id: string; + at?: string; + disabled?: boolean; + config?: unknown; +} + +const knownExtensionParameters = ['at', 'disabled', 'config']; + +// Since we'll never merge arrays in config the config reader context +// isn't too much of a help. Fall back to manual config reading logic +// as the Config interface makes it quite hard for us otherwise. +/** @internal */ +export function readAppExtensionParameters( + rootConfig: Config, +): ExtensionParameters[] { + const arr = rootConfig.getOptional('app.extensions'); + if (!Array.isArray(arr)) { + if (arr === undefined) { + return []; + } + // This will throw, and show which part of config had the wrong type + rootConfig.getConfigArray('app.extensions'); + return []; + } + + return arr.map((arrayEntry, arrayIndex) => + expandShorthandExtensionParameters(arrayEntry, arrayIndex), + ); +} + +/** @internal */ +export function expandShorthandExtensionParameters( + arrayEntry: JsonValue, + arrayIndex: number, +): ExtensionParameters { + function errorMsg(msg: string, key?: string, prop?: string) { + return `Invalid extension configuration at app.extensions[${arrayIndex}]${ + key ? `[${key}]` : '' + }${prop ? `.${prop}` : ''}, ${msg}`; + } + + // NOTE(freben): This check is intentionally not complete and doesn't check + // whether letters and digits are used, etc. It's not up to the config reading + // logic to decide what constitutes a valid extension ID; that should be + // decided by the logic that loads and instantiates the extensions. This check + // is just here to catch real mistakes or truly conceptually wrong input. + function assertValidId(id: string) { + if (!id || id !== id.trim()) { + throw new Error( + errorMsg('extension ID must not be empty or contain whitespace'), + ); + } + + if (id.includes('/')) { + let message = `extension ID must not contain slashes; got '${id}'`; + const good = id.split('/')[0]; + if (good) { + message += `, did you mean '${good}'?`; + } + throw new Error(errorMsg(message)); + } + } + + // Example YAML: + // - entity.card.about + if (typeof arrayEntry === 'string') { + assertValidId(arrayEntry); + return { + id: arrayEntry, + disabled: false, + }; + } + + // All remaining cases are single-key objects + if ( + typeof arrayEntry !== 'object' || + arrayEntry === null || + Array.isArray(arrayEntry) + ) { + throw new Error(errorMsg('must be a string or an object')); + } + const keys = Object.keys(arrayEntry); + if (keys.length !== 1) { + const joinedKeys = keys.length ? `'${keys.join("', '")}'` : 'none'; + throw new Error(errorMsg(`must have exactly one key, got ${joinedKeys}`)); + } + + const id = String(keys[0]); + const value = arrayEntry[id]; + assertValidId(id); + + // This example covers a potentially common mistake in the syntax + // Example YAML: + // - entity.card.about: + if (value === null) { + return { + id, + disabled: false, + }; + } + + // Example YAML: + // - catalog.page.cicd: false + if (typeof value === 'boolean') { + return { + id, + disabled: !value, + }; + } + + // The remaining case is the generic object. Example YAML: + // - tech-radar.page: + // at: core.router/routes + // disabled: false + // config: + // path: /tech-radar + // width: 1500 + // height: 800 + if (typeof value !== 'object' || Array.isArray(value)) { + // We don't mention null here - we don't want people to explicitly enter + // - entity.card.about: null + throw new Error(errorMsg('value must be a boolean or object', id)); + } + + const at = value.at; + const disabled = value.disabled; + const config = value.config; + + if (at !== undefined && typeof at !== 'string') { + throw new Error(errorMsg('must be a string', id, 'at')); + } else if (disabled !== undefined && typeof disabled !== 'boolean') { + throw new Error(errorMsg('must be a boolean', id, 'disabled')); + } else if ( + config !== undefined && + (typeof config !== 'object' || config === null || Array.isArray(config)) + ) { + throw new Error(errorMsg('must be an object', id, 'config')); + } + + const unknownKeys = Object.keys(value).filter( + k => !knownExtensionParameters.includes(k), + ); + if (unknownKeys.length > 0) { + throw new Error( + errorMsg( + `unknown parameter; expected one of '${knownExtensionParameters.join( + "', '", + )}'`, + id, + unknownKeys.join(', '), + ), + ); + } + + return { + id, + at, + disabled, + config, + }; +} + +export interface ExtensionInstanceParameters { + extension: Extension; + source?: BackstagePlugin; + at: string; + config?: unknown; +} + +/** @internal */ +export function mergeExtensionParameters(options: { + sources: BackstagePlugin[]; + builtinExtensions: Extension[]; + parameters: Array; +}): ExtensionInstanceParameters[] { + const { sources, builtinExtensions, parameters } = options; + + const overrides = [ + ...sources.flatMap(plugin => + plugin.extensions.map(extension => ({ + extension, + params: { + source: plugin, + at: extension.at, + disabled: extension.disabled, + config: undefined as unknown, + }, + })), + ), + ...builtinExtensions.map(extension => ({ + extension, + params: { + source: undefined, + at: extension.at, + disabled: extension.disabled, + config: undefined as unknown, + }, + })), + ]; + + for (const overrideParam of parameters) { + const existingIndex = overrides.findIndex( + e => e.extension.id === overrideParam.id, + ); + if (existingIndex !== -1) { + const existing = overrides[existingIndex]; + if (overrideParam.at) { + existing.params.at = overrideParam.at; + } + if (overrideParam.config) { + // TODO: merge config? + existing.params.config = overrideParam.config; + } + if ( + Boolean(existing.params.disabled) !== Boolean(overrideParam.disabled) + ) { + existing.params.disabled = Boolean(overrideParam.disabled); + if (!existing.params.disabled) { + // bump + overrides.splice(existingIndex, 1); + overrides.push(existing); + } + } + } else { + throw new Error(`Extension ${overrideParam.id} does not exist`); + } + } + + return overrides + .filter(override => !override.params.disabled) + .map(param => ({ + extension: param.extension, + at: param.params.at, + source: param.params.source, + config: param.params.config, + })); +} diff --git a/packages/frontend-plugin-api/.eslintrc.js b/packages/frontend-plugin-api/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/frontend-plugin-api/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/frontend-plugin-api/CHANGELOG.md b/packages/frontend-plugin-api/CHANGELOG.md new file mode 100644 index 0000000000..3070b37d9f --- /dev/null +++ b/packages/frontend-plugin-api/CHANGELOG.md @@ -0,0 +1,9 @@ +# @backstage/frontend-plugin-api + +## 0.0.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/types@1.1.0 diff --git a/packages/frontend-plugin-api/README.md b/packages/frontend-plugin-api/README.md new file mode 100644 index 0000000000..1fd3da79d8 --- /dev/null +++ b/packages/frontend-plugin-api/README.md @@ -0,0 +1,9 @@ +# @backstage/frontend-plugin-api + +**This package is EXPERIMENTAL, we recommend against using it for production deployments** + +This package provides the core API used by Backstage frontend plugins. It implements the design outlined in [RFC: Frontend System Evolution](https://github.com/backstage/backstage/issues/18372). + +## Documentation + +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md new file mode 100644 index 0000000000..2959dcb92e --- /dev/null +++ b/packages/frontend-plugin-api/api-report.md @@ -0,0 +1,290 @@ +## API Report File for "@backstage/frontend-plugin-api" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { AnyApiFactory } from '@backstage/core-plugin-api'; +import { AnyApiRef } from '@backstage/core-plugin-api'; +import { ComponentType } from 'react'; +import { JsonObject } from '@backstage/types'; +import { default as React_2 } from 'react'; +import { ReactNode } from 'react'; +import { RouteRef } from '@backstage/core-plugin-api'; +import { z } from 'zod'; +import { ZodSchema } from 'zod'; +import { ZodTypeDef } from 'zod'; + +// @public (undocumented) +export type AnyExtensionDataMap = { + [name in string]: ExtensionDataRef; +}; + +// @public (undocumented) +export interface BackstagePlugin { + // (undocumented) + $$type: 'backstage-plugin'; + // (undocumented) + extensions: Extension[]; + // (undocumented) + id: string; +} + +// @public (undocumented) +export interface ConfigurableExtensionDataRef< + TData, + TConfig extends { + optional?: true; + } = {}, +> extends ExtensionDataRef { + // (undocumented) + optional(): ConfigurableExtensionDataRef< + TData, + TData & { + optional: true; + } + >; +} + +// @public (undocumented) +export const coreExtensionData: { + reactComponent: ConfigurableExtensionDataRef, {}>; + routePath: ConfigurableExtensionDataRef; + apiFactory: ConfigurableExtensionDataRef; + routeRef: ConfigurableExtensionDataRef, {}>; +}; + +// @public (undocumented) +export function createApiExtension< + TConfig extends {}, + TInputs extends Record< + string, + { + extensionData: AnyExtensionDataMap; + } + >, +>( + options: ( + | { + api: AnyApiRef; + factory: (options: { + config: TConfig; + inputs: ExtensionDataInputValues; + }) => AnyApiFactory; + } + | { + factory: AnyApiFactory; + } + ) & { + configSchema?: PortableSchema; + inputs?: TInputs; + }, +): Extension; + +// @public (undocumented) +export function createExtension< + TOutput extends AnyExtensionDataMap, + TInputs extends Record< + string, + { + extensionData: AnyExtensionDataMap; + } + >, + TConfig = never, +>( + options: CreateExtensionOptions, +): Extension; + +// @public (undocumented) +export function createExtensionDataRef( + id: string, +): ConfigurableExtensionDataRef; + +// @public (undocumented) +export interface CreateExtensionOptions< + TOutput extends AnyExtensionDataMap, + TInputs extends Record< + string, + { + extensionData: AnyExtensionDataMap; + } + >, + TConfig, +> { + // (undocumented) + at: string; + // (undocumented) + configSchema?: PortableSchema; + // (undocumented) + disabled?: boolean; + // (undocumented) + factory(options: { + source?: BackstagePlugin; + bind: ExtensionDataBind; + config: TConfig; + inputs: ExtensionDataInputValues; + }): void; + // (undocumented) + id: string; + // (undocumented) + inputs?: TInputs; + // (undocumented) + output: TOutput; +} + +// @public +export function createPageExtension< + TConfig extends { + path: string; + }, + TInputs extends Record< + string, + { + extensionData: AnyExtensionDataMap; + } + >, +>( + options: ( + | { + defaultPath: string; + } + | { + configSchema: PortableSchema; + } + ) & { + id: string; + at?: string; + disabled?: boolean; + inputs?: TInputs; + routeRef?: RouteRef; + component: (props: { + config: TConfig; + inputs: ExtensionDataInputValues; + }) => Promise; + }, +): Extension; + +// @public (undocumented) +export function createPlugin(options: PluginOptions): BackstagePlugin; + +// @public (undocumented) +export function createSchemaFromZod( + schemaCreator: (zImpl: typeof z) => ZodSchema, +): PortableSchema; + +// @public (undocumented) +export interface Extension { + // (undocumented) + $$type: 'extension'; + // (undocumented) + at: string; + // (undocumented) + configSchema?: PortableSchema; + // (undocumented) + disabled: boolean; + // (undocumented) + factory(options: { + source?: BackstagePlugin; + bind: ExtensionDataBind; + config: TConfig; + inputs: Record>>; + }): void; + // (undocumented) + id: string; + // (undocumented) + inputs: Record< + string, + { + extensionData: AnyExtensionDataMap; + } + >; + // (undocumented) + output: AnyExtensionDataMap; +} + +// @public (undocumented) +export function ExtensionBoundary( + props: ExtensionBoundaryProps, +): React_2.JSX.Element; + +// @public (undocumented) +export interface ExtensionBoundaryProps { + // (undocumented) + children: ReactNode; + // (undocumented) + source?: BackstagePlugin; +} + +// @public (undocumented) +export type ExtensionDataBind = ( + values: { + [DataName in keyof TMap as TMap[DataName]['config'] extends { + optional: true; + } + ? never + : DataName]: TMap[DataName]['T']; + } & { + [DataName in keyof TMap as TMap[DataName]['config'] extends { + optional: true; + } + ? DataName + : never]?: TMap[DataName]['T']; + }, +) => void; + +// @public (undocumented) +export type ExtensionDataInputValues< + TInputs extends { + [name in string]: { + extensionData: AnyExtensionDataMap; + }; + }, +> = { + [InputName in keyof TInputs]: Array< + { + [DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends { + optional: true; + } + ? never + : DataName]: TInputs[InputName]['extensionData'][DataName]['T']; + } & { + [DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends { + optional: true; + } + ? DataName + : never]?: TInputs[InputName]['extensionData'][DataName]['T']; + } + >; +}; + +// @public (undocumented) +export type ExtensionDataRef< + TData, + TConfig extends { + optional?: true; + } = {}, +> = { + id: string; + T: TData; + config: TConfig; + $$type: 'extension-data'; +}; + +// @public (undocumented) +export interface PluginOptions { + // (undocumented) + extensions?: Extension[]; + // (undocumented) + id: string; +} + +// @public (undocumented) +export type PortableSchema = { + parse: (input: unknown) => TOutput; + schema: JsonObject; +}; + +// @public (undocumented) +export function useRouteRef(routeRef: RouteRef): () => string; +``` diff --git a/packages/frontend-plugin-api/catalog-info.yaml b/packages/frontend-plugin-api/catalog-info.yaml new file mode 100644 index 0000000000..4531f6eca3 --- /dev/null +++ b/packages/frontend-plugin-api/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-frontend-plugin-api + title: '@backstage/frontend-plugin-api' +spec: + lifecycle: experimental + type: backstage-web-library + owner: maintainers diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json new file mode 100644 index 0000000000..fda7bd8a05 --- /dev/null +++ b/packages/frontend-plugin-api/package.json @@ -0,0 +1,46 @@ +{ + "name": "@backstage/frontend-plugin-api", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "web-library" + }, + "sideEffects": false, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/frontend-app-api": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3" + }, + "files": [ + "dist" + ], + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0", + "react-router-dom": "6.0.0-beta.0 || ^6.3.0" + }, + "dependencies": { + "@backstage/core-plugin-api": "workspace:^", + "@backstage/types": "workspace:^", + "@types/react": "^16.13.1 || ^17.0.0", + "lodash": "^4.17.21", + "zod": "^3.21.4", + "zod-to-json-schema": "^3.21.4" + } +} diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx new file mode 100644 index 0000000000..c6a218c15f --- /dev/null +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx @@ -0,0 +1,29 @@ +/* + * 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 React, { ReactNode } from 'react'; +import { BackstagePlugin } from '../wiring'; + +/** @public */ +export interface ExtensionBoundaryProps { + children: ReactNode; + source?: BackstagePlugin; +} + +/** @public */ +export function ExtensionBoundary(props: ExtensionBoundaryProps) { + return <>{props.children}; +} diff --git a/packages/frontend-plugin-api/src/components/index.ts b/packages/frontend-plugin-api/src/components/index.ts new file mode 100644 index 0000000000..7056a59271 --- /dev/null +++ b/packages/frontend-plugin-api/src/components/index.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. + */ + +export { + ExtensionBoundary, + type ExtensionBoundaryProps, +} from './ExtensionBoundary'; diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts new file mode 100644 index 0000000000..fadb814d4f --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts @@ -0,0 +1,84 @@ +/* + * 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 { createApiExtension } from './createApiExtension'; +import { createApiFactory, createApiRef } from '@backstage/core-plugin-api'; + +describe('createApiExtension', () => { + it('fills in the expected values for an existing factory', () => { + const api = createApiRef<{ foo: string }>({ id: 'test' }); + const factory = createApiFactory({ + api, + deps: {}, + factory: () => ({ foo: 'bar' }), + }); + + const extension = createApiExtension({ + factory, + }); + + expect(extension).toEqual({ + $$type: 'extension', + id: 'apis.test', + at: 'core/apis', + disabled: false, + configSchema: undefined, + inputs: {}, + output: { + api: expect.objectContaining({ + $$type: 'extension-data', + id: 'core.api.factory', + config: {}, + }), + }, + factory: expect.any(Function), + }); + }); + + it('fills in the expected values for a ref and custom factory', () => { + const api = createApiRef<{ foo: string }>({ id: 'test' }); + const factory = jest.fn(() => ({ foo: 'bar' })); + + const extension = createApiExtension({ + api, + inputs: {}, + factory({ config: _config, inputs: _inputs }) { + return createApiFactory({ + api, + deps: {}, + factory, + }); + }, + }); + + expect(extension).toEqual({ + $$type: 'extension', + id: 'apis.test', + at: 'core/apis', + disabled: false, + configSchema: undefined, + inputs: {}, + output: { + api: expect.objectContaining({ + $$type: 'extension-data', + id: 'core.api.factory', + config: {}, + }), + }, + factory: expect.any(Function), + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts new file mode 100644 index 0000000000..f81f07ca72 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts @@ -0,0 +1,68 @@ +/* + * 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 { AnyApiFactory, AnyApiRef } from '@backstage/core-plugin-api'; +import { PortableSchema } from '../schema'; +import { + AnyExtensionDataMap, + ExtensionDataInputValues, + createExtension, + coreExtensionData, +} from '../wiring'; + +/** @public */ +export function createApiExtension< + TConfig extends {}, + TInputs extends Record, +>( + options: ( + | { + api: AnyApiRef; + factory: (options: { + config: TConfig; + inputs: ExtensionDataInputValues; + }) => AnyApiFactory; + } + | { + factory: AnyApiFactory; + } + ) & { + configSchema?: PortableSchema; + inputs?: TInputs; + }, +) { + const { factory, configSchema, inputs: extensionInputs } = options; + + const apiRef = + 'api' in options ? options.api : (factory as { api: AnyApiRef }).api; + + return createExtension({ + id: `apis.${apiRef.id}`, + at: 'core/apis', + inputs: extensionInputs, + configSchema, + output: { + api: coreExtensionData.apiFactory, + }, + factory({ bind, config, inputs }) { + if (typeof factory === 'function') { + bind({ api: factory({ config, inputs }) }); + } else { + bind({ api: factory }); + } + }, + }); +} diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx new file mode 100644 index 0000000000..db389ae4b1 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx @@ -0,0 +1,103 @@ +/* + * 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 React from 'react'; +import { PortableSchema } from '../schema'; +import { coreExtensionData } from '../wiring'; +import { createPageExtension } from './createPageExtension'; + +describe('createPageExtension', () => { + it('creates the extension properly', () => { + const configSchema: PortableSchema<{ path: string }> = { + parse: jest.fn(), + schema: {} as any, + }; + + expect( + createPageExtension({ + id: 'test', + configSchema, + component: async () =>
, + }), + ).toEqual({ + $$type: 'extension', + id: 'test', + at: 'core.router/routes', + configSchema: expect.anything(), + disabled: false, + inputs: {}, + output: { + component: expect.anything(), + path: expect.anything(), + routeRef: expect.anything(), + }, + factory: expect.any(Function), + }); + + expect( + createPageExtension({ + id: 'test', + at: 'other/place', + disabled: true, + configSchema, + inputs: { + first: { + extensionData: { component: coreExtensionData.reactComponent }, + }, + }, + component: async () =>
, + }), + ).toEqual({ + $$type: 'extension', + id: 'test', + at: 'other/place', + configSchema: expect.anything(), + disabled: true, + inputs: { + first: { + extensionData: { component: coreExtensionData.reactComponent }, + }, + }, + output: { + component: expect.anything(), + path: expect.anything(), + routeRef: expect.anything(), + }, + factory: expect.any(Function), + }); + + expect( + createPageExtension({ + id: 'test', + defaultPath: '/here', + component: async () =>
, + }), + ).toEqual({ + $$type: 'extension', + id: 'test', + at: 'core.router/routes', + configSchema: expect.anything(), + disabled: false, + inputs: {}, + output: { + component: expect.anything(), + path: expect.anything(), + routeRef: expect.anything(), + }, + factory: expect.any(Function), + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx new file mode 100644 index 0000000000..167dd397dd --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -0,0 +1,95 @@ +/* + * 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 } from '@backstage/core-plugin-api'; +import React from 'react'; +import { ExtensionBoundary } from '../components'; +import { createSchemaFromZod, PortableSchema } from '../schema'; +import { + AnyExtensionDataMap, + coreExtensionData, + createExtension, + Extension, + ExtensionDataInputValues, +} from '../wiring'; + +/** + * Helper for creating extensions for a routable React page component. + * + * @public + */ +export function createPageExtension< + TConfig extends { path: string }, + TInputs extends Record, +>( + options: ( + | { + defaultPath: string; + } + | { + configSchema: PortableSchema; + } + ) & { + id: string; + at?: string; + disabled?: boolean; + inputs?: TInputs; + routeRef?: RouteRef; + component: (props: { + config: TConfig; + inputs: ExtensionDataInputValues; + }) => Promise; + }, +): Extension { + const configSchema = + 'configSchema' in options + ? options.configSchema + : (createSchemaFromZod(z => + z.object({ path: z.string().default(options.defaultPath) }), + ) as PortableSchema); + + return createExtension({ + id: options.id, + at: options.at ?? 'core.router/routes', + disabled: options.disabled, + output: { + component: coreExtensionData.reactComponent, + path: coreExtensionData.routePath, + routeRef: coreExtensionData.routeRef.optional(), + }, + inputs: options.inputs, + configSchema, + factory({ bind, config, inputs, source }) { + const LazyComponent = React.lazy(() => + options + .component({ config, inputs }) + .then(element => ({ default: () => element })), + ); + + bind({ + path: config.path, + component: () => ( + + + + + + ), + routeRef: options.routeRef, + }); + }, + }); +} diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts new file mode 100644 index 0000000000..fa6e60894f --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/index.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 { createApiExtension } from './createApiExtension'; +export { createPageExtension } from './createPageExtension'; diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts new file mode 100644 index 0000000000..4e1a383899 --- /dev/null +++ b/packages/frontend-plugin-api/src/index.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +/** + * Core API used by Backstage frontend plugins. + * + * @packageDocumentation + */ + +export * from './components'; +export * from './extensions'; +export * from './schema'; +export * from './wiring'; +export * from './routing'; diff --git a/packages/frontend-plugin-api/src/routing/index.ts b/packages/frontend-plugin-api/src/routing/index.ts new file mode 100644 index 0000000000..4506538fa0 --- /dev/null +++ b/packages/frontend-plugin-api/src/routing/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { useRouteRef } from './useRouteRef'; diff --git a/packages/frontend-plugin-api/src/routing/useRouteRef.ts b/packages/frontend-plugin-api/src/routing/useRouteRef.ts new file mode 100644 index 0000000000..ad1c16ea8e --- /dev/null +++ b/packages/frontend-plugin-api/src/routing/useRouteRef.ts @@ -0,0 +1,38 @@ +/* + * 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 } from '@backstage/core-plugin-api'; +// eslint-disable-next-line @backstage/no-forbidden-package-imports +import { RoutingContext } from '@backstage/frontend-app-api/src/routing/RoutingContext'; +import { useContext, useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; + +/** @public */ +export function useRouteRef(routeRef: RouteRef): () => string { + const { pathname } = useLocation(); + const resolver = useContext(RoutingContext); + + const routeFunc = useMemo( + () => resolver && resolver.resolve(routeRef, { pathname }), + [resolver, routeRef, pathname], + ); + + if (!routeFunc) { + throw new Error(`Failed to resolve routeRef ${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 new file mode 100644 index 0000000000..90eba44249 --- /dev/null +++ b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.test.ts @@ -0,0 +1,34 @@ +/* + * 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 { createSchemaFromZod } from './createSchemaFromZod'; + +describe('createSchemaFromZod', () => { + it('should provide nice parse errors', () => { + const { parse } = createSchemaFromZod(z => + z.object({ + foo: z.union([z.string(), z.number()]), + derp: z.object({ bar: z.number() }), + }), + ); + + expect(() => 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`); + }); +}); diff --git a/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts new file mode 100644 index 0000000000..bfaf57f522 --- /dev/null +++ b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts @@ -0,0 +1,54 @@ +/* + * 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 { JsonObject } from '@backstage/types'; +import { z, ZodSchema, ZodTypeDef } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; +import { PortableSchema } from './types'; + +/** @public */ +export function createSchemaFromZod( + schemaCreator: (zImpl: typeof z) => ZodSchema, +): PortableSchema { + const schema = schemaCreator(z); + return { + // TODO: Types allow z.array etc here but it will break stuff + parse: input => { + const result = schema.safeParse(input); + if (result.success) { + return result.data; + } + + throw new Error(result.error.issues.map(formatIssue).join('; ')); + }, + // TODO: Verify why we are not compatible with the latest zodToJsonSchema. + schema: zodToJsonSchema(schema) as JsonObject, + }; +} + +function formatIssue(issue: z.ZodIssue): string { + if (issue.code === 'invalid_union') { + return formatIssue(issue.unionErrors[0].issues[0]); + } + let message = issue.message; + if (message === 'Required') { + message = `Missing required value`; + } + if (issue.path.length) { + message += ` at '${issue.path.join('.')}'`; + } + return message; +} diff --git a/packages/frontend-plugin-api/src/schema/index.ts b/packages/frontend-plugin-api/src/schema/index.ts new file mode 100644 index 0000000000..7f21c4e07d --- /dev/null +++ b/packages/frontend-plugin-api/src/schema/index.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 { createSchemaFromZod } from './createSchemaFromZod'; +export { type PortableSchema } from './types'; diff --git a/packages/frontend-plugin-api/src/schema/types.ts b/packages/frontend-plugin-api/src/schema/types.ts new file mode 100644 index 0000000000..9636d3b896 --- /dev/null +++ b/packages/frontend-plugin-api/src/schema/types.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. + */ + +import { JsonObject } from '@backstage/types'; + +/** @public */ +export type PortableSchema = { + parse: (input: unknown) => TOutput; + schema: JsonObject; +}; diff --git a/packages/frontend-plugin-api/src/setupTests.ts b/packages/frontend-plugin-api/src/setupTests.ts new file mode 100644 index 0000000000..c30f1d15cb --- /dev/null +++ b/packages/frontend-plugin-api/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 '@testing-library/jest-dom'; diff --git a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts new file mode 100644 index 0000000000..1f4e4370c9 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts @@ -0,0 +1,27 @@ +/* + * 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 { AnyApiFactory, RouteRef } from '@backstage/core-plugin-api'; +import { ComponentType } from 'react'; +import { createExtensionDataRef } from './createExtensionDataRef'; + +/** @public */ +export const coreExtensionData = { + reactComponent: createExtensionDataRef('core.reactComponent'), + routePath: createExtensionDataRef('core.routing.path'), + apiFactory: createExtensionDataRef('core.api.factory'), + routeRef: createExtensionDataRef('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 new file mode 100644 index 0000000000..a992642d61 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -0,0 +1,156 @@ +/* + * 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 { createExtension } from './createExtension'; +import { createExtensionDataRef } from './createExtensionDataRef'; + +const stringData = createExtensionDataRef('string'); + +function unused(..._any: any[]) {} + +describe('createExtension', () => { + it('should create an extension with a simple output', () => { + const extension = createExtension({ + id: 'test', + at: 'root', + output: { + foo: stringData, + }, + factory({ bind }) { + bind({ + foo: 'bar', + }); + bind({ + // @ts-expect-error + foo: 3, + }); + bind({ + // @ts-expect-error + bar: 'bar', + }); + // @ts-expect-error + bind({}); + // @ts-expect-error + bind(); + // @ts-expect-error + bind('bar'); + }, + }); + expect(extension.id).toBe('test'); + }); + + it('should create an extension with a some optional output', () => { + const extension = createExtension({ + id: 'test', + at: 'root', + output: { + foo: stringData, + bar: stringData.optional(), + }, + factory({ bind }) { + bind({ + foo: 'bar', + }); + bind({ + foo: 'bar', + bar: 'baz', + }); + bind({ + // @ts-expect-error + foo: 3, + }); + bind({ + foo: 'bar', + // @ts-expect-error + bar: 3, + }); + // @ts-expect-error + bind({ bar: 'bar' }); + // @ts-expect-error + bind({}); + // @ts-expect-error + bind(); + // @ts-expect-error + bind('bar'); + }, + }); + expect(extension.id).toBe('test'); + }); + + it('should create an extension with input', () => { + const extension = createExtension({ + id: 'test', + at: 'root', + inputs: { + mixed: { + extensionData: { + required: stringData, + optional: stringData.optional(), + }, + }, + onlyRequired: { + extensionData: { + required: stringData, + }, + }, + onlyOptional: { + extensionData: { + optional: stringData.optional(), + }, + }, + }, + output: { + foo: stringData, + }, + factory({ bind, inputs }) { + const a1: string = inputs.mixed?.[0].required; + // @ts-expect-error + const a2: number = inputs.mixed?.[0].required; + // @ts-expect-error + const a3: any = inputs.mixed?.[0].nonExistent; + unused(a1, a2, a3); + + const b1: string | undefined = inputs.mixed?.[0].optional; + // @ts-expect-error + const b2: string = inputs.mixed?.[0].optional; + // @ts-expect-error + const b3: number = inputs.mixed?.[0].optional; + // @ts-expect-error + const b4: number | undefined = inputs.mixed?.[0].optional; + unused(b1, b2, b3, b4); + + const c1: string = inputs.onlyRequired?.[0].required; + // @ts-expect-error + const c2: number = inputs.onlyRequired?.[0].required; + unused(c1, c2); + + const d1: string | undefined = inputs.onlyOptional?.[0].optional; + // @ts-expect-error + const d2: string = inputs.onlyOptional?.[0].optional; + // @ts-expect-error + const d3: number = inputs.onlyOptional?.[0].optional; + // @ts-expect-error + const d4: number | undefined = inputs.onlyOptional?.[0].optional; + unused(d1, d2, d3, d4); + + bind({ + foo: 'bar', + }); + }, + }); + expect(extension.id).toBe('test'); + }); +}); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts new file mode 100644 index 0000000000..bcea03595b --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -0,0 +1,101 @@ +/* + * 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 { PortableSchema } from '../schema'; +import { BackstagePlugin } from './createPlugin'; +import { AnyExtensionDataMap, Extension } from './types'; + +/** @public */ +export type ExtensionDataInputValues< + TInputs extends { [name in string]: { extensionData: AnyExtensionDataMap } }, +> = { + [InputName in keyof TInputs]: Array< + { + [DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends { + optional: true; + } + ? never + : DataName]: TInputs[InputName]['extensionData'][DataName]['T']; + } & { + [DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends { + optional: true; + } + ? DataName + : never]?: TInputs[InputName]['extensionData'][DataName]['T']; + } + >; +}; + +/** @public */ +export type ExtensionDataBind = ( + values: { + [DataName in keyof TMap as TMap[DataName]['config'] extends { + optional: true; + } + ? never + : DataName]: TMap[DataName]['T']; + } & { + [DataName in keyof TMap as TMap[DataName]['config'] extends { + optional: true; + } + ? DataName + : never]?: TMap[DataName]['T']; + }, +) => void; + +/** @public */ +export interface CreateExtensionOptions< + TOutput extends AnyExtensionDataMap, + TInputs extends Record, + TConfig, +> { + id: string; + at: string; + disabled?: boolean; + inputs?: TInputs; + output: TOutput; + configSchema?: PortableSchema; + factory(options: { + source?: BackstagePlugin; + bind: ExtensionDataBind; + config: TConfig; + inputs: ExtensionDataInputValues; + }): void; +} + +/** @public */ +export function createExtension< + TOutput extends AnyExtensionDataMap, + TInputs extends Record, + TConfig = never, +>( + options: CreateExtensionOptions, +): Extension { + return { + ...options, + disabled: options.disabled ?? false, + $$type: 'extension', + inputs: options.inputs ?? {}, + factory({ bind, config, inputs }) { + // TODO: Simplify this, but TS wouldn't infer the input type for some reason + return options.factory({ + bind, + config, + inputs: inputs as ExtensionDataInputValues, + }); + }, + }; +} diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts new file mode 100644 index 0000000000..6d6309bee4 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts @@ -0,0 +1,49 @@ +/* + * 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. + */ + +/** @public */ +export type ExtensionDataRef< + TData, + TConfig extends { optional?: true } = {}, +> = { + id: string; + T: TData; + config: TConfig; + $$type: 'extension-data'; +}; + +/** @public */ +export interface ConfigurableExtensionDataRef< + TData, + TConfig extends { optional?: true } = {}, +> extends ExtensionDataRef { + optional(): ConfigurableExtensionDataRef; +} + +// TODO: change to options object with ID. +/** @public */ +export function createExtensionDataRef( + id: string, +): ConfigurableExtensionDataRef { + return { + id, + $$type: 'extension-data', + config: {}, + optional() { + return { ...this, config: { ...this.config, optional: true } }; + }, + } as ConfigurableExtensionDataRef; +} diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts new file mode 100644 index 0000000000..724dad5143 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts @@ -0,0 +1,190 @@ +/* + * 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 React from 'react'; +import { createApp } from '@backstage/frontend-app-api'; +import { render, screen } from '@testing-library/react'; +import { createSchemaFromZod } from '../schema/createSchemaFromZod'; +import { createPlugin, BackstagePlugin } from './createPlugin'; +import { JsonObject } from '@backstage/types'; +import { createExtension } from './createExtension'; +import { createExtensionDataRef } from './createExtensionDataRef'; +import { coreExtensionData } from './coreExtensionData'; + +const nameExtensionDataRef = createExtensionDataRef('name'); + +const TechRadarPage = createExtension({ + id: 'plugin.techradar.page', + at: 'test.output/names', + output: { + name: nameExtensionDataRef, + }, + factory({ bind }) { + bind({ name: 'TechRadar' }); + }, +}); + +const CatalogPage = createExtension({ + id: 'plugin.catalog.page', + at: 'test.output/names', + output: { + name: nameExtensionDataRef, + }, + configSchema: createSchemaFromZod(z => + z.object({ name: z.string().default('Catalog') }), + ), + factory({ bind, config }) { + bind({ name: config.name }); + }, +}); + +const TechDocsAddon = createExtension({ + id: 'plugin.techdocs.addon.example', + at: 'plugin.techdocs.page/addons', + output: { + name: nameExtensionDataRef, + }, + configSchema: createSchemaFromZod(z => + z.object({ name: z.string().default('TechDocsAddon') }), + ), + factory({ bind, config }) { + bind({ name: config.name }); + }, +}); + +const TechDocsPage = createExtension({ + id: 'plugin.techdocs.page', + at: 'test.output/names', + inputs: { + addons: { + extensionData: { + name: nameExtensionDataRef, + }, + }, + }, + output: { + name: nameExtensionDataRef, + }, + factory({ bind, inputs }) { + bind({ name: `TechDocs-${inputs.addons.map(n => n.name).join('-')}` }); + }, +}); + +const outputExtension = createExtension({ + id: 'test.output', + at: 'root', + inputs: { + names: { + extensionData: { + name: nameExtensionDataRef, + }, + }, + }, + output: { + component: coreExtensionData.reactComponent, + }, + factory({ bind, inputs }) { + bind({ + component: () => + React.createElement('span', {}, [ + `Names: ${inputs.names.map(n => n.name).join(', ')}`, + ]), + }); + }, +}); + +function createTestAppRoot({ + plugins, + config = {}, +}: { + plugins: BackstagePlugin[]; + config: JsonObject; +}) { + Object.defineProperty(process.env, 'APP_CONFIG', { + value: [ + { + data: config, + context: 'test', + }, + ], + configurable: true, + }); + + return createApp({ plugins: plugins }).createRoot(); +} + +describe('createPlugin', () => { + it('should create an empty plugin', () => { + const plugin = createPlugin({ id: 'empty' }); + + expect(plugin).toBeDefined(); + }); + + it('should create a plugin with extension instances', () => { + const plugin = createPlugin({ + id: 'empty', + extensions: [TechRadarPage, CatalogPage, outputExtension], + }); + expect(plugin).toBeDefined(); + + render( + createTestAppRoot({ + plugins: [plugin], + config: { app: { extensions: [{ 'core.router': false }] } }, + }), + ); + + expect(screen.getByText('Names: TechRadar, Catalog')).toBeInTheDocument(); + }); + + it('should create a plugin with nested extension instances', () => { + const plugin = createPlugin({ + id: 'empty', + extensions: [ + TechRadarPage, + CatalogPage, + TechDocsPage, + TechDocsAddon, + outputExtension, + ], + }); + expect(plugin).toBeDefined(); + + render( + createTestAppRoot({ + plugins: [plugin], + config: { + app: { + extensions: [ + { 'core.router': false }, + { + 'plugin.catalog.page': { + config: { name: 'CatalogRenamed' }, + }, + }, + ], + }, + }, + }), + ); + + expect( + screen.getByText( + 'Names: TechRadar, CatalogRenamed, TechDocs-TechDocsAddon', + ), + ).toBeInTheDocument(); + }); +}); diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.ts new file mode 100644 index 0000000000..7f7e44ff03 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.ts @@ -0,0 +1,39 @@ +/* + * 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 { Extension } from './types'; + +/** @public */ +export interface PluginOptions { + id: string; + extensions?: Extension[]; +} + +/** @public */ +export interface BackstagePlugin { + $$type: 'backstage-plugin'; + id: string; + extensions: Extension[]; +} + +/** @public */ +export function createPlugin(options: PluginOptions): BackstagePlugin { + return { + ...options, + $$type: 'backstage-plugin', + extensions: options.extensions ?? [], + }; +} diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts new file mode 100644 index 0000000000..175200cf0c --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/index.ts @@ -0,0 +1,34 @@ +/* + * 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 { coreExtensionData } from './coreExtensionData'; +export { + createExtension, + type CreateExtensionOptions, + type ExtensionDataBind, + type ExtensionDataInputValues, +} from './createExtension'; +export { + createExtensionDataRef, + type ExtensionDataRef, + type ConfigurableExtensionDataRef, +} from './createExtensionDataRef'; +export { + createPlugin, + type BackstagePlugin, + type PluginOptions, +} from './createPlugin'; +export type { AnyExtensionDataMap, Extension } from './types'; diff --git a/packages/frontend-plugin-api/src/wiring/types.ts b/packages/frontend-plugin-api/src/wiring/types.ts new file mode 100644 index 0000000000..dbe4633ff5 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/types.ts @@ -0,0 +1,42 @@ +/* + * 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 { PortableSchema } from '../schema'; +import { ExtensionDataBind } from './createExtension'; +import { ExtensionDataRef } from './createExtensionDataRef'; +import { BackstagePlugin } from './createPlugin'; + +/** @public */ +export type AnyExtensionDataMap = { + [name in string]: ExtensionDataRef; +}; + +/** @public */ +export interface Extension { + $$type: 'extension'; + id: string; + at: string; + disabled: boolean; + inputs: Record; + output: AnyExtensionDataMap; + configSchema?: PortableSchema; + factory(options: { + source?: BackstagePlugin; + bind: ExtensionDataBind; + config: TConfig; + inputs: Record>>; + }): void; +} diff --git a/packages/integration-aws-node/CHANGELOG.md b/packages/integration-aws-node/CHANGELOG.md index f6d802fe1c..e251a013ea 100644 --- a/packages/integration-aws-node/CHANGELOG.md +++ b/packages/integration-aws-node/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/integration-aws-node +## 0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/errors@1.2.1 + +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/errors@1.2.1 + ## 0.1.5 ### Patch Changes diff --git a/packages/integration-aws-node/catalog-info.yaml b/packages/integration-aws-node/catalog-info.yaml new file mode 100644 index 0000000000..4c8264d575 --- /dev/null +++ b/packages/integration-aws-node/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-integration-aws-node + title: '@backstage/integration-aws-node' + description: Helpers for fetching AWS account credentials +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/packages/integration-aws-node/package.json b/packages/integration-aws-node/package.json index 808c976730..34d5aede67 100644 --- a/packages/integration-aws-node/package.json +++ b/packages/integration-aws-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration-aws-node", "description": "Helpers for fetching AWS account credentials", - "version": "0.1.5", + "version": "0.1.6-next.1", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index dd43ed34a0..5555d52bc9 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/integration-react +## 1.1.19-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/integration@1.7.0-next.2 + - @backstage/theme@0.4.1 + +## 1.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## 1.1.18-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## 1.1.16 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/integration@1.6.0 + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + ## 1.1.16-next.1 ### Patch Changes diff --git a/packages/integration-react/api-report.md b/packages/integration-react/api-report.md index 3af0704990..c1a2e34907 100644 --- a/packages/integration-react/api-report.md +++ b/packages/integration-react/api-report.md @@ -3,8 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { ApiFactory } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; import { AuthRequestOptions } from '@backstage/core-plugin-api'; @@ -13,6 +11,7 @@ import { Config } from '@backstage/config'; import { OAuthApi } from '@backstage/core-plugin-api'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { SessionApi } from '@backstage/core-plugin-api'; @@ -108,7 +107,7 @@ export interface ScmAuthTokenResponse { // @public export const ScmIntegrationIcon: ( props: ScmIntegrationIconProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type ScmIntegrationIconProps = { diff --git a/packages/integration-react/catalog-info.yaml b/packages/integration-react/catalog-info.yaml new file mode 100644 index 0000000000..8df5e52c95 --- /dev/null +++ b/packages/integration-react/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-integration-react + title: '@backstage/integration-react' + description: Frontend package for managing integrations towards external systems +spec: + lifecycle: production + type: backstage-web-library + owner: maintainers diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 71f7597635..f20d7dd662 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration-react", "description": "Frontend package for managing integrations towards external systems", - "version": "1.1.16-next.1", + "version": "1.1.19-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,6 +19,7 @@ "url": "https://github.com/backstage/backstage", "directory": "packages/integration-react" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -37,6 +38,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -52,8 +54,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index d1f3f46306..c9bb363d4c 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/integration +## 1.7.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/errors@1.2.1 + +## 1.7.0-next.1 + +### Patch Changes + +- 2d2fc9d20ebb: Additional fix for Gitiles auth links +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/errors@1.2.1 + +## 1.7.0-next.0 + +### Minor Changes + +- 5f1a92b9f19f: Added `AzureDevOpsCredentialsProvider` to support multiple Azure DevOps organizations and **deprecated** `AzureIntegrationConfig.credential` and `AzureIntegrationConfig.token` in favour of `AzureIntegrationConfig.credentials`. You can now use specific credentials for different Azure DevOps (Server) organizations by specifying the `organizations` field on a credential: + + ```yaml + integrations: + azure: + - host: dev.azure.com + credentials: + - organizations: + - my-org + - my-other-org + clientId: ${AZURE_CLIENT_ID} + clientSecret: ${AZURE_CLIENT_SECRET} + tenantId: ${AZURE_TENANT_ID} + - organizations: + - yet-another-org + personalAccessToken: ${PERSONAL_ACCESS_TOKEN} + ``` + + See the [Azure integration documentation](https://backstage.io/docs/integrations/azure/locations) for more information. + +### Patch Changes + +- cb2e19d82d95: Gitiles: Fixed auth prefix issue +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## 1.6.0 + +### Minor Changes + +- 443afcf7f567: Added `buildGerritGitilesArchiveUrl()` to construct a Gitiles URL to download an archive. + Gitiles URL that uses an authenticated prefix (`/a/`) can now be parsed by the integration. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + ## 1.5.1 ### Patch Changes diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index c2501b4ac2..f64c06257e 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -39,16 +39,58 @@ export type AwsS3IntegrationConfig = { }; // @public -export type AzureClientSecretCredential = { +export type AzureClientSecretCredential = AzureCredentialBase & { + kind: 'ClientSecret'; tenantId: string; clientId: string; clientSecret: string; }; // @public -export type AzureCredential = +export type AzureCredentialBase = { + kind: AzureDevOpsCredentialKind; + organizations?: string[]; +}; + +// @public +export type AzureDevOpsCredential = | AzureClientSecretCredential - | AzureManagedIdentityCredential; + | AzureManagedIdentityCredential + | PersonalAccessTokenCredential; + +// @public +export type AzureDevOpsCredentialKind = + | 'PersonalAccessToken' + | 'ClientSecret' + | 'ManagedIdentity'; + +// @public +export type AzureDevOpsCredentialLike = Omit< + Partial & + Partial & + Partial, + 'kind' +>; + +// @public +export type AzureDevOpsCredentials = { + headers: { + [name: string]: string; + }; + token: string; + type: AzureDevOpsCredentialType; +}; + +// @public +export interface AzureDevOpsCredentialsProvider { + // (undocumented) + getCredentials(opts: { + url: string; + }): Promise; +} + +// @public +export type AzureDevOpsCredentialType = 'bearer' | 'pat'; // @public export class AzureIntegration implements ScmIntegration { @@ -75,11 +117,13 @@ export class AzureIntegration implements ScmIntegration { export type AzureIntegrationConfig = { host: string; token?: string; - credential?: AzureCredential; + credential?: AzureDevOpsCredential; + credentials?: AzureDevOpsCredential[]; }; // @public -export type AzureManagedIdentityCredential = { +export type AzureManagedIdentityCredential = AzureCredentialBase & { + kind: 'ManagedIdentity'; clientId: string; }; @@ -172,6 +216,28 @@ export type BitbucketServerIntegrationConfig = { password?: string; }; +// @public +export function buildGerritGitilesArchiveUrl( + config: GerritIntegrationConfig, + project: string, + branch: string, + filePath: string, +): string; + +// @public +export class DefaultAzureDevOpsCredentialsProvider + implements AzureDevOpsCredentialsProvider +{ + // (undocumented) + static fromIntegrations( + integrations: ScmIntegrationRegistry, + ): DefaultAzureDevOpsCredentialsProvider; + // (undocumented) + getCredentials(opts: { + url: string; + }): Promise; +} + // @public export class DefaultGithubCredentialsProvider implements GithubCredentialsProvider @@ -242,7 +308,7 @@ export function getAzureDownloadUrl(url: string): string; // @public export function getAzureFileFetchUrl(url: string): string; -// @public +// @public @deprecated export function getAzureRequestOptions( config: AzureIntegrationConfig, additionalHeaders?: Record, @@ -592,6 +658,12 @@ export function parseGerritGitilesUrl( // @public export function parseGerritJsonResponse(response: Response): Promise; +// @public +export type PersonalAccessTokenCredential = AzureCredentialBase & { + kind: 'PersonalAccessToken'; + personalAccessToken: string; +}; + // @public export function readAwsS3IntegrationConfig( config: Config, diff --git a/packages/integration/catalog-info.yaml b/packages/integration/catalog-info.yaml new file mode 100644 index 0000000000..7f602c42be --- /dev/null +++ b/packages/integration/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-integration + title: '@backstage/integration' + description: Helpers for managing integrations towards external systems +spec: + lifecycle: production + type: backstage-common-library + owner: maintainers diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index a1eb7aceb9..9c08ec03a9 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -30,8 +30,38 @@ export interface Config { /** * Token used to authenticate requests. * @visibility secret + * @deprecated Use `credentials` instead. */ token?: string; + + /** + * The credential to use for requests. + * + * If no credential is specified anonymous access is used. + * + * @visibility secret + * @deprecated Use `credentials` instead. + */ + credential?: { + clientId?: string; + clientSecret?: string; + tenantId?: string; + personalAccessToken?: string; + }; + + /** + * The credentials to use for requests. If multiple credentials are specified the first one that matches the organization is used. + * If not organization matches the first credential without an organization is used. + * + * If no credentials are specified at all, either a default credential (for Azure DevOps) or anonymous access (for Azure DevOps Server) is used. + * @visibility secret + */ + credentials?: { + clientId?: string; + clientSecret?: string; + tenantId?: string; + personalAccessToken?: string; + }[]; }>; /** diff --git a/packages/integration/package.json b/packages/integration/package.json index e02e070266..3e4f7ec2f2 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration", "description": "Helpers for managing integrations towards external systems", - "version": "1.5.1", + "version": "1.7.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,6 +23,7 @@ "keywords": [ "backstage" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/packages/integration/src/azure/AzureIntegration.ts b/packages/integration/src/azure/AzureIntegration.ts index d036d88c21..fe2cbf349c 100644 --- a/packages/integration/src/azure/AzureIntegration.ts +++ b/packages/integration/src/azure/AzureIntegration.ts @@ -29,6 +29,7 @@ export class AzureIntegration implements ScmIntegration { const configs = readAzureIntegrationConfigs( config.getOptionalConfigArray('integrations.azure') ?? [], ); + return basicIntegrations( configs.map(c => new AzureIntegration(c)), i => i.config.host, diff --git a/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.test.ts b/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.test.ts new file mode 100644 index 0000000000..c9fbb4c324 --- /dev/null +++ b/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.test.ts @@ -0,0 +1,188 @@ +/* + * 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 { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider'; +import { + AccessToken, + ClientSecretCredential, + ManagedIdentityCredential, +} from '@azure/identity'; + +const MockedClientSecretCredential = ClientSecretCredential as jest.MockedClass< + typeof ClientSecretCredential +>; + +const MockedManagedIdentityCredential = + ManagedIdentityCredential as jest.MockedClass< + typeof ManagedIdentityCredential + >; + +jest.mock('@azure/identity'); + +const seconds = (s: number) => s * 1000; +const minutes = (m: number) => seconds(60) * m; +const hours = (h: number) => minutes(60) * h; + +describe('CachedAzureDevOpsCredentialsProvider', () => { + beforeEach(() => { + jest.resetAllMocks(); + + MockedClientSecretCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: Date.now() + hours(8), + token: 'fake-client-secret-token', + } as AccessToken), + ); + + MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: Date.now() + hours(8), + token: 'fake-managed-identity-token', + } as AccessToken), + ); + }); + + it('Should return a pat credential when a personal access token is configured', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'PersonalAccessToken', + personalAccessToken: 'token', + }); + const { headers, type, token } = await manager.getCredentials(); + + expect(headers).toStrictEqual({ + Authorization: `Basic ${btoa(`:${token}`)}`, + }); + + expect(type).toBe('pat'); + expect(token).toBe('token'); + }); + + it('Should return a bearer credential when a client secret is configured', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenantId', + }); + + const { headers, type, token } = await manager.getCredentials(); + + expect(headers).toStrictEqual({ + Authorization: `Bearer fake-client-secret-token`, + }); + + expect(type).toBe('bearer'); + expect(token).toBe('fake-client-secret-token'); + }); + + it('Should return a bearer credential when a managed identity is configured', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ManagedIdentity', + clientId: 'id', + }); + + const { headers, type, token } = await manager.getCredentials(); + + expect(headers).toStrictEqual({ + Authorization: `Bearer fake-managed-identity-token`, + }); + + expect(type).toBe('bearer'); + expect(token).toBe('fake-managed-identity-token'); + }); + + it('Should not refresh the client secret token when it does not expire within 10 minutes', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenantId', + }); + + await manager.getCredentials(); + await manager.getCredentials(); + await manager.getCredentials(); + + expect( + MockedClientSecretCredential.prototype.getToken, + ).toHaveBeenCalledTimes(1); + }); + + it('Should return the managed identity token when it does not expire within 10 minutes', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ManagedIdentity', + clientId: 'id', + }); + + await manager.getCredentials(); + await manager.getCredentials(); + await manager.getCredentials(); + + expect( + MockedManagedIdentityCredential.prototype.getToken, + ).toHaveBeenCalledTimes(1); + }); + + it('Should refresh the client secret token when it expires within 10 minutes', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenantId', + }); + + MockedClientSecretCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: Date.now() + minutes(9) + seconds(59), + token: 'fake-client-secret-token', + } as AccessToken), + ); + + await manager.getCredentials(); + await manager.getCredentials(); + + expect( + MockedClientSecretCredential.prototype.getToken, + ).toHaveBeenCalledTimes(2); + }); + + it('Should refresh the managed identity token when it expires within 10 minutes', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ManagedIdentity', + clientId: 'id', + }); + + MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: Date.now() + minutes(9) + seconds(59), + token: 'fake-managed-identity-token', + } as AccessToken), + ); + + await manager.getCredentials(); + await manager.getCredentials(); + + expect( + MockedManagedIdentityCredential.prototype.getToken, + ).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.ts b/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.ts new file mode 100644 index 0000000000..aee4cefc41 --- /dev/null +++ b/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.ts @@ -0,0 +1,127 @@ +/* + * 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 { AzureDevOpsCredential, PersonalAccessTokenCredential } from './config'; +import { + ClientSecretCredential, + ManagedIdentityCredential, + TokenCredential, +} from '@azure/identity'; +import { + AzureDevOpsCredentials, + AzureDevOpsCredentialsProvider, +} from './types'; + +type CachedAzureDevOpsCredentials = AzureDevOpsCredentials & { + expiresAt?: number; +}; + +function exhaustiveCheck(_param: never) {} + +const tenMinutes = 1000 * 60 * 10; + +/** + * A credentials provider that caches the credentials for as long as it is valid. + * + * @public + */ +export class CachedAzureDevOpsCredentialsProvider + implements AzureDevOpsCredentialsProvider +{ + azureDevOpsScope = '499b84ac-1321-427f-aa17-267ca6975798/.default'; + cached: CachedAzureDevOpsCredentials | undefined; + + static fromAzureDevOpsCredential( + credential: AzureDevOpsCredential, + ): CachedAzureDevOpsCredentialsProvider { + switch (credential.kind) { + case 'PersonalAccessToken': + return CachedAzureDevOpsCredentialsProvider.fromPersonalAccessTokenCredential( + credential, + ); + case 'ClientSecret': + return CachedAzureDevOpsCredentialsProvider.fromTokenCredential( + new ClientSecretCredential( + credential.tenantId, + credential.clientId, + credential.clientSecret, + ), + ); + case 'ManagedIdentity': + return CachedAzureDevOpsCredentialsProvider.fromTokenCredential( + new ManagedIdentityCredential(credential.clientId), + ); + default: + exhaustiveCheck(credential); + + throw new Error( + `Credential kind '${(credential as any).kind}' not supported`, + ); + } + } + + static fromTokenCredential( + credential: TokenCredential, + ): CachedAzureDevOpsCredentialsProvider { + return new CachedAzureDevOpsCredentialsProvider(credential); + } + + static fromPersonalAccessTokenCredential( + credential: PersonalAccessTokenCredential, + ) { + return new CachedAzureDevOpsCredentialsProvider( + credential.personalAccessToken, + ); + } + + private constructor(private readonly credential: TokenCredential | string) {} + + async getCredentials(): Promise { + if ( + this.cached === undefined || + (this.cached.expiresAt !== undefined && + Date.now() > this.cached.expiresAt) + ) { + if (typeof this.credential === 'string') { + this.cached = { + headers: { + Authorization: `Basic ${btoa(`:${this.credential}`)}`, + }, + type: 'pat', + token: this.credential, + }; + } else { + const accessToken = await this.credential.getToken( + this.azureDevOpsScope, + ); + + if (!accessToken) { + throw new Error('Failed to retrieve access token'); + } + + this.cached = { + expiresAt: accessToken.expiresOnTimestamp - tenMinutes, + headers: { + Authorization: `Bearer ${accessToken.token}`, + }, + type: 'bearer', + token: accessToken.token, + }; + } + } + + return this.cached; + } +} diff --git a/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.test.ts b/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.test.ts new file mode 100644 index 0000000000..8f66d5742d --- /dev/null +++ b/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.test.ts @@ -0,0 +1,465 @@ +/* + * 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 { ScmIntegrations } from '../ScmIntegrations'; +import { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider'; +import { AzureDevOpsCredentialLike, AzureIntegrationConfig } from './config'; +import { ConfigReader } from '@backstage/config'; +import { DefaultAzureDevOpsCredentialsProvider } from './DefaultAzureDevOpsCredentialsProvider'; +import { + AccessToken, + ClientSecretCredential, + DefaultAzureCredential, + ManagedIdentityCredential, +} from '@azure/identity'; +import { DateTime } from 'luxon'; + +type AzureIntegrationConfigLike = Partial< + Omit +> & { + credential?: Partial; + credentials?: Partial[]; +}; + +const MockedClientSecretCredential = ClientSecretCredential as jest.MockedClass< + typeof ClientSecretCredential +>; + +const MockedManagedIdentityCredential = + ManagedIdentityCredential as jest.MockedClass< + typeof ManagedIdentityCredential + >; + +const MockedDefaultAzureCredential = DefaultAzureCredential as jest.MockedClass< + typeof DefaultAzureCredential +>; + +jest.mock('@azure/identity'); + +describe('DefaultAzureDevOpsCredentialProvider', () => { + const buildProvider = (azureIntegrations: AzureIntegrationConfigLike[]) => + DefaultAzureDevOpsCredentialsProvider.fromIntegrations( + ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + azure: azureIntegrations, + }, + }), + ), + ); + + beforeEach(() => { + jest.resetAllMocks(); + + MockedClientSecretCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), + token: 'fake-client-secret-token', + } as AccessToken), + ); + + MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), + token: 'fake-managed-identity-token', + } as AccessToken), + ); + + MockedDefaultAzureCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), + token: 'fake-default-azure-credential-token', + } as AccessToken), + ); + + jest.spyOn( + CachedAzureDevOpsCredentialsProvider, + 'fromAzureDevOpsCredential', + ); + + jest.spyOn(CachedAzureDevOpsCredentialsProvider, 'fromTokenCredential'); + + jest.spyOn( + CachedAzureDevOpsCredentialsProvider, + 'fromPersonalAccessTokenCredential', + ); + }); + + describe('fromIntegrations', () => { + it('Should create a credential provider when a credential is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 'pat', + }, + ], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential, + ).toHaveBeenCalledTimes(1); + }); + + it('Should create a single credential provider per credential', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'client-id-1', + }, + { + organizations: ['org3', 'org4'], + clientId: 'client-id-2', + }, + ], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential, + ).toHaveBeenCalledTimes(2); + }); + + it('Should create a default credential provider for Azure DevOps when no credential is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledTimes(1); + + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledWith(new DefaultAzureCredential()); + }); + + it('Should create a default credential provider for Azure DevOps when no default credential is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'client-id', + tenantId: 'tenant-id', + clientSecret: 'client-secret', + }, + ], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledTimes(2); + + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledWith(new DefaultAzureCredential()); + }); + + it('Should not create a default credential provider for Azure DevOps when another default credential is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + clientId: 'client-id', + tenantId: 'tenant-id', + clientSecret: 'client-secret', + }, + ], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledTimes(1); + + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledWith(expect.any(ClientSecretCredential)); + }); + + it('Should not create a default credential provider for on-premise Azure DevOps server when no credential is specified', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential, + ).toHaveBeenCalledTimes(0); + + // expect 1 call because the Azure integration adds a default integration for dev.azure.com when it is not configured + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledTimes(1); + + expect( + CachedAzureDevOpsCredentialsProvider.fromPersonalAccessTokenCredential, + ).toHaveBeenCalledTimes(0); + }); + }); + + describe('getCredentials', () => { + describe('Azure DevOps (dev.azure.com)', () => { + it('Should return a token when a credential with the same organization is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1'], + personalAccessToken: 'pat', + }, + ], + }, + ]); + + const credentials = provider.getCredentials({ + url: 'https://dev.azure.com/org1/project1', + }); + + expect(credentials).toBeDefined(); + }); + + it('Should return DefaultAzureCredential when no credential with the same organization is specified and host is dev.azure.com', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1'], + personalAccessToken: 'pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: 'https://dev.azure.com/org2/project1', + }); + + expect(credentials).toMatchObject({ + token: 'fake-default-azure-credential-token', + }); + }); + + it('Should return undefined when no credential is specified and host is Azure DevOps server', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [], + }, + ]); + + const credentials = await provider.getCredentials({ + url: 'https://my.devops.server/org2/project1', + }); + + expect(credentials).toBeUndefined(); + }); + + it('Should prefer organization credential when a credential with the same organization is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 'fallback-pat', + }, + { + organizations: ['org1'], + personalAccessToken: 'org1-pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: 'https://dev.azure.com/org1/project1', + }); + + expect(credentials).toMatchObject({ + token: 'org1-pat', + }); + }); + + it('Should fallback to host credential when no credential with the same organization is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 'fallback-pat', + }, + { + organizations: ['org1'], + personalAccessToken: 'org1-pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: 'https://dev.azure.com/org2/project1', + }); + + expect(credentials).toMatchObject({ + token: 'fallback-pat', + }); + }); + }); + describe('Azure DevOps Server', () => { + [ + 'https://{host}/{organization}/{project}', + 'https://{host}/tfs/{organization}/{project}', + ].map(format => { + describe(`With url format ${format}`, () => { + const formatUrl = (opts: { + host: string; + organization: string; + project: string; + }) => + format + .replace('{host}', opts.host) + .replace('{organization}', opts.organization) + .replace('{project}', opts.project); + + it('Should return a token when a credential with the same host is specified', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [ + { + personalAccessToken: 'pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: formatUrl({ + host: 'my.devops.server', + organization: 'org1', + project: 'project1', + }), + }); + + expect(credentials).toBeDefined(); + }); + + it('Should prefer organization credential when a credential with the same organization is specified', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [ + { + personalAccessToken: 'fallback-pat', + }, + { + organizations: ['org1'], + personalAccessToken: 'org1-pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: formatUrl({ + host: 'my.devops.server', + organization: 'org1', + project: 'project1', + }), + }); + + expect(credentials).toMatchObject({ + token: 'org1-pat', + }); + }); + + it('Should fallback to host credential when no credential with the same organization is specified', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [ + { + personalAccessToken: 'fallback-pat', + }, + { + organizations: ['org1'], + personalAccessToken: 'org1-pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: formatUrl({ + host: 'my.devops.server', + organization: 'org2', + project: 'project1', + }), + }); + + expect(credentials).toMatchObject({ + token: 'fallback-pat', + }); + }); + + it('Should return a undefined when no credential with the same host is specified', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [ + { + personalAccessToken: 'pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: formatUrl({ + host: 'my.other.devops.server', + organization: 'org1', + project: 'project1', + }), + }); + + expect(credentials).toBeUndefined(); + }); + }); + }); + }); + }); +}); diff --git a/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.ts b/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.ts new file mode 100644 index 0000000000..1ff35b0612 --- /dev/null +++ b/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.ts @@ -0,0 +1,132 @@ +/* + * 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 { + AzureDevOpsCredentials, + AzureDevOpsCredentialsProvider, +} from './types'; +import { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider'; +import { ScmIntegrationRegistry } from '../registry'; +import { DefaultAzureCredential } from '@azure/identity'; + +/** + * Default implementation of AzureDevOpsCredentialsProvider. + * @public + */ +export class DefaultAzureDevOpsCredentialsProvider + implements AzureDevOpsCredentialsProvider +{ + static fromIntegrations( + integrations: ScmIntegrationRegistry, + ): DefaultAzureDevOpsCredentialsProvider { + const providers = integrations.azure.list().reduce((acc, integration) => { + integration.config.credentials?.forEach(credential => { + if ( + credential.organizations === undefined || + credential.organizations.length === 0 + ) { + if (acc.get(integration.config.host) === undefined) { + acc.set( + integration.config.host, + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential( + credential, + ), + ); + } + } else { + const provider = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential( + credential, + ); + credential.organizations?.forEach(organization => { + acc.set(`${integration.config.host}/${organization}`, provider); + }); + } + }); + + if ( + integration.config.host === 'dev.azure.com' && + acc.get(integration.config.host) === undefined + ) { + acc.set( + integration.config.host, + CachedAzureDevOpsCredentialsProvider.fromTokenCredential( + new DefaultAzureCredential(), + ), + ); + } + + return acc; + }, new Map()); + + return new DefaultAzureDevOpsCredentialsProvider(providers); + } + + private constructor( + private readonly providers: Map< + string, + CachedAzureDevOpsCredentialsProvider + >, + ) {} + + private forAzureDevOpsServerOrganization( + url: URL, + ): AzureDevOpsCredentialsProvider | undefined { + const parts = url.pathname.split('/').filter(part => part !== ''); + if (url.host !== 'dev.azure.com' && parts.length > 0) { + if (parts[0] !== 'tfs') { + // url format: https://{host}/{organization} + return this.providers.get(`${url.host}/${parts[0]}`); + } else if (parts[0] === 'tfs' && parts.length > 1) { + // url format: https://{host}/tfs/{organization} + return this.providers.get(`${url.host}/${parts[1]}`); + } + } + + return undefined; + } + + private forAzureDevOpsOrganization( + url: URL, + ): AzureDevOpsCredentialsProvider | undefined { + const parts = url.pathname.split('/').filter(part => part !== ''); + if (url.host === 'dev.azure.com' && parts.length > 0) { + // url format: https://{host}/{organization} + return this.providers.get(`${url.host}/${parts[0]}`); + } + + return undefined; + } + + private forHost(url: URL): AzureDevOpsCredentialsProvider | undefined { + return this.providers.get(url.host); + } + + async getCredentials(opts: { + url: string; + }): Promise { + const url = new URL(opts.url); + const provider = + this.forAzureDevOpsOrganization(url) ?? + this.forAzureDevOpsServerOrganization(url) ?? + this.forHost(url); + + if (provider === undefined) { + return undefined; + } + + return provider.getCredentials(opts); + } +} diff --git a/packages/integration/src/azure/config.test.ts b/packages/integration/src/azure/config.test.ts index 54711e4ddb..eb36120c32 100644 --- a/packages/integration/src/azure/config.test.ts +++ b/packages/integration/src/azure/config.test.ts @@ -17,18 +17,30 @@ import { Config, ConfigReader } from '@backstage/config'; import { loadConfigSchema } from '@backstage/config-loader'; import { + AzureDevOpsCredentialLike, AzureIntegrationConfig, readAzureIntegrationConfig, readAzureIntegrationConfigs, } from './config'; +type AzureIntegrationConfigLike = Partial< + Omit +> & { + credential?: Partial; + credentials?: Partial[]; +}; + describe('readAzureIntegrationConfig', () => { - function buildConfig(data: Partial): Config { + const valid: any = { + host: 'dev.azure.com', + }; + + function buildConfig(data: AzureIntegrationConfigLike): Config { return new ConfigReader(data); } async function buildFrontendConfig( - data: Partial, + data: AzureIntegrationConfigLike, ): Promise { const fullSchema = await loadConfigSchema({ dependencies: ['@backstage/integration'], @@ -51,39 +63,155 @@ describe('readAzureIntegrationConfig', () => { return new ConfigReader((processed[0].data as any).integrations.azure[0]); } - it('reads all values when using a token', () => { - const output = readAzureIntegrationConfig( - buildConfig({ - host: 'a.com', - token: 't', - }), - ); - - expect(output).toEqual({ - host: 'a.com', - token: 't', - }); - }); - - it('reads all values when using a credential', () => { + it('reads all values when using a personal access token credential', () => { const output = readAzureIntegrationConfig( buildConfig({ host: 'dev.azure.com', - credential: { - clientId: 'id', - clientSecret: 'secret', - tenantId: 'tenant', - }, + credentials: [ + { + organizations: ['org1'], + personalAccessToken: 't', + }, + ], }), ); expect(output).toEqual({ host: 'dev.azure.com', - credential: { - clientId: 'id', - clientSecret: 'secret', - tenantId: 'tenant', - }, + credentials: [ + { + kind: 'PersonalAccessToken', + organizations: ['org1'], + personalAccessToken: 't', + }, + ], + }); + }); + + it('reads all values when using a personal access token credential (without organizations)', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'a.com', + credentials: [ + { + personalAccessToken: 't', + }, + ], + }), + ); + + expect(output).toEqual({ + host: 'a.com', + credentials: [ + { + kind: 'PersonalAccessToken', + personalAccessToken: 't', + }, + ], + }); + }); + + it('reads all values when using a client secret credential', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }), + ); + + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'ClientSecret', + organizations: ['org1', 'org2'], + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }); + }); + + it('reads all values when using a client secret credential (without organizations)', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + credentials: [ + { + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }), + ); + + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }); + }); + + it('reads all values when using a managed identity credential', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + }, + ], + }), + ); + + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'ManagedIdentity', + organizations: ['org1', 'org2'], + clientId: 'id', + }, + ], + }); + }); + + it('reads all values when using a managed identity credential (without organizations)', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + credentials: [ + { + clientId: 'id', + }, + ], + }), + ); + + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'ManagedIdentity', + clientId: 'id', + }, + ], }); }); @@ -92,96 +220,296 @@ describe('readAzureIntegrationConfig', () => { expect(output).toEqual({ host: 'dev.azure.com' }); }); - it('rejects funky configs', () => { - const valid: any = { - host: 'dev.azure.com', - }; + it('maps deprecated token to credentials', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + token: 't', + }), + ); + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'PersonalAccessToken', + personalAccessToken: 't', + }, + ], + }); + }); + + it('maps deprecated credential to credentials', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + credential: { + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenantId', + }, + }), + ); + + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenantId', + }, + ], + }); + }); + + it('rejects config when host is not valid', () => { expect(() => readAzureIntegrationConfig(buildConfig({ ...valid, host: 7 })), ).toThrow(/host/); + }); - expect(() => - readAzureIntegrationConfig(buildConfig({ ...valid, token: 7 })), - ).toThrow(/token/); - - expect(() => - readAzureIntegrationConfig(buildConfig({ ...valid, credential: 7 })), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig( - buildConfig({ ...valid, credential: { clientId: 7 } }), - ), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig( - buildConfig({ ...valid, credential: { clientSecret: 7 } }), - ), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig( - buildConfig({ ...valid, credential: { tenantId: 7 } }), - ), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig(buildConfig({ ...valid, credential: {} })), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig( - buildConfig({ ...valid, credential: { clientSecret: 'secret' } }), - ), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig( - buildConfig({ ...valid, credential: { tenantId: 'tenant' } }), - ), - ).toThrow(/credential/); - + it('rejects config when organizations is not valid', () => { expect(() => readAzureIntegrationConfig( buildConfig({ ...valid, - credential: { clientId: 'id', clientSecret: 'secret' }, + credentials: [ + { + organizations: [1, 2, 'org'], + }, + ], }), ), - ).toThrow(/credential/); + ).toThrow(/credentials/); + }); + it('rejects config when token is not valid', () => { expect(() => readAzureIntegrationConfig( buildConfig({ ...valid, - credential: { clientId: 'id', tenantId: 'tenant' }, + credentials: [ + { + personalAccessToken: 7, + }, + ], }), ), - ).toThrow(/credential/); + ).toThrow(/credentials/); + }); + it('rejects config when clientId is not valid', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + clientId: 7, + }, + ], + }), + ), + ).toThrow(/credentials/); + }); + + it('rejects config when clientSecret is not valid', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + clientId: 'id', + clientSecret: 7, + tenantId: 'tenant', + }, + ], + }), + ), + ).toThrow(/credentials/); + }); + + it('rejects config when tenantId is not valid', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + clientId: 'id', + clientSecret: 'secret', + tenantId: 7, + }, + ], + }), + ), + ).toThrow(/credentials/); + }); + + it('rejects config when at least one credential not valid', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + personalAccessToken: 'token', + }, + { + clientId: 'id', + }, + { + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + { + clientId: 'id', + personalAccessToken: 'token', + }, + ], + }), + ), + ).toThrow(/not a valid credential/); + }); + + it('rejects config when using a client secret credential for Azure DevOps server', () => { expect(() => readAzureIntegrationConfig( buildConfig({ ...valid, host: 'a.com', + credentials: [ + { + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }), + ), + ).toThrow(/hosts/); + }); + + it('rejects config when using a managed identity for Azure DevOps server', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + host: 'a.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + }, + ], + }), + ), + ).toThrow(/personal access tokens/); + }); + + it('rejects config when using organizations for Azure DevOps server', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + host: 'a.com', + credentials: [ + { + clientId: 'id', + }, + ], + }), + ), + ).toThrow(/hosts/); + }); + + it('rejects config when both the credential and credentials field are specified', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, credential: { clientId: 'id', - tenantId: 'tenant', - clientSecret: 'secret', }, + credentials: [ + { + clientId: 'id', + }, + ], }), ), ).toThrow(/credential/); }); + it('rejects config when both the token and credentials field are specified', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + token: 'token', + credentials: [ + { + clientId: 'id', + }, + ], + }), + ), + ).toThrow(/token/); + }); + + it('rejects config when more than one credential does not specify an organization', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + clientId: 'id', + }, + { + organizations: [], + personalAccessToken: 'pat', + }, + ], + }), + ), + ).toThrow(/organizations/); + }); + + it('rejects config when multiple credentials specify the same organization', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + }, + { + organizations: ['org2', 'org3'], + personalAccessToken: 'pat', + }, + ], + }), + ), + ).toThrow(/organization org2/); + }); + it('works on the frontend', async () => { expect( readAzureIntegrationConfig( await buildFrontendConfig({ host: 'a.com', - token: 't', + credentials: [ + { + personalAccessToken: 't', + }, + ], }), ), ).toEqual({ @@ -191,46 +519,184 @@ describe('readAzureIntegrationConfig', () => { }); describe('readAzureIntegrationConfigs', () => { - function buildConfig(data: Partial[]): Config[] { + function buildConfig(data: AzureIntegrationConfigLike[]): Config[] { return data.map(item => new ConfigReader(item)); } - it('reads all values when using a token', () => { - const output = readAzureIntegrationConfigs( - buildConfig([ - { - host: 'a.com', - token: 't', - }, - ]), - ); - expect(output).toContainEqual({ - host: 'a.com', - token: 't', - }); - }); - - it('reads all values when using a credential', () => { + it('reads all values when using a personal access token credential', () => { const output = readAzureIntegrationConfigs( buildConfig([ { host: 'dev.azure.com', - credential: { + credentials: [ + { + organizations: ['org1'], + personalAccessToken: 't', + }, + ], + }, + ]), + ); + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'PersonalAccessToken', + organizations: ['org1'], + personalAccessToken: 't', + }, + ], + }, + ]); + }); + + it('reads all values when using a personal access token credential (without organizations)', () => { + const output = readAzureIntegrationConfigs( + buildConfig([ + { + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 't', + }, + ], + }, + ]), + ); + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'PersonalAccessToken', + personalAccessToken: 't', + }, + ], + }, + ]); + }); + + it('reads all values when using a client secret credential', () => { + const output = readAzureIntegrationConfigs( + buildConfig([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }, + ]), + ); + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'ClientSecret', + organizations: ['org1', 'org2'], clientId: 'id', clientSecret: 'secret', tenantId: 'tenant', }, + ], + }, + ]); + }); + + it('reads all values when using a client secret credential (without organizations)', () => { + const output = readAzureIntegrationConfigs( + buildConfig([ + { + host: 'dev.azure.com', + credentials: [ + { + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], }, ]), ); - expect(output).toContainEqual({ - host: 'dev.azure.com', - credential: { - clientId: 'id', - clientSecret: 'secret', - tenantId: 'tenant', + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], }, - }); + ]); + }); + + it('reads all values when using a managed identity credential', () => { + const output = readAzureIntegrationConfigs( + buildConfig([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + }, + ], + }, + ]), + ); + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'ManagedIdentity', + organizations: ['org1', 'org2'], + clientId: 'id', + }, + ], + }, + ]); + }); + + it('reads all values when using a managed identity credential (without organizations)', () => { + const output = readAzureIntegrationConfigs( + buildConfig([ + { + host: 'dev.azure.com', + credentials: [ + { + clientId: 'id', + }, + ], + }, + ]), + ); + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'ManagedIdentity', + clientId: 'id', + }, + ], + }, + ]); }); it('adds a default entry when missing', () => { diff --git a/packages/integration/src/azure/config.ts b/packages/integration/src/azure/config.ts index 3f12f3ef13..98704ba570 100644 --- a/packages/integration/src/azure/config.ts +++ b/packages/integration/src/azure/config.ts @@ -36,6 +36,8 @@ export type AzureIntegrationConfig = { * The authorization token to use for requests. * * If no token is specified, anonymous access is used. + * + * @deprecated Use `credentials` instead. */ token?: string; @@ -43,15 +45,50 @@ export type AzureIntegrationConfig = { * The credential to use for requests. * * If no credential is specified anonymous access is used. + * + * @deprecated Use `credentials` instead. */ - credential?: AzureCredential; + credential?: AzureDevOpsCredential; + + /** + * The credentials to use for requests. If multiple credentials are specified the first one that matches the organization is used. + * If not organization matches the first credential without an organization is used. + * + * If no credentials are specified at all, either a default credential (for Azure DevOps) or anonymous access (for Azure DevOps Server) is used. + */ + credentials?: AzureDevOpsCredential[]; }; /** - * Authenticate using a client secret that was generated for an App Registration. + * The kind of Azure DevOps credential. * @public */ -export type AzureClientSecretCredential = { +export type AzureDevOpsCredentialKind = + | 'PersonalAccessToken' + | 'ClientSecret' + | 'ManagedIdentity'; + +/** + * Common fields for the Azure DevOps credentials. + * @public + */ +export type AzureCredentialBase = { + /** + * The kind of credential. + */ + kind: AzureDevOpsCredentialKind; + /** + * The Azure DevOps organizations for which to use this credential. + */ + organizations?: string[]; +}; + +/** + * A client secret credential that was generated for an App Registration. + * @public + */ +export type AzureClientSecretCredential = AzureCredentialBase & { + kind: 'ClientSecret'; /** * The Azure Active Directory tenant */ @@ -68,10 +105,11 @@ export type AzureClientSecretCredential = { }; /** - * Authenticate using a managed identity available at the deployment environment. + * A managed identity credential. * @public */ -export type AzureManagedIdentityCredential = { +export type AzureManagedIdentityCredential = AzureCredentialBase & { + kind: 'ManagedIdentity'; /** * The clientId */ @@ -79,33 +117,77 @@ export type AzureManagedIdentityCredential = { }; /** - * Credential used to authenticate to Azure Active Directory. + * A personal access token credential. * @public */ -export type AzureCredential = +export type PersonalAccessTokenCredential = AzureCredentialBase & { + kind: 'PersonalAccessToken'; + personalAccessToken: string; +}; + +/** + * The general shape of a credential that can be used to authenticate to Azure DevOps. + * @public + */ +export type AzureDevOpsCredentialLike = Omit< + Partial & + Partial & + Partial, + 'kind' +>; + +/** + * Credential used to authenticate to Azure DevOps. + * @public + */ +export type AzureDevOpsCredential = | AzureClientSecretCredential - | AzureManagedIdentityCredential; -export const isAzureClientSecretCredential = ( - credential: Partial, -): credential is AzureClientSecretCredential => { - const clientSecretCredential = credential as AzureClientSecretCredential; + | AzureManagedIdentityCredential + | PersonalAccessTokenCredential; - return ( - Object.keys(credential).length === 3 && - clientSecretCredential.clientId !== undefined && - clientSecretCredential.clientSecret !== undefined && - clientSecretCredential.tenantId !== undefined - ); -}; +const AzureDevOpsCredentialFields = [ + 'clientId', + 'clientSecret', + 'tenantId', + 'personalAccessToken', +] as const; +type AzureDevOpsCredentialField = (typeof AzureDevOpsCredentialFields)[number]; -export const isAzureManagedIdentityCredential = ( - credential: Partial, -): credential is AzureManagedIdentityCredential => { - return ( - Object.keys(credential).length === 1 && - (credential as AzureManagedIdentityCredential).clientId !== undefined - ); -}; +const AzureDevopsCredentialFieldMap = new Map< + AzureDevOpsCredentialKind, + AzureDevOpsCredentialField[] +>([ + ['ClientSecret', ['clientId', 'clientSecret', 'tenantId']], + ['ManagedIdentity', ['clientId']], + ['PersonalAccessToken', ['personalAccessToken']], +]); + +function asAzureDevOpsCredential( + credential: AzureDevOpsCredentialLike, +): AzureDevOpsCredential { + for (const entry of AzureDevopsCredentialFieldMap.entries()) { + const [kind, requiredFields] = entry; + + const forbiddenFields = AzureDevOpsCredentialFields.filter( + field => !requiredFields.includes(field as AzureDevOpsCredentialField), + ); + + if ( + requiredFields.every(field => credential[field] !== undefined) && + forbiddenFields.every(field => credential[field] === undefined) + ) { + return { + kind, + organizations: credential.organizations, + ...requiredFields.reduce((acc, field) => { + acc[field] = credential[field]; + return acc; + }, {} as Record), + } as AzureDevOpsCredential; + } + } + throw new Error('is not a valid credential'); +} /** * Reads a single Azure integration config. @@ -117,15 +199,62 @@ export function readAzureIntegrationConfig( config: Config, ): AzureIntegrationConfig { const host = config.getOptionalString('host') ?? AZURE_HOST; + + let credentialConfigs = config + .getOptionalConfigArray('credentials') + ?.map(credential => { + const result: Partial = { + organizations: credential.getOptionalStringArray('organizations'), + personalAccessToken: credential.getOptionalString( + 'personalAccessToken', + ), + tenantId: credential.getOptionalString('tenantId'), + clientId: credential.getOptionalString('clientId'), + clientSecret: credential.getOptionalString('clientSecret'), + }; + + return result; + }); + const token = config.getOptionalString('token'); - const credential = config.getOptional('credential') - ? { + if ( + config.getOptional('credential') !== undefined && + config.getOptional('credentials') !== undefined + ) { + throw new Error( + `Invalid Azure integration config, 'credential' and 'credentials' cannot be used together. Use 'credentials' instead.`, + ); + } + + if ( + config.getOptional('token') !== undefined && + config.getOptional('credentials') !== undefined + ) { + throw new Error( + `Invalid Azure integration config, 'token' and 'credentials' cannot be used together. Use 'credentials' instead.`, + ); + } + + if (token !== undefined) { + const mapped = [{ personalAccessToken: token }]; + credentialConfigs = credentialConfigs?.concat(mapped) ?? mapped; + } + + if (config.getOptional('credential') !== undefined) { + const mapped = [ + { + organizations: config.getOptionalStringArray( + 'credential.organizations', + ), + token: config.getOptionalString('credential.token'), tenantId: config.getOptionalString('credential.tenantId'), clientId: config.getOptionalString('credential.clientId'), clientSecret: config.getOptionalString('credential.clientSecret'), - } - : undefined; + }, + ]; + credentialConfigs = credentialConfigs?.concat(mapped) ?? mapped; + } if (!isValidHost(host)) { throw new Error( @@ -133,29 +262,92 @@ export function readAzureIntegrationConfig( ); } - if ( - credential && - !isAzureClientSecretCredential(credential) && - !isAzureManagedIdentityCredential(credential) - ) { - throw new Error( - `Invalid Azure integration config, credential is not valid`, + let credentials: AzureDevOpsCredential[] | undefined = undefined; + if (credentialConfigs !== undefined) { + const errors = credentialConfigs + ?.reduce((acc, credentialConfig, index) => { + let error: string | undefined = undefined; + try { + asAzureDevOpsCredential(credentialConfig); + } catch (e) { + error = e.message; + } + + if (error !== undefined) { + acc.push(`credential at position ${index + 1} ${error}`); + } + + return acc; + }, Array.of()) + .concat( + Object.entries( + credentialConfigs + .filter( + credential => + credential.organizations !== undefined && + credential.organizations.length > 0, + ) + .reduce((acc, credential, index) => { + credential.organizations?.forEach(organization => { + if (!acc[organization]) { + acc[organization] = []; + } + + acc[organization].push(index + 1); + }); + + return acc; + }, {} as Record), + ) + .filter(([_, indexes]) => indexes.length > 1) + .reduce((acc, [org, indexes]) => { + acc.push( + `organization ${org} is specified multiple times in credentials at positions ${indexes + .slice(0, indexes.length - 1) + .join(', ')} and ${indexes[indexes.length - 1]}`, + ); + return acc; + }, Array.of()), + ); + + if (errors?.length > 0) { + throw new Error( + `Invalid Azure integration config for ${host}: ${errors.join('; ')}`, + ); + } + + credentials = credentialConfigs.map(credentialConfig => + asAzureDevOpsCredential(credentialConfig), ); + + if ( + credentials.some( + credential => credential.kind !== 'PersonalAccessToken', + ) && + host !== AZURE_HOST + ) { + throw new Error( + `Invalid Azure integration config for ${host}, only personal access tokens can be used with hosts other than ${AZURE_HOST}`, + ); + } + + if ( + credentials.filter( + credential => + credential.organizations === undefined || + credential.organizations.length === 0, + ).length > 1 + ) { + throw new Error( + `Invalid Azure integration config for ${host}, you cannot specify multiple credentials without organizations`, + ); + } } - if (credential && host !== AZURE_HOST) { - throw new Error( - `Invalid Azure integration config, credential can only be used with ${AZURE_HOST}`, - ); - } - - if (credential && token) { - throw new Error( - `Invalid Azure integration config, specify either a token or a credential but not both`, - ); - } - - return { host, token, credential }; + return { + host, + credentials, + }; } /** diff --git a/packages/integration/src/azure/core.test.ts b/packages/integration/src/azure/core.test.ts index e6ef5401ac..601057e499 100644 --- a/packages/integration/src/azure/core.test.ts +++ b/packages/integration/src/azure/core.test.ts @@ -14,11 +14,7 @@ * limitations under the License. */ -import { - getAzureFileFetchUrl, - getAzureDownloadUrl, - getAzureRequestOptions, -} from './core'; +import { getAzureFileFetchUrl, getAzureDownloadUrl } from './core'; import { AccessToken, ClientSecretCredential, @@ -44,66 +40,6 @@ MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() => ); describe('azure core', () => { - describe('getAzureRequestOptions', () => { - it('should not add authorization header when not using token or credential', async () => { - expect(await getAzureRequestOptions({ host: '' })).toEqual( - expect.objectContaining({ - headers: expect.not.objectContaining({ - Authorization: expect.anything(), - }), - }), - ); - }); - - it('should add authorization header when using a personal access token', async () => { - expect( - await getAzureRequestOptions({ host: '', token: '0123456789' }), - ).toEqual( - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: 'Basic OjAxMjM0NTY3ODk=', - }), - }), - ); - }); - - it('should add authorization header when using a client secret', async () => { - expect( - await getAzureRequestOptions({ - host: '', - credential: { - clientId: 'fake-id', - clientSecret: 'fake-secret', - tenantId: 'fake-tenant', - }, - }), - ).toEqual( - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: 'Bearer fake-client-secret-token', - }), - }), - ); - }); - - it('should add authorization header when using a managed identity', async () => { - expect( - await getAzureRequestOptions({ - host: '', - credential: { - clientId: 'fake-id', - }, - }), - ).toEqual( - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: 'Bearer fake-managed-identity-token', - }), - }), - ); - }); - }); - describe('getAzureFileFetchUrl', () => { it.each([ { diff --git a/packages/integration/src/azure/core.ts b/packages/integration/src/azure/core.ts index 31aa13d91f..1c25373962 100644 --- a/packages/integration/src/azure/core.ts +++ b/packages/integration/src/azure/core.ts @@ -15,15 +15,6 @@ */ import { AzureUrl } from './AzureUrl'; -import { - AzureIntegrationConfig, - isAzureManagedIdentityCredential, - isAzureClientSecretCredential, -} from './config'; -import { - ClientSecretCredential, - ManagedIdentityCredential, -} from '@azure/identity'; /** * Given a URL pointing to a file on a provider, returns a URL that is suitable @@ -62,45 +53,3 @@ export function getAzureDownloadUrl(url: string): string { export function getAzureCommitsUrl(url: string): string { return AzureUrl.fromRepoUrl(url).toCommitsUrl(); } - -/** - * Gets the request options necessary to make requests to a given provider. - * - * @param config - The relevant provider config - * @param additionalHeaders - Additional headers for the request - * @public - */ -export async function getAzureRequestOptions( - config: AzureIntegrationConfig, - additionalHeaders?: Record, -): Promise<{ headers: Record }> { - const azureDevOpsScope = '499b84ac-1321-427f-aa17-267ca6975798/.default'; - const headers: Record = additionalHeaders - ? { ...additionalHeaders } - : {}; - - const { token, credential } = config; - if (credential) { - if (isAzureClientSecretCredential(credential)) { - const servicePrincipal = new ClientSecretCredential( - credential.tenantId, - credential.clientId, - credential.clientSecret, - ); - - const accessToken = await servicePrincipal.getToken(azureDevOpsScope); - headers.Authorization = `Bearer ${accessToken.token}`; - } else if (isAzureManagedIdentityCredential(credential)) { - const managedIdentity = new ManagedIdentityCredential( - credential.clientId, - ); - const accessToken = await managedIdentity.getToken(azureDevOpsScope); - headers.Authorization = `Bearer ${accessToken.token}`; - } - } else if (token) { - const buffer = Buffer.from(`:${config.token}`, 'utf8'); - headers.Authorization = `Basic ${buffer.toString('base64')}`; - } - - return { headers }; -} diff --git a/packages/integration/src/azure/deprecated.test.ts b/packages/integration/src/azure/deprecated.test.ts new file mode 100644 index 0000000000..4411ef5f2f --- /dev/null +++ b/packages/integration/src/azure/deprecated.test.ts @@ -0,0 +1,128 @@ +/* + * 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 { getAzureRequestOptions } from './deprecated'; +import { DateTime } from 'luxon'; +import { + AccessToken, + ClientSecretCredential, + ManagedIdentityCredential, +} from '@azure/identity'; + +jest.mock('@azure/identity'); + +const MockedClientSecretCredential = ClientSecretCredential as jest.MockedClass< + typeof ClientSecretCredential +>; + +const MockedManagedIdentityCredential = + ManagedIdentityCredential as jest.MockedClass< + typeof ManagedIdentityCredential + >; + +describe('azure core', () => { + beforeEach(() => { + jest.resetAllMocks(); + + MockedClientSecretCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), + token: 'fake-client-secret-token', + } as AccessToken), + ); + MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), + token: 'fake-managed-identity-token', + } as AccessToken), + ); + }); + + describe('getAzureRequestOptions', () => { + it('should not add authorization header when not using token or credential', async () => { + expect(await getAzureRequestOptions({ host: '' })).toEqual( + expect.objectContaining({ + headers: expect.not.objectContaining({ + Authorization: expect.anything(), + }), + }), + ); + }); + + it('should add authorization header when using a personal access token', async () => { + const pat = '0123456789'; + const encoded = Buffer.from(`:${pat}`).toString('base64'); + expect( + await getAzureRequestOptions({ + host: '', + credentials: [ + { + kind: 'PersonalAccessToken', + personalAccessToken: pat, + }, + ], + }), + ).toEqual( + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Basic ${encoded}`, + }), + }), + ); + }); + + it('should add authorization header when using a client secret', async () => { + expect( + await getAzureRequestOptions({ + host: '', + credentials: [ + { + kind: 'ClientSecret', + clientId: 'fake-id', + clientSecret: 'fake-secret', + tenantId: 'fake-tenant', + }, + ], + }), + ).toEqual( + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer fake-client-secret-token', + }), + }), + ); + }); + + it('should add authorization header when using a managed identity', async () => { + expect( + await getAzureRequestOptions({ + host: '', + credentials: [ + { + kind: 'ManagedIdentity', + clientId: 'fake-id', + }, + ], + }), + ).toEqual( + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer fake-managed-identity-token', + }), + }), + ); + }); + }); +}); diff --git a/packages/integration/src/azure/deprecated.ts b/packages/integration/src/azure/deprecated.ts new file mode 100644 index 0000000000..fd19f3dc14 --- /dev/null +++ b/packages/integration/src/azure/deprecated.ts @@ -0,0 +1,61 @@ +/* + * 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 { AzureIntegrationConfig } from './config'; +import { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider'; + +/** + * Gets the request options necessary to make requests to a given provider. + * + * @param config - The relevant provider config + * @param additionalHeaders - Additional headers for the request + * @public + * @deprecated Use {@link AzureDevOpsCredentialsProvider} instead. + */ +export async function getAzureRequestOptions( + config: AzureIntegrationConfig, + additionalHeaders?: Record, +): Promise<{ headers: Record }> { + const headers: Record = additionalHeaders + ? { ...additionalHeaders } + : {}; + + /* + * Since we do not have a way to determine which organization the request is for, + * we will use the first credential that does not have an organization specified. + */ + const credentialConfig = config.credentials?.filter( + credential => + credential.organizations === undefined || + credential.organizations.length === 0, + )[0]; + + if (credentialConfig) { + const credentialsProvider = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential( + credentialConfig, + ); + const credentials = await credentialsProvider.getCredentials(); + + return { + headers: { + ...credentials?.headers, + ...headers, + }, + }; + } + + return { headers }; +} diff --git a/packages/integration/src/azure/index.ts b/packages/integration/src/azure/index.ts index 21cf069807..b5f5c0a316 100644 --- a/packages/integration/src/azure/index.ts +++ b/packages/integration/src/azure/index.ts @@ -21,13 +21,21 @@ export { } from './config'; export type { AzureIntegrationConfig, - AzureCredential, - AzureManagedIdentityCredential, + AzureDevOpsCredentialKind, + AzureCredentialBase, AzureClientSecretCredential, + AzureManagedIdentityCredential, + PersonalAccessTokenCredential, + AzureDevOpsCredentialLike, + AzureDevOpsCredential, } from './config'; export { getAzureCommitsUrl, getAzureDownloadUrl, getAzureFileFetchUrl, - getAzureRequestOptions, } from './core'; + +export * from './types'; +export { DefaultAzureDevOpsCredentialsProvider } from './DefaultAzureDevOpsCredentialsProvider'; + +export * from './deprecated'; diff --git a/packages/integration/src/azure/types.ts b/packages/integration/src/azure/types.ts new file mode 100644 index 0000000000..72b341432c --- /dev/null +++ b/packages/integration/src/azure/types.ts @@ -0,0 +1,44 @@ +/* + * 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. + */ + +/** + * The type of Azure DevOps credential, either bearer or pat. + * @public + */ +export type AzureDevOpsCredentialType = 'bearer' | 'pat'; + +/** + * A set of credentials for Azure DevOps. + * + * @public + */ +export type AzureDevOpsCredentials = { + headers: { [name: string]: string }; + token: string; + type: AzureDevOpsCredentialType; +}; + +/** + * This allows implementations to be provided to retrieve Azure DevOps credentials. + * + * @public + * + */ +export interface AzureDevOpsCredentialsProvider { + getCredentials(opts: { + url: string; + }): Promise; +} diff --git a/packages/integration/src/gerrit/core.test.ts b/packages/integration/src/gerrit/core.test.ts index 5a829e88f9..5fbb41763a 100644 --- a/packages/integration/src/gerrit/core.test.ts +++ b/packages/integration/src/gerrit/core.test.ts @@ -20,6 +20,7 @@ import fetch from 'cross-fetch'; import { setupRequestMockHandlers } from '../helpers'; import { GerritIntegrationConfig } from './config'; import { + buildGerritGitilesArchiveUrl, buildGerritGitilesUrl, getGerritBranchApiUrl, getGerritCloneRepoUrl, @@ -33,6 +34,86 @@ describe('gerrit core', () => { const worker = setupServer(); setupRequestMockHandlers(worker); + describe('buildGerritGitilesArchiveUrl', () => { + const config: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com', + gitilesBaseUrl: 'https://gerrit.com/gitiles', + }; + const configWithPath: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com/gerrit', + gitilesBaseUrl: 'https://gerrit.com/gerrit/plugins/gitiles', + }; + const configWithDedicatedGitiles: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com/gerrit', + gitilesBaseUrl: 'https://dedicated-gitiles-server.com/gerrit/gitiles', + }; + it('can create an archive url for a branch', () => { + expect(buildGerritGitilesArchiveUrl(config, 'repo', 'dev', '')).toEqual( + 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz', + ); + + expect(buildGerritGitilesArchiveUrl(config, 'repo', 'dev', '/')).toEqual( + 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz', + ); + }); + it('can create an archive url for a specific directory', () => { + expect( + buildGerritGitilesArchiveUrl(config, 'repo', 'dev', 'docs'), + ).toEqual( + 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', + ); + }); + it('can create an authenticated url when auth is enabled', () => { + const authConfig = { + ...config, + username: 'username', + password: 'password', + }; + expect( + buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'), + ).toEqual( + 'https://gerrit.com/a/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', + ); + }); + it('can create an authenticated url when auth is enabled and an url-path is used', () => { + const authConfig = { + ...configWithPath, + username: 'username', + password: 'password', + }; + expect( + buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'), + ).toEqual( + 'https://gerrit.com/gerrit/a/plugins/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', + ); + }); + it('Cannot build an authenticated url when a dedicated Gitiles server is used', () => { + const authConfig = { + ...configWithDedicatedGitiles, + username: 'username', + password: 'password', + }; + expect(() => + buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'), + ).toThrow( + 'Since the baseUrl (Gerrit) is not part of the gitilesBaseUrl, an authentication URL could not be constructed.', + ); + }); + it('Build a non-authenticated url when a dedicated Gitiles server is used', () => { + const authConfig = { + ...configWithDedicatedGitiles, + }; + expect( + buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'), + ).toEqual( + 'https://dedicated-gitiles-server.com/gerrit/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', + ); + }); + }); + describe('buildGerritGitilesUrl', () => { it('can create an url from arguments', () => { const config: GerritIntegrationConfig = { @@ -86,6 +167,25 @@ describe('gerrit core', () => { ); expect(rootPath).toEqual('/'); }); + it('can parse a valid authenticated gitiles url.', () => { + const config: GerritIntegrationConfig = { + host: 'gerrit.com', + gitilesBaseUrl: 'https://gerrit.com/gitiles', + }; + const { branch, filePath, project } = parseGerritGitilesUrl( + config, + 'https://gerrit.com/a/gitiles/web/project/+/refs/heads/master/README.md', + ); + expect(project).toEqual('web/project'); + expect(branch).toEqual('master'); + expect(filePath).toEqual('README.md'); + + const { filePath: rootPath } = parseGerritGitilesUrl( + config, + 'https://gerrit.com/gitiles/web/project/+/refs/heads/master', + ); + expect(rootPath).toEqual('/'); + }); it('throws on incorrect gitiles urls.', () => { const config: GerritIntegrationConfig = { host: 'gerrit.com', diff --git a/packages/integration/src/gerrit/core.ts b/packages/integration/src/gerrit/core.ts index 0403d8fc75..b3bdbc30e4 100644 --- a/packages/integration/src/gerrit/core.ts +++ b/packages/integration/src/gerrit/core.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { trimStart } from 'lodash'; import { GerritIntegrationConfig } from './config'; @@ -37,6 +36,7 @@ const GERRIT_BODY_PREFIX = ")]}'"; * * Gitiles url: * https://g.com/optional_path/\{project\}/+/refs/heads/\{branch\}/\{filePath\} + * https://g.com/a/optional_path/\{project\}/+/refs/heads/\{branch\}/\{filePath\} * * * @param url - An URL pointing to a file stored in git. @@ -47,7 +47,17 @@ export function parseGerritGitilesUrl( config: GerritIntegrationConfig, url: string, ): { branch: string; filePath: string; project: string } { - const urlPath = url.replace(config.gitilesBaseUrl!, ''); + const baseUrlParse = new URL(config.gitilesBaseUrl!); + const urlParse = new URL(url); + + // Remove the gerrit authentication prefix '/a/' from the url + // In case of the gitilesBaseUrl is https://review.gerrit.com/plugins/gitiles + // and the url provided is https://review.gerrit.com/a/plugins/gitiles/... + // remove the prefix only if the pathname start with '/a/' + const urlPath = urlParse.pathname + .substring(urlParse.pathname.startsWith('/a/') ? 2 : 0) + .replace(baseUrlParse.pathname, ''); + const parts = urlPath.split('/').filter(p => !!p); const projectEndIndex = parts.indexOf('+'); @@ -91,6 +101,28 @@ export function buildGerritGitilesUrl( }/${project}/+/refs/heads/${branch}/${trimStart(filePath, '/')}`; } +/** + * Build a Gerrit Gitiles archive url that targets a specific branch and path + * + * @param config - A Gerrit provider config. + * @param project - The name of the git project + * @param branch - The branch we will target. + * @param filePath - The absolute file path. + * @public + */ +export function buildGerritGitilesArchiveUrl( + config: GerritIntegrationConfig, + project: string, + branch: string, + filePath: string, +): string { + const archiveName = + filePath === '/' || filePath === '' ? '.tar.gz' : `/${filePath}.tar.gz`; + return `${getGitilesAuthenticationUrl( + config, + )}/${project}/+archive/refs/heads/${branch}${archiveName}`; +} + /** * Return the authentication prefix. * @@ -109,6 +141,39 @@ export function getAuthenticationPrefix( return config.password ? '/a/' : '/'; } +/** + * Return the authentication gitiles url. + * + * @remarks + * + * To authenticate with a password the API url must be prefixed with "/a/". + * If no password is set anonymous access (without the prefix) will + * be used. + * + * @param config - A Gerrit provider config. + */ +export function getGitilesAuthenticationUrl( + config: GerritIntegrationConfig, +): string { + if (!config.baseUrl || !config.gitilesBaseUrl) { + throw new Error( + 'Unexpected Gerrit config values. baseUrl or gitilesBaseUrl not set.', + ); + } + if (config.gitilesBaseUrl.startsWith(config.baseUrl)) { + return config.gitilesBaseUrl.replace( + config.baseUrl.concat('/'), + config.baseUrl.concat(getAuthenticationPrefix(config)), + ); + } + if (config.password) { + throw new Error( + 'Since the baseUrl (Gerrit) is not part of the gitilesBaseUrl, an authentication URL could not be constructed.', + ); + } + return config.gitilesBaseUrl!; +} + /** * Return the url to get branch info from the Gerrit API. * diff --git a/packages/integration/src/gerrit/index.ts b/packages/integration/src/gerrit/index.ts index 127c949c4e..6c6295f7a3 100644 --- a/packages/integration/src/gerrit/index.ts +++ b/packages/integration/src/gerrit/index.ts @@ -19,6 +19,7 @@ export { readGerritIntegrationConfigs, } from './config'; export { + buildGerritGitilesArchiveUrl, getGerritBranchApiUrl, getGerritCloneRepoUrl, getGerritFileContentsApiUrl, diff --git a/packages/release-manifests/CHANGELOG.md b/packages/release-manifests/CHANGELOG.md index f59447666f..024aa18386 100644 --- a/packages/release-manifests/CHANGELOG.md +++ b/packages/release-manifests/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/release-manifests +## 0.0.10-next.0 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency + ## 0.0.9 ### Patch Changes diff --git a/packages/release-manifests/catalog-info.yaml b/packages/release-manifests/catalog-info.yaml new file mode 100644 index 0000000000..4564145a3d --- /dev/null +++ b/packages/release-manifests/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-release-manifests + title: '@backstage/release-manifests' + description: Helper library for receiving release manifests +spec: + lifecycle: experimental + type: backstage-common-library + owner: maintainers diff --git a/packages/release-manifests/package.json b/packages/release-manifests/package.json index 0c6ad64446..e5169bc681 100644 --- a/packages/release-manifests/package.json +++ b/packages/release-manifests/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/release-manifests", "description": "Helper library for receiving release manifests", - "version": "0.0.9", + "version": "0.0.10-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -23,6 +23,7 @@ "backstage" ], "license": "Apache-2.0", + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", @@ -37,7 +38,6 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/test-utils": "workspace:^", - "@types/node": "^16.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index 763c9f1cc1..5fff69f612 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/repo-tools +## 0.3.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + +## 0.3.4-next.1 + +### Patch Changes + +- 6f874cdb04eb: Fixed a bug with the `generate-catalog-info` command that could cause `metadata.description` values to be overwritten by `package.json` description values only because unrelated attributes were being changed. +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + +## 0.3.4-next.0 + +### Patch Changes + +- 0109d3f11159: The `generate-catalog-info` command now uses the first listed `CODEOWNER` as Component owner when initially + creating the `catalog-info.yaml` file. It continues to allow any one listed `CODEOWNER` when updating + entity metadata. +- ec13d3e86028: Fixed a bug with the `generate-catalog-info` command that could cause the `--dry-run` flag to indicate changes to files when no changes would actually be made if the command were run without the flag. +- db60a16e0a54: Added a `--ci` flag to the `generate-catalog-info` command. This flag behaves similarly to the same flag on `api-reports`: if `catalog-info.yaml` files would have been added or modified, then the process exits with status code `1`, and instructions are printed. +- Updated dependencies + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/cli-node@0.1.3 + - @backstage/errors@1.2.1 + +## 0.3.3 + +### Patch Changes + +- 75702e85862a: Bumped `@microsoft/api-extractor` dependency to `^7.36.4`, and `@microsoft/api-documenter` to `^7.22.33`. +- 1f3337ebc707: Introducing a new, experimental command `backstage-repo-tools generate-catalog-info`, which can be used to create standardized `catalog-info.yaml` files for each Backstage package in a Backstage monorepo. It can also be used to automatically fix existing `catalog-info.yaml` files with the correct metadata (including `metadata.name`, `metadata.title`, and `metadata.description` introspected from the package's `package.json`, as well as `spec.owner` introspected from `CODEOWNERS`), e.g. in a post-commit hook. +- ebeb77586975: Update `schema openapi generate` command to now create a default router that can be imported and used directly. +- Updated dependencies + - @backstage/cli-node@0.1.3 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + ## 0.3.3-next.1 ### Patch Changes diff --git a/packages/repo-tools/catalog-info.yaml b/packages/repo-tools/catalog-info.yaml new file mode 100644 index 0000000000..66b5ecc64b --- /dev/null +++ b/packages/repo-tools/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-repo-tools + title: '@backstage/repo-tools' + description: 'CLI for Backstage repo tooling ' +spec: + lifecycle: experimental + type: backstage-cli + owner: maintainers diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md index a0df5e721c..12d71f3be4 100644 --- a/packages/repo-tools/cli-report.md +++ b/packages/repo-tools/cli-report.md @@ -14,6 +14,7 @@ Options: Commands: api-reports [options] [paths...] type-deps + generate-catalog-info [options] schema [command] help [command] ``` @@ -36,6 +37,17 @@ Options: -h, --help ``` +### `backstage-repo-tools generate-catalog-info` + +``` +Usage: backstage-repo-tools generate-catalog-info [options] + +Options: + --dry-run + --ci + -h, --help +``` + ### `backstage-repo-tools schema` ``` diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index c5d9e266c9..49cb6578f3 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.3.3-next.1", + "version": "0.3.4-next.2", "publishConfig": { "access": "public" }, @@ -32,12 +32,13 @@ "dependencies": { "@apidevtools/swagger-parser": "^10.1.0", "@apisyouwonthate/style-guide": "^1.4.0", + "@backstage/catalog-model": "workspace:^", "@backstage/cli-common": "workspace:^", "@backstage/cli-node": "workspace:^", "@backstage/errors": "workspace:^", "@manypkg/get-packages": "^1.1.3", - "@microsoft/api-documenter": "^7.19.27", - "@microsoft/api-extractor": "^7.33.7", + "@microsoft/api-documenter": "^7.22.33", + "@microsoft/api-extractor": "^7.36.4", "@stoplight/spectral-core": "^1.18.0", "@stoplight/spectral-formatters": "^1.1.0", "@stoplight/spectral-functions": "^1.7.2", @@ -46,6 +47,7 @@ "@stoplight/spectral-runtime": "^1.1.2", "@stoplight/types": "^13.14.0", "chalk": "^4.0.0", + "codeowners-utils": "^1.0.2", "commander": "^9.1.0", "fs-extra": "10.1.0", "glob": "^8.0.3", @@ -54,7 +56,8 @@ "lodash": "^4.17.21", "minimatch": "^5.1.1", "p-limit": "^3.0.2", - "ts-node": "^10.0.0" + "ts-node": "^10.0.0", + "yaml-diff-patch": "^2.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/repo-tools/src/commands/generate-catalog-info/codeowners.ts b/packages/repo-tools/src/commands/generate-catalog-info/codeowners.ts new file mode 100644 index 0000000000..25703e4fc2 --- /dev/null +++ b/packages/repo-tools/src/commands/generate-catalog-info/codeowners.ts @@ -0,0 +1,69 @@ +/* + * 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 { + CodeOwnersEntry, + CODEOWNERS_PATHS, + matchFile as matchCodeowner, + parse as parseCodeowners, +} from 'codeowners-utils'; +import { relative as relativePath, resolve as resolvePath } from 'path'; +import { readFile } from './utils'; + +export async function loadCodeowners(): Promise { + const maybeFiles = await Promise.allSettled( + CODEOWNERS_PATHS.map(async path => + readFile(resolvePath('.', path), { encoding: 'utf-8' }), + ), + ); + const file = maybeFiles.find( + maybeFile => maybeFile.status === 'fulfilled', + ) as PromiseFulfilledResult | undefined; + + if (!file) { + throw new Error( + 'This utility expects a CODEOWNERS file, but no such file was found.', + ); + } + + return parseCodeowners(file.value); +} + +export function getPossibleCodeowners( + codeowners: CodeOwnersEntry[], + relPath: string, +): string[] { + const codeownerMaybe = matchCodeowner(relPath, codeowners); + return codeownerMaybe + ? codeownerMaybe.owners.map( + owner => (owner.match(/(?:\@[^\/]+\/)?([^\@\/]*)$/) || [])[1], + ) + : []; +} + +export function getOwnerFromCodeowners( + codeowners: CodeOwnersEntry[], + absPath: string, +): string { + const relPath = relativePath('.', absPath); + const possibleOwners = getPossibleCodeowners(codeowners, relPath); + const owner = possibleOwners[0]; + + if (!owner) { + throw new Error(`${relPath} isn't owned by anyone in CODEOWNERS`); + } + + return owner; +} diff --git a/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts b/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts new file mode 100644 index 0000000000..b8a473c67b --- /dev/null +++ b/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts @@ -0,0 +1,267 @@ +/* + * 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 YAML from 'js-yaml'; +import pLimit from 'p-limit'; +import { relative as relativePath, resolve as resolvePath } from 'path'; +import { yamlOverwrite } from 'yaml-diff-patch'; +import chalk from 'chalk'; +import { PackageGraph, PackageRole } from '@backstage/cli-node'; +import { Entity } from '@backstage/catalog-model'; +import { + getOwnerFromCodeowners, + getPossibleCodeowners, + loadCodeowners, +} from './codeowners'; +import { + BackstagePackageJson, + isBackstagePackage, + isRejected, + isFulfilled, + readFile, + writeFile, +} from './utils'; +import { CodeOwnersEntry } from 'codeowners-utils'; + +type CreateFixPackageInfoYamlsOptions = { + ci?: boolean; + dryRun?: boolean; +}; + +export default async (opts: CreateFixPackageInfoYamlsOptions) => { + const { dryRun = false, ci = false } = opts; + const packages = await PackageGraph.listTargetPackages(); + const codeowners = await loadCodeowners(); + const limit = pLimit(10); + + // If --ci is passed, don't make any changes to the file system; we're only + // interested in knowing if changes would have been made. + const isDryRun = ci ? true : dryRun; + const checkForChanges = ci; + + const results = await Promise.allSettled( + packages.map(({ packageJson, dir }) => + limit(async () => { + if (!isBackstagePackage(packageJson)) { + return ''; + } + + // Check if there is already a corresponding catalog-info.yaml + const infoYamlPath = resolvePath(dir, 'catalog-info.yaml'); + let yamlString = ''; + + try { + yamlString = await readFile(infoYamlPath, { encoding: 'utf-8' }); + } catch (e) { + if (e.code === 'ENOENT') { + return await createCatalogInfoYaml({ + yamlPath: infoYamlPath, + packageJson, + codeowners, + dryRun: isDryRun, + }); + } + + throw e; + } + + return await fixCatalogInfoYaml({ + yamlPath: infoYamlPath, + packageJson, + codeowners, + yamlString, + dryRun: isDryRun, + ci, + }); + }), + ), + ); + + const rejects = results.filter(isRejected); + if (rejects.length > 0) { + // Problems encountered. Print details here. + console.error( + chalk.red('Unable to create or fix catalog-info.yaml files\n'), + ); + rejects.forEach(reject => console.error(` ${reject.reason}`)); + console.error(); + process.exit(1); + } + + if (checkForChanges) { + const instructions = results + .filter(isFulfilled) + .map(r => r.value) + .filter(r => r !== ''); + + // Non-empty instructions indicate changes would have been made. + if (instructions.length > 0) { + console.error( + '\ncatalog-info.yaml file(s) out of sync with CODEOWNERS and/or package.json (see instructions above)\n', + ); + process.exit(1); + } else { + console.error( + 'catalog-info.yaml file(s) in sync with CODEOWNERS and package.json', + ); + } + } +}; + +type CreateOptions = { + yamlPath: string; + packageJson: BackstagePackageJson; + codeowners: CodeOwnersEntry[]; + dryRun: boolean; +}; + +type FixOptions = CreateOptions & { + yamlString: string; + ci: boolean; +}; + +type BackstagePackageEntity = Entity & { + spec: { + type: 'backstage-package'; + backstageRole: PackageRole; + lifecycle: string; + owner: string; + [key: string]: any; + }; +}; + +async function createCatalogInfoYaml(options: CreateOptions) { + const { codeowners, dryRun, packageJson, yamlPath } = options; + const instruction = `Create ${relativePath('.', yamlPath)}`; + const owner = getOwnerFromCodeowners(codeowners, yamlPath); + const entity = createOrMergeEntity(packageJson, owner); + + if (dryRun) { + console.error(instruction); + } else { + await writeFile(yamlPath, YAML.dump(entity)); + } + + return instruction; +} + +async function fixCatalogInfoYaml(options: FixOptions) { + const { ci, codeowners, dryRun, packageJson, yamlPath, yamlString } = options; + const possibleOwners = getPossibleCodeowners( + codeowners, + relativePath('.', yamlPath), + ); + const safeName = packageJson.name + .replace(/[^a-z0-9_\-\.]+/g, '-') + .replace(/^[^a-z0-9]|[^a-z0-9]$/g, ''); + let yamlJson: BackstagePackageEntity; + + try { + yamlJson = YAML.load(yamlString) as BackstagePackageEntity; + } catch (e) { + throw new Error(`Unable to parse ${relativePath('.', yamlPath)}: ${e}`); + } + + const badOwner = !possibleOwners.includes(yamlJson.spec?.owner); + const badTitle = yamlJson.metadata.title !== packageJson.name; + const badName = yamlJson.metadata.name !== safeName; + const badType = + yamlJson.spec?.type !== `backstage-${packageJson.backstage.role}`; + + if (badOwner || badTitle || badName || badType) { + const owner = badOwner + ? getOwnerFromCodeowners(codeowners, yamlPath) + : yamlJson.spec?.owner; + const newJson = createOrMergeEntity(packageJson, owner, yamlJson); + const instructions = [`Update ${relativePath('.', yamlPath)}`]; + + // Show more detailed instructions when --ci flag is set. + if (ci) { + if (badOwner) { + instructions.push( + ` spec.owner cannot be "${ + yamlJson.spec.owner + }" because it must be one of (${possibleOwners.join( + ', ', + )}) as listed in CODEOWNERS`, + ); + } + + if (badTitle) { + instructions.push( + ` metadata.title cannot be "${yamlJson.metadata.title}" because it must be exactly "${packageJson.name}", the package.json name`, + ); + } + + if (badName) { + instructions.push( + ` metadata.name cannot be "${yamlJson.metadata.name}" because it must be exactly "${safeName}", as derived from package.json name`, + ); + } + + if (badType) { + instructions.push( + ` spec.type cannot be "${yamlJson.spec.type}" because it must be exactly "backstage-${packageJson.backstage.role}", as derived from package.json backstage.role`, + ); + } + } + + if (dryRun) { + console.error(instructions.join('\n')); + } else { + await writeFile(yamlPath, yamlOverwrite(yamlString, newJson)); + } + + return instructions.join('\n'); + } + + return ''; +} + +/** + * Canonical representation on how to create (or update) a backstage-package + * component. + */ +function createOrMergeEntity( + packageJson: BackstagePackageJson, + owner: string, + existingEntity: BackstagePackageEntity | Record = {}, +): BackstagePackageEntity { + const safeEntityName = packageJson.name + .replace(/[^a-z0-9_\-\.]+/g, '-') + .replace(/^[^a-z0-9]|[^a-z0-9]$/g, ''); + + return { + ...existingEntity, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + ...existingEntity.metadata, + // Provide default name/title/description values. + name: safeEntityName, + title: packageJson.name, + ...(packageJson.description && !existingEntity.metadata?.description + ? { description: packageJson.description } + : undefined), + }, + spec: { + lifecycle: 'experimental', + ...existingEntity.spec, + type: `backstage-${packageJson.backstage.role}`, + owner, + }, + }; +} diff --git a/packages/repo-tools/src/commands/generate-catalog-info/utils.ts b/packages/repo-tools/src/commands/generate-catalog-info/utils.ts new file mode 100644 index 0000000000..b00fbd57a8 --- /dev/null +++ b/packages/repo-tools/src/commands/generate-catalog-info/utils.ts @@ -0,0 +1,50 @@ +/* + * 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 fs from 'fs'; +import { Package } from '@manypkg/get-packages'; +import { + BackstagePackageJson as BackstagePackageJsonActual, + PackageRole, +} from '@backstage/cli-node'; +import { promisify } from 'util'; + +export const readFile = promisify(fs.readFile); +export const writeFile = promisify(fs.writeFile); + +export type BackstagePackageJson = BackstagePackageJsonActual & { + description?: string; + backstage: { + role: PackageRole; + }; +}; + +export function isBackstagePackage( + packageJson: Package['packageJson'], +): packageJson is BackstagePackageJson { + return ( + packageJson && + packageJson.hasOwnProperty('backstage') && + (packageJson as any)?.backstage?.role !== 'undefined' + ); +} + +export const isRejected = ( + input: PromiseSettledResult, +): input is PromiseRejectedResult => input.status === 'rejected'; + +export const isFulfilled = ( + input: PromiseSettledResult, +): input is PromiseFulfilledResult => input.status === 'fulfilled'; diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index e19ffde4b8..c429a253f8 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -96,6 +96,25 @@ export function registerCommands(program: Command) { .description('Find inconsistencies in types of all packages and plugins') .action(lazy(() => import('./type-deps/type-deps').then(m => m.default))); + program + .command('generate-catalog-info') + .option( + '--dry-run', + 'Shows what would happen without actually writing any yaml.', + ) + .option( + '--ci', + 'CI run checks that there are no changes to catalog-info.yaml files', + ) + .description('Create or fix info yaml files for all backstage packages') + .action( + lazy(() => + import('./generate-catalog-info/generate-catalog-info').then( + m => m.default, + ), + ), + ); + registerSchemaCommand(program); } diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 915d54fec3..96af70f375 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,91 @@ # techdocs-cli-embedded-app +## 0.2.86-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.7.0-next.2 + - @backstage/plugin-catalog@1.13.0-next.2 + - @backstage/integration-react@1.1.19-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/app-defaults@1.4.3-next.2 + - @backstage/core-app-api@1.10.0-next.2 + - @backstage/test-utils@1.4.3-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/cli@0.22.13-next.2 + - @backstage/plugin-techdocs-react@1.1.10-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## 0.2.86-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-catalog@1.12.5-next.1 + - @backstage/plugin-techdocs@1.6.9-next.1 + - @backstage/app-defaults@1.4.3-next.1 + - @backstage/cli@0.22.13-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/plugin-techdocs-react@1.1.10-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-app-api@1.10.0-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/test-utils@1.4.3-next.1 + - @backstage/theme@0.4.1 + +## 0.2.86-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/cli@0.22.12-next.0 + - @backstage/plugin-techdocs@1.6.8-next.0 + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/app-defaults@1.4.3-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/test-utils@1.4.3-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-techdocs-react@1.1.10-next.0 + +## 0.2.85 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.9.1 + - @backstage/integration-react@1.1.16 + - @backstage/cli@0.22.10 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog@1.12.1 + - @backstage/app-defaults@1.4.2 + - @backstage/core-plugin-api@1.5.3 + - @backstage/test-utils@1.4.2 + - @backstage/plugin-techdocs@1.6.6 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-techdocs-react@1.1.9 + +## 0.2.85-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.12.1-next.2 + - @backstage/plugin-techdocs@1.6.6-next.2 + - @backstage/cli@0.22.10-next.1 + - @backstage/integration-react@1.1.16-next.1 + ## 0.2.85-next.1 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/catalog-info.yaml b/packages/techdocs-cli-embedded-app/catalog-info.yaml new file mode 100644 index 0000000000..27c9325210 --- /dev/null +++ b/packages/techdocs-cli-embedded-app/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: techdocs-cli-embedded-app + title: techdocs-cli-embedded-app +spec: + lifecycle: production + type: backstage-frontend + owner: techdocs-maintainers diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index d980d2ab6c..8847b535a9 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.85-next.1", + "version": "0.2.86-next.2", "private": true, "backstage": { "role": "frontend" @@ -40,7 +40,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", "@types/react": "*", "@types/react-dom": "*", "cross-env": "^7.0.0", diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index ec0678b2d6..cbaa7ac27c 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,62 @@ # @techdocs/cli +## 1.5.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-techdocs-node@1.8.0-next.2 + - @backstage/cli-common@0.1.12 + +## 1.5.0-next.1 + +### Minor Changes + +- 10a86bd4ae12: Add optional config and cli option for techdocs to specify default mkdocs plugins. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-techdocs-node@1.8.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/cli-common@0.1.12 + +## 1.4.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/plugin-techdocs-node@1.7.6-next.0 + +## 1.4.5 + +### Patch Changes + +- 971bdd6a4732: Bumped internal `nodemon` dependency. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-techdocs-node@1.7.4 + - @backstage/catalog-model@1.4.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + +## 1.4.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-techdocs-node@1.7.4-next.2 + ## 1.4.5-next.1 ### Patch Changes diff --git a/packages/techdocs-cli/catalog-info.yaml b/packages/techdocs-cli/catalog-info.yaml new file mode 100644 index 0000000000..da80a9be3d --- /dev/null +++ b/packages/techdocs-cli/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: techdocs-cli + title: '@techdocs/cli' + description: Utility CLI for managing TechDocs sites in Backstage. +spec: + lifecycle: production + type: backstage-cli + owner: techdocs-maintainers diff --git a/packages/techdocs-cli/cli-report.md b/packages/techdocs-cli/cli-report.md index 556068de85..5f148daf24 100644 --- a/packages/techdocs-cli/cli-report.md +++ b/packages/techdocs-cli/cli-report.md @@ -37,6 +37,7 @@ Options: -v --verbose --omitTechdocsCoreMkdocsPlugin --legacyCopyReadmeMdToIndexMd + --defaultPlugin [defaultPlugins...] -h, --help ``` diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 82ba61e0f6..7adf7561bc 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.4.5-next.1", + "version": "1.5.0-next.2", "publishConfig": { "access": "public" }, @@ -46,7 +46,7 @@ "@types/webpack-env": "^1.15.3", "cypress": "^10.0.0", "find-process": "^1.4.5", - "nodemon": "^2.0.2", + "nodemon": "^3.0.1", "techdocs-cli-embedded-app": "link:../techdocs-cli-embedded-app", "ts-node": "^10.0.0" }, diff --git a/packages/techdocs-cli/src/commands/generate/generate.ts b/packages/techdocs-cli/src/commands/generate/generate.ts index 5d80beb38a..934eaf1efd 100644 --- a/packages/techdocs-cli/src/commands/generate/generate.ts +++ b/packages/techdocs-cli/src/commands/generate/generate.ts @@ -47,6 +47,7 @@ export default async function generate(opts: OptionValues) { const dockerImage = opts.dockerImage; const pullImage = opts.pull; const legacyCopyReadmeMdToIndexMd = opts.legacyCopyReadmeMdToIndexMd; + const defaultPlugins = opts.defaultPlugin; logger.info(`Using source dir ${sourceDir}`); logger.info(`Will output generated files in ${outputDir}`); @@ -68,6 +69,7 @@ export default async function generate(opts: OptionValues) { mkdocs: { legacyCopyReadmeMdToIndexMd, omitTechdocsCorePlugin, + defaultPlugins, }, }, }, diff --git a/packages/techdocs-cli/src/commands/index.ts b/packages/techdocs-cli/src/commands/index.ts index 98816a6b32..3321bb7f43 100644 --- a/packages/techdocs-cli/src/commands/index.ts +++ b/packages/techdocs-cli/src/commands/index.ts @@ -70,6 +70,11 @@ export function registerCommands(program: Command) { 'Attempt to ensure an index.md exists falling back to using /README.md or README.md in case a default /index.md is not provided.', false, ) + .option( + '--defaultPlugin [defaultPlugins...]', + 'Plugins which should be added automatically to the mkdocs.yaml file', + [], + ) .alias('build') .action(lazy(() => import('./generate/generate').then(m => m.default))); diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index a3fb8ba382..e2170a7169 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,58 @@ # @backstage/test-utils +## 1.4.3-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/core-app-api@1.10.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-permission-react@0.4.15-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## 1.4.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/core-app-api@1.10.0-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-react@0.4.15-next.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## 1.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.10.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.15-next.0 + +## 1.4.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.9.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.14 + ## 1.4.2-next.0 ### Patch Changes diff --git a/packages/test-utils/alpha-api-report.md b/packages/test-utils/alpha-api-report.md index 3e0c4aa6c2..c37d79279c 100644 --- a/packages/test-utils/alpha-api-report.md +++ b/packages/test-utils/alpha-api-report.md @@ -4,11 +4,12 @@ ```ts import { PropsWithChildren } from 'react'; +import { default as React_2 } from 'react'; // @alpha export const MockPluginProvider: ({ children, -}: PropsWithChildren<{}>) => JSX.Element; +}: PropsWithChildren<{}>) => React_2.JSX.Element; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index 01fc31c28f..d4b38a243e 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -26,6 +26,7 @@ import { JsonValue } from '@backstage/types'; import { MatcherFunction } from '@testing-library/react'; import { Observable } from '@backstage/types'; import { PermissionApi } from '@backstage/plugin-permission-react'; +import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RenderOptions } from '@testing-library/react'; @@ -214,7 +215,7 @@ export type SyncLogCollector = () => void; // @public export const TestApiProvider: ( props: TestApiProviderProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type TestApiProviderProps = { diff --git a/packages/test-utils/catalog-info.yaml b/packages/test-utils/catalog-info.yaml new file mode 100644 index 0000000000..4b894f08b5 --- /dev/null +++ b/packages/test-utils/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-test-utils + title: '@backstage/test-utils' + description: Utilities to test Backstage plugins and apps. +spec: + lifecycle: production + type: backstage-web-library + owner: maintainers diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index ef074d13b2..1ba9c0229e 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "1.4.2-next.0", + "version": "1.4.3-next.2", "publishConfig": { "access": "public" }, @@ -35,6 +35,7 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", @@ -69,7 +70,6 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "@types/node": "^16.11.26", "msw": "^1.0.0" }, "files": [ diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 6545cc312a..0aec9ff4f8 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -255,7 +255,8 @@ export const textContentMatcher = return false; } - const hasText = (textNode: Element) => textNode?.textContent === text; + const hasText = (textNode: Element) => + textNode?.textContent?.includes(text) ?? false; const childrenDontHaveText = (containerNode: Element) => Array.from(containerNode?.children).every(child => !hasText(child)); diff --git a/packages/theme/catalog-info.yaml b/packages/theme/catalog-info.yaml new file mode 100644 index 0000000000..1fee85f58d --- /dev/null +++ b/packages/theme/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-theme + title: '@backstage/theme' + description: material-ui theme for use with Backstage. +spec: + lifecycle: experimental + type: backstage-web-library + owner: maintainers diff --git a/packages/theme/package.json b/packages/theme/package.json index f5d1f090cf..1e035770e7 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -22,6 +22,7 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/packages/types/catalog-info.yaml b/packages/types/catalog-info.yaml new file mode 100644 index 0000000000..1bd30379cc --- /dev/null +++ b/packages/types/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-types + title: '@backstage/types' + description: Common TypeScript types used within Backstage +spec: + lifecycle: production + type: backstage-common-library + owner: maintainers diff --git a/packages/types/package.json b/packages/types/package.json index 76dbcd3606..29092dfdff 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -23,6 +23,7 @@ "keywords": [ "backstage" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/packages/version-bridge/catalog-info.yaml b/packages/version-bridge/catalog-info.yaml new file mode 100644 index 0000000000..6f37a4b276 --- /dev/null +++ b/packages/version-bridge/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-version-bridge + title: '@backstage/version-bridge' + description: >- + Utilities used by @backstage packages to support multiple concurrent + versions +spec: + lifecycle: production + type: backstage-web-library + owner: maintainers diff --git a/packages/version-bridge/package.json b/packages/version-bridge/package.json index 7e42bcb4cb..eb08531674 100644 --- a/packages/version-bridge/package.json +++ b/packages/version-bridge/package.json @@ -22,6 +22,7 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/plugins/adr-backend/CHANGELOG.md b/plugins/adr-backend/CHANGELOG.md index 07b40ac349..cc78d9d886 100644 --- a/plugins/adr-backend/CHANGELOG.md +++ b/plugins/adr-backend/CHANGELOG.md @@ -1,5 +1,73 @@ # @backstage/plugin-adr-backend +## 0.3.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-adr-common@0.2.15-next.2 + - @backstage/plugin-search-common@1.2.6-next.1 + +## 0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-adr-common@0.2.15-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## 0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-adr-common@0.2.14-next.0 + - @backstage/plugin-search-common@1.2.5 + +## 0.3.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/integration@1.6.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-adr-common@0.2.12 + - @backstage/plugin-search-common@1.2.5 + +## 0.3.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + ## 0.3.6-next.1 ### Patch Changes diff --git a/plugins/adr-backend/api-report.md b/plugins/adr-backend/api-report.md index 2fd22ef9cd..1247a7aa8f 100644 --- a/plugins/adr-backend/api-report.md +++ b/plugins/adr-backend/api-report.md @@ -45,7 +45,8 @@ export type AdrParserContext = { }; // @public -export const adrPlugin: () => BackendFeature; +const adrPlugin: () => BackendFeature; +export default adrPlugin; // @public (undocumented) export type AdrRouterOptions = { diff --git a/plugins/adr-backend/catalog-info.yaml b/plugins/adr-backend/catalog-info.yaml new file mode 100644 index 0000000000..9bd35f2722 --- /dev/null +++ b/plugins/adr-backend/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-adr-backend + title: '@backstage/plugin-adr-backend' +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: kuangp diff --git a/plugins/adr-backend/package.json b/plugins/adr-backend/package.json index 0caa9563d8..80d0e65f5b 100644 --- a/plugins/adr-backend/package.json +++ b/plugins/adr-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-adr-backend", - "version": "0.3.6-next.1", + "version": "0.3.9-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/adr-backend/src/index.ts b/plugins/adr-backend/src/index.ts index eeaf6b4cd0..a2a9788dcb 100644 --- a/plugins/adr-backend/src/index.ts +++ b/plugins/adr-backend/src/index.ts @@ -22,4 +22,4 @@ export * from './search'; export * from './service'; -export { adrPlugin } from './plugin'; +export { adrPlugin as default } from './plugin'; diff --git a/plugins/adr-common/CHANGELOG.md b/plugins/adr-common/CHANGELOG.md index 9b8d5dbf74..13cc0dfa53 100644 --- a/plugins/adr-common/CHANGELOG.md +++ b/plugins/adr-common/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-adr-common +## 0.2.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## 0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.6-next.0 + +## 0.2.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/plugin-search-common@1.2.5 + +## 0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.6.0 + - @backstage/catalog-model@1.4.1 + - @backstage/plugin-search-common@1.2.5 + ## 0.2.11 ### Patch Changes diff --git a/plugins/adr-common/catalog-info.yaml b/plugins/adr-common/catalog-info.yaml new file mode 100644 index 0000000000..defcbe9fdb --- /dev/null +++ b/plugins/adr-common/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-adr-common + title: '@backstage/plugin-adr-common' + description: Common functionalities for the adr plugin +spec: + lifecycle: experimental + type: backstage-common-library + owner: kuangp diff --git a/plugins/adr-common/package.json b/plugins/adr-common/package.json index 2def65cf55..e9315cab0d 100644 --- a/plugins/adr-common/package.json +++ b/plugins/adr-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-adr-common", "description": "Common functionalities for the adr plugin", - "version": "0.2.11", + "version": "0.2.15-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,6 +20,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/adr-common" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/plugins/adr/CHANGELOG.md b/plugins/adr/CHANGELOG.md index 57f888eaa7..17009339d9 100644 --- a/plugins/adr/CHANGELOG.md +++ b/plugins/adr/CHANGELOG.md @@ -1,5 +1,75 @@ # @backstage/plugin-adr +## 0.6.7-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/integration-react@1.1.19-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/plugin-search-react@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-adr-common@0.2.15-next.2 + - @backstage/plugin-search-common@1.2.6-next.1 + +## 0.6.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/plugin-search-react@1.7.0-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-adr-common@0.2.15-next.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## 0.6.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-adr-common@0.2.14-next.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-search-react@1.6.5-next.0 + +## 0.6.4 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/integration-react@1.1.16 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/plugin-search-react@1.6.4 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-adr-common@0.2.12 + - @backstage/plugin-search-common@1.2.5 + +## 0.6.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/integration-react@1.1.16-next.1 + ## 0.6.4-next.1 ### Patch Changes diff --git a/plugins/adr/api-report.md b/plugins/adr/api-report.md index 4cec5047fc..6ed7b68196 100644 --- a/plugins/adr/api-report.md +++ b/plugins/adr/api-report.md @@ -12,6 +12,8 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; import { isAdrAvailable } from '@backstage/plugin-adr-common'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -76,7 +78,10 @@ export const adrPlugin: BackstagePlugin< // @public export const AdrReader: { - (props: { adr: string; decorators?: AdrContentDecorator[] }): JSX.Element; + (props: { + adr: string; + decorators?: AdrContentDecorator[]; + }): React_2.JSX.Element; decorators: Readonly<{ createRewriteRelativeLinksDecorator(): AdrContentDecorator; createRewriteRelativeEmbedsDecorator(): AdrContentDecorator; @@ -107,7 +112,7 @@ export type AdrSearchResultListItemProps = { export const EntityAdrContent: (props: { contentDecorators?: AdrContentDecorator[] | undefined; filePathFilterFn?: AdrFilePathFilterFn | undefined; -}) => JSX.Element; +}) => JSX_2.Element; export { isAdrAvailable }; ``` diff --git a/plugins/adr/catalog-info.yaml b/plugins/adr/catalog-info.yaml new file mode 100644 index 0000000000..61fb6116d6 --- /dev/null +++ b/plugins/adr/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-adr + title: '@backstage/plugin-adr' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: kuangp diff --git a/plugins/adr/package.json b/plugins/adr/package.json index 84d5080a4a..d288f92d9c 100644 --- a/plugins/adr/package.json +++ b/plugins/adr/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-adr", - "version": "0.6.4-next.1", + "version": "0.6.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,6 +18,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/adr" }, + "sideEffects": false, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", @@ -61,7 +62,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/git-url-parse": "^9.0.0", - "@types/node": "*", "cross-fetch": "^3.1.5", "msw": "^1.0.0" }, diff --git a/plugins/airbrake-backend/CHANGELOG.md b/plugins/airbrake-backend/CHANGELOG.md index 3892e5563b..d750762517 100644 --- a/plugins/airbrake-backend/CHANGELOG.md +++ b/plugins/airbrake-backend/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/plugin-airbrake-backend +## 0.2.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + +## 0.2.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + +## 0.2.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + +## 0.2.21 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/config@1.0.8 + +## 0.2.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + ## 0.2.21-next.1 ### Patch Changes diff --git a/plugins/airbrake-backend/api-report.md b/plugins/airbrake-backend/api-report.md index c410245e31..4658767b4a 100644 --- a/plugins/airbrake-backend/api-report.md +++ b/plugins/airbrake-backend/api-report.md @@ -14,7 +14,8 @@ export interface AirbrakeConfig { } // @public -export const airbrakePlugin: () => BackendFeature; +const airbrakePlugin: () => BackendFeature; +export default airbrakePlugin; // @public export function createRouter(options: RouterOptions): Promise; diff --git a/plugins/airbrake-backend/catalog-info.yaml b/plugins/airbrake-backend/catalog-info.yaml new file mode 100644 index 0000000000..90884f5d34 --- /dev/null +++ b/plugins/airbrake-backend/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-airbrake-backend + title: '@backstage/plugin-airbrake-backend' +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: maintainers diff --git a/plugins/airbrake-backend/package.json b/plugins/airbrake-backend/package.json index f3ff319d65..141551c20b 100644 --- a/plugins/airbrake-backend/package.json +++ b/plugins/airbrake-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake-backend", - "version": "0.2.21-next.1", + "version": "0.2.24-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/airbrake-backend/src/index.ts b/plugins/airbrake-backend/src/index.ts index 5905a7e0e5..3f660dad62 100644 --- a/plugins/airbrake-backend/src/index.ts +++ b/plugins/airbrake-backend/src/index.ts @@ -22,4 +22,4 @@ export * from './service/router'; export * from './config'; -export { airbrakePlugin } from './plugin'; +export { airbrakePlugin as default } from './plugin'; diff --git a/plugins/airbrake/CHANGELOG.md b/plugins/airbrake/CHANGELOG.md index 5ef51bc443..fe22cddd46 100644 --- a/plugins/airbrake/CHANGELOG.md +++ b/plugins/airbrake/CHANGELOG.md @@ -1,5 +1,68 @@ # @backstage/plugin-airbrake +## 0.3.24-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/test-utils@1.4.3-next.2 + - @backstage/dev-utils@1.0.21-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## 0.3.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/dev-utils@1.0.21-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/test-utils@1.4.3-next.1 + - @backstage/theme@0.4.1 + +## 0.3.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/dev-utils@1.0.20-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/test-utils@1.4.3-next.0 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.3.21 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/dev-utils@1.0.18 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/test-utils@1.4.2 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## 0.3.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/dev-utils@1.0.18-next.2 + ## 0.3.21-next.1 ### Patch Changes diff --git a/plugins/airbrake/api-report.md b/plugins/airbrake/api-report.md index 773e2a029a..a04d57d7e9 100644 --- a/plugins/airbrake/api-report.md +++ b/plugins/airbrake/api-report.md @@ -3,10 +3,9 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public @@ -19,7 +18,7 @@ export const airbrakePlugin: BackstagePlugin< >; // @public -export const EntityAirbrakeContent: () => JSX.Element; +export const EntityAirbrakeContent: () => React_2.JSX.Element; // @public export const isAirbrakeAvailable: (entity: Entity) => boolean; diff --git a/plugins/airbrake/catalog-info.yaml b/plugins/airbrake/catalog-info.yaml new file mode 100644 index 0000000000..d1eac0d04e --- /dev/null +++ b/plugins/airbrake/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-airbrake + title: '@backstage/plugin-airbrake' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 72f3bc6094..2ed15d168b 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake", - "version": "0.3.21-next.1", + "version": "0.3.24-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,6 +18,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/airbrake" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -38,6 +39,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -54,8 +56,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md index 8bbf800b78..f61140dd91 100644 --- a/plugins/allure/CHANGELOG.md +++ b/plugins/allure/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-allure +## 0.1.40-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## 0.1.40-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## 0.1.39-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.1.37 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## 0.1.37-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.1.37-next.1 ### Patch Changes diff --git a/plugins/allure/api-report.md b/plugins/allure/api-report.md index c1ce499ab2..a192851f26 100644 --- a/plugins/allure/api-report.md +++ b/plugins/allure/api-report.md @@ -7,6 +7,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -22,7 +23,7 @@ export const allurePlugin: BackstagePlugin< >; // @public (undocumented) -export const EntityAllureReportContent: () => JSX.Element; +export const EntityAllureReportContent: () => JSX_2.Element; // @public (undocumented) export const isAllureReportAvailable: (entity: Entity) => boolean; diff --git a/plugins/allure/catalog-info.yaml b/plugins/allure/catalog-info.yaml new file mode 100644 index 0000000000..4ea1238b30 --- /dev/null +++ b/plugins/allure/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-allure + title: '@backstage/plugin-allure' + description: A Backstage plugin that integrates with Allure +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/allure/package.json b/plugins/allure/package.json index 730347cf8c..90055b48d2 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-allure", "description": "A Backstage plugin that integrates with Allure", - "version": "0.1.37-next.1", + "version": "0.1.40-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,6 +19,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/allure" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -37,6 +38,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -53,8 +55,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/analytics-module-ga/CHANGELOG.md b/plugins/analytics-module-ga/CHANGELOG.md index d40cb001e2..7236fd68d5 100644 --- a/plugins/analytics-module-ga/CHANGELOG.md +++ b/plugins/analytics-module-ga/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/plugin-analytics-module-ga +## 0.1.33-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/theme@0.4.1 + +## 0.1.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## 0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## 0.1.32 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + ## 0.1.32-next.1 ### Patch Changes diff --git a/plugins/analytics-module-ga/catalog-info.yaml b/plugins/analytics-module-ga/catalog-info.yaml new file mode 100644 index 0000000000..24dddf1aa5 --- /dev/null +++ b/plugins/analytics-module-ga/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-analytics-module-ga + title: '@backstage/plugin-analytics-module-ga' +spec: + lifecycle: experimental + type: backstage-frontend-plugin-module + owner: maintainers diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index ef77925719..91bc1029b2 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-analytics-module-ga", - "version": "0.1.32-next.1", + "version": "0.1.33-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,6 +18,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/analytics-module-ga" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -53,7 +54,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, diff --git a/plugins/analytics-module-ga4/CHANGELOG.md b/plugins/analytics-module-ga4/CHANGELOG.md index fd589fec4b..ba52a27703 100644 --- a/plugins/analytics-module-ga4/CHANGELOG.md +++ b/plugins/analytics-module-ga4/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/plugin-analytics-module-ga4 +## 0.1.4-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/theme@0.4.1 + +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## 0.1.3 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + ## 0.1.3-next.1 ### Patch Changes diff --git a/plugins/analytics-module-ga4/catalog-info.yaml b/plugins/analytics-module-ga4/catalog-info.yaml new file mode 100644 index 0000000000..8d98ebe82c --- /dev/null +++ b/plugins/analytics-module-ga4/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-analytics-module-ga4 + title: '@backstage/plugin-analytics-module-ga4' +spec: + lifecycle: experimental + type: backstage-frontend-plugin-module + owner: maintainers diff --git a/plugins/analytics-module-ga4/package.json b/plugins/analytics-module-ga4/package.json index ef55377f0c..2ec3e28c93 100644 --- a/plugins/analytics-module-ga4/package.json +++ b/plugins/analytics-module-ga4/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-analytics-module-ga4", - "version": "0.1.3-next.1", + "version": "0.1.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,6 +18,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/plugins/analytics-module-ga4" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -50,7 +51,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/jest": "^28.1.3", - "@types/node": "^16.11.26", "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, diff --git a/plugins/analytics-module-newrelic-browser/CHANGELOG.md b/plugins/analytics-module-newrelic-browser/CHANGELOG.md index beea288700..59b4b9dfc8 100644 --- a/plugins/analytics-module-newrelic-browser/CHANGELOG.md +++ b/plugins/analytics-module-newrelic-browser/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/plugin-analytics-module-newrelic-browser +## 0.0.2-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + +## 0.0.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + +## 0.0.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/config@1.0.8 + +## 0.0.1 + +### Patch Changes + +- ec7357258853: Introduced the New Relic Browser analytics module. Check out the plugins [README.md](https://github.com/backstage/backstage/tree/master/plugins/analytics-module-newrelic-browser) for more details! +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/config@1.0.8 + ## 0.0.1-next.0 ### Patch Changes diff --git a/plugins/analytics-module-newrelic-browser/catalog-info.yaml b/plugins/analytics-module-newrelic-browser/catalog-info.yaml new file mode 100644 index 0000000000..68f38c4848 --- /dev/null +++ b/plugins/analytics-module-newrelic-browser/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-analytics-module-newrelic-browser + title: '@backstage/plugin-analytics-module-newrelic-browser' +spec: + lifecycle: experimental + type: backstage-frontend-plugin-module + owner: maintainers diff --git a/plugins/analytics-module-newrelic-browser/package.json b/plugins/analytics-module-newrelic-browser/package.json index 677d3b8e16..860d33e809 100644 --- a/plugins/analytics-module-newrelic-browser/package.json +++ b/plugins/analytics-module-newrelic-browser/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-analytics-module-newrelic-browser", - "version": "0.0.1-next.0", + "version": "0.0.2-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -12,6 +12,7 @@ "backstage": { "role": "frontend-plugin-module" }, + "sideEffects": false, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", @@ -39,7 +40,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "*", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/apache-airflow/CHANGELOG.md b/plugins/apache-airflow/CHANGELOG.md index 42756b19df..a73eb1a44a 100644 --- a/plugins/apache-airflow/CHANGELOG.md +++ b/plugins/apache-airflow/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-apache-airflow +## 0.2.15-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + +## 0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + +## 0.2.15-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + +## 0.2.14 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + ## 0.2.14-next.1 ### Patch Changes diff --git a/plugins/apache-airflow/api-report.md b/plugins/apache-airflow/api-report.md index b427a0148e..be2a208bd4 100644 --- a/plugins/apache-airflow/api-report.md +++ b/plugins/apache-airflow/api-report.md @@ -6,15 +6,16 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public export const ApacheAirflowDagTable: (props: { dagIds?: string[] | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) -export const ApacheAirflowPage: () => JSX.Element; +export const ApacheAirflowPage: () => JSX_2.Element; // @public (undocumented) export const apacheAirflowPlugin: BackstagePlugin< diff --git a/plugins/apache-airflow/catalog-info.yaml b/plugins/apache-airflow/catalog-info.yaml new file mode 100644 index 0000000000..d564af4912 --- /dev/null +++ b/plugins/apache-airflow/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-apache-airflow + title: '@backstage/plugin-apache-airflow' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json index a1e84dc6fb..61e4f349c7 100644 --- a/plugins/apache-airflow/package.json +++ b/plugins/apache-airflow/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apache-airflow", - "version": "0.2.14-next.1", + "version": "0.2.15-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,6 +18,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/apache-airflow" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -33,6 +34,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "qs": "^6.10.1", "react-use": "^17.2.4" @@ -51,8 +53,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/api-docs-module-protoc-gen-doc/CHANGELOG.md b/plugins/api-docs-module-protoc-gen-doc/CHANGELOG.md index 87ee39981f..4c35a4eab9 100644 --- a/plugins/api-docs-module-protoc-gen-doc/CHANGELOG.md +++ b/plugins/api-docs-module-protoc-gen-doc/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-api-docs-module-protoc-gen-doc +## 0.1.3-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency + ## 0.1.2 ### Patch Changes diff --git a/plugins/api-docs-module-protoc-gen-doc/api-report.md b/plugins/api-docs-module-protoc-gen-doc/api-report.md index 696fee2a20..836f8313d8 100644 --- a/plugins/api-docs-module-protoc-gen-doc/api-report.md +++ b/plugins/api-docs-module-protoc-gen-doc/api-report.md @@ -3,12 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// +import { default as React_2 } from 'react'; // @public (undocumented) export const grpcDocsApiWidget: { type: string; title: string; - component: (definition: string) => JSX.Element; + component: (definition: string) => React_2.JSX.Element; }; ``` diff --git a/plugins/api-docs-module-protoc-gen-doc/catalog-info.yaml b/plugins/api-docs-module-protoc-gen-doc/catalog-info.yaml new file mode 100644 index 0000000000..b69366878b --- /dev/null +++ b/plugins/api-docs-module-protoc-gen-doc/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-api-docs-module-protoc-gen-doc + title: '@backstage/plugin-api-docs-module-protoc-gen-doc' + description: >- + Additional functionalities for the api-docs plugin that renders the output + of the protoc-gen-doc +spec: + lifecycle: experimental + type: backstage-frontend-plugin-module + owner: maintainers diff --git a/plugins/api-docs-module-protoc-gen-doc/package.json b/plugins/api-docs-module-protoc-gen-doc/package.json index 2d79af78a3..64942f3f3b 100644 --- a/plugins/api-docs-module-protoc-gen-doc/package.json +++ b/plugins/api-docs-module-protoc-gen-doc/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-api-docs-module-protoc-gen-doc", "description": "Additional functionalities for the api-docs plugin that renders the output of the protoc-gen-doc", - "version": "0.1.2", + "version": "0.1.3-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,6 +22,7 @@ "keywords": [ "backstage" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", @@ -31,6 +32,7 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "grpc-docs": "^1.1.2" }, "peerDependencies": { @@ -40,8 +42,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "@testing-library/jest-dom": "^5.16.4", - "@types/react": "^16.13.1 || ^17.0.0" + "@testing-library/jest-dom": "^5.16.4" }, "files": [ "dist" diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 7ed022f59e..775286fdc4 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/plugin-api-docs +## 0.9.11-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/plugin-catalog@1.13.0-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## 0.9.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/plugin-catalog@1.12.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## 0.9.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.9.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog@1.12.1 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## 0.9.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/plugin-catalog@1.12.1-next.2 + ## 0.9.7-next.1 ### Patch Changes diff --git a/plugins/api-docs/api-report.md b/plugins/api-docs/api-report.md index a9b21664c3..59d490a1fe 100644 --- a/plugins/api-docs/api-report.md +++ b/plugins/api-docs/api-report.md @@ -11,6 +11,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CatalogTableRow } from '@backstage/plugin-catalog'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { TableColumn } from '@backstage/core-components'; @@ -18,14 +19,14 @@ import { TableProps } from '@backstage/core-components'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; // @public (undocumented) -export const ApiDefinitionCard: () => JSX.Element; +export const ApiDefinitionCard: () => React_2.JSX.Element; // @public export function ApiDefinitionDialog(props: { open: boolean; entity: ApiEntity; onClose: () => void; -}): JSX.Element; +}): React_2.JSX.Element; // @public (undocumented) export type ApiDefinitionWidget = { @@ -62,20 +63,22 @@ export { apiDocsPlugin as plugin }; // @public export const ApiExplorerIndexPage: ( props: DefaultApiExplorerPageProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export const ApiExplorerPage: ( props: DefaultApiExplorerPageProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) -export const ApiTypeTitle: (props: { apiEntity: ApiEntity }) => JSX.Element; +export const ApiTypeTitle: (props: { + apiEntity: ApiEntity; +}) => React_2.JSX.Element; // @public (undocumented) export const AsyncApiDefinitionWidget: ( props: AsyncApiDefinitionWidgetProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type AsyncApiDefinitionWidgetProps = { @@ -86,17 +89,17 @@ export type AsyncApiDefinitionWidgetProps = { export const ConsumedApisCard: (props: { variant?: InfoCardVariants; columns?: TableColumn[]; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const ConsumingComponentsCard: (props: { variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public export const DefaultApiExplorerPage: ( props: DefaultApiExplorerPageProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type DefaultApiExplorerPageProps = { @@ -109,40 +112,40 @@ export type DefaultApiExplorerPageProps = { export function defaultDefinitionWidgets(): ApiDefinitionWidget[]; // @public (undocumented) -export const EntityApiDefinitionCard: () => JSX.Element; +export const EntityApiDefinitionCard: () => JSX_2.Element; // @public (undocumented) export const EntityConsumedApisCard: (props: { variant?: InfoCardVariants | undefined; columns?: TableColumn[] | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityConsumingComponentsCard: (props: { variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityHasApisCard: (props: { variant?: InfoCardVariants | undefined; columns?: TableColumn[] | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityProvidedApisCard: (props: { variant?: InfoCardVariants | undefined; columns?: TableColumn[] | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityProvidingComponentsCard: (props: { variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const GraphQlDefinitionWidget: ( props: GraphQlDefinitionWidgetProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type GraphQlDefinitionWidgetProps = { @@ -153,12 +156,12 @@ export type GraphQlDefinitionWidgetProps = { export const HasApisCard: (props: { variant?: InfoCardVariants; columns?: TableColumn[]; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const OpenApiDefinitionWidget: ( props: OpenApiDefinitionWidgetProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type OpenApiDefinitionWidgetProps = { @@ -168,7 +171,7 @@ export type OpenApiDefinitionWidgetProps = { // @public (undocumented) export const PlainApiDefinitionWidget: ( props: PlainApiDefinitionWidgetProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type PlainApiDefinitionWidgetProps = { @@ -180,17 +183,17 @@ export type PlainApiDefinitionWidgetProps = { export const ProvidedApisCard: (props: { variant?: InfoCardVariants; columns?: TableColumn[]; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const ProvidingComponentsCard: (props: { variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const TrpcApiDefinitionWidget: ( props: TrpcApiDefinitionWidgetProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type TrpcApiDefinitionWidgetProps = { diff --git a/plugins/api-docs/catalog-info.yaml b/plugins/api-docs/catalog-info.yaml new file mode 100644 index 0000000000..3c09133857 --- /dev/null +++ b/plugins/api-docs/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-api-docs + title: '@backstage/plugin-api-docs' + description: A Backstage plugin that helps represent API entities in the frontend +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: sda-se-reviewers diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 8d67619aa9..4257d26cf8 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-api-docs", "description": "A Backstage plugin that helps represent API entities in the frontend", - "version": "0.9.7-next.1", + "version": "0.9.11-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,6 +22,7 @@ "keywords": [ "backstage" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -64,7 +65,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", "@types/swagger-ui-react": "^4.18.0", "cross-fetch": "^3.1.5", "msw": "^1.0.0" diff --git a/plugins/apollo-explorer/CHANGELOG.md b/plugins/apollo-explorer/CHANGELOG.md index b733c807ba..1f3c0d445b 100644 --- a/plugins/apollo-explorer/CHANGELOG.md +++ b/plugins/apollo-explorer/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-apollo-explorer +## 0.1.15-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/theme@0.4.1 + +## 0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## 0.1.15-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## 0.1.14 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/theme@0.4.1 + ## 0.1.14-next.1 ### Patch Changes diff --git a/plugins/apollo-explorer/api-report.md b/plugins/apollo-explorer/api-report.md index 66cc473d0b..2dd6c14a90 100644 --- a/plugins/apollo-explorer/api-report.md +++ b/plugins/apollo-explorer/api-report.md @@ -7,6 +7,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { JSONObject } from '@apollo/explorer/src/helpers/types'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public @@ -30,7 +31,7 @@ export const ApolloExplorerPage: (props: { } | undefined; }[]; -}) => JSX.Element; +}) => JSX_2.Element; // @public export const apolloExplorerPlugin: BackstagePlugin< diff --git a/plugins/apollo-explorer/catalog-info.yaml b/plugins/apollo-explorer/catalog-info.yaml new file mode 100644 index 0000000000..7e2380679c --- /dev/null +++ b/plugins/apollo-explorer/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-apollo-explorer + title: '@backstage/plugin-apollo-explorer' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/apollo-explorer/package.json b/plugins/apollo-explorer/package.json index f989ba29ad..e6782fae91 100644 --- a/plugins/apollo-explorer/package.json +++ b/plugins/apollo-explorer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apollo-explorer", - "version": "0.1.14-next.1", + "version": "0.1.15-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,6 +18,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/apollo-explorer" }, + "sideEffects": false, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", @@ -34,6 +35,7 @@ "@backstage/theme": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4", "use-deep-compare-effect": "^1.8.1" }, @@ -51,8 +53,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index a92677ab41..3bcbd4a8bb 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-app-backend +## 0.3.51-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.5.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/types@1.1.0 + - @backstage/plugin-app-node@0.1.3-next.2 + +## 0.3.51-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/config-loader@1.5.0-next.1 + - @backstage/plugin-app-node@0.1.3-next.1 + - @backstage/types@1.1.0 + +## 0.3.50-next.0 + +### Patch Changes + +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config-loader@1.5.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-app-node@0.1.2-next.0 + +## 0.3.48 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- d564ad142b17: Migrated the alpha `appBackend` export to use static configuration and extension points rather than accepting options. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config-loader@1.4.0 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-app-node@0.1.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + +## 0.3.48-next.2 + +### Patch Changes + +- d564ad142b17: Migrated the alpha `appBackend` export to use static configuration and extension points rather than accepting options. +- Updated dependencies + - @backstage/plugin-app-node@0.1.0-next.0 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/config-loader@1.4.0-next.1 + ## 0.3.48-next.1 ### Patch Changes diff --git a/plugins/app-backend/alpha-api-report.md b/plugins/app-backend/alpha-api-report.md index 5fb0546a79..615065efd7 100644 --- a/plugins/app-backend/alpha-api-report.md +++ b/plugins/app-backend/alpha-api-report.md @@ -6,7 +6,8 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; // @alpha -export const appPlugin: () => BackendFeature; +const appPlugin: () => BackendFeature; +export default appPlugin; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/app-backend/catalog-info.yaml b/plugins/app-backend/catalog-info.yaml new file mode 100644 index 0000000000..27135f4f8d --- /dev/null +++ b/plugins/app-backend/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-app-backend + title: '@backstage/plugin-app-backend' + description: A Backstage backend plugin that serves the Backstage frontend app +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: maintainers diff --git a/plugins/app-backend/migrations/20211229105307_init.js b/plugins/app-backend/migrations/20211229105307_init.js index 29e37b35f7..25ed48c307 100644 --- a/plugins/app-backend/migrations/20211229105307_init.js +++ b/plugins/app-backend/migrations/20211229105307_init.js @@ -20,11 +20,20 @@ * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { - return knex.schema.createTable('static_assets_cache', table => { + const isMySQL = knex.client.config.client.includes('mysql'); + await knex.schema.createTable('static_assets_cache', table => { table.comment( 'A cache of static assets that where previously deployed and may still be lazy-loaded by clients', ); - table.text('path').primary().notNullable().comment('The path of the file'); + if (!isMySQL) { + table + .text('path') + .primary() + .notNullable() + .comment('The path of the file'); + } else { + table.text('path').notNullable().comment('The path of the file'); + } table .dateTime('last_modified_at') .defaultTo(knex.fn.now()) @@ -35,6 +44,12 @@ exports.up = async function up(knex) { table.binary('content').notNullable().comment('The asset content'); table.index('last_modified_at', 'static_asset_cache_last_modified_at_idx'); }); + // specifically for mysql specify a unique index up to 254 characters(mysql limit) + if (isMySQL) { + await knex.schema.raw( + 'create unique index static_assets_cache_path_idx on static_assets_cache(path(254));', + ); + } }; /** diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index be1b6b3a80..9db3d48bf0 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-app-backend", "description": "A Backstage backend plugin that serves the Backstage frontend app", - "version": "0.3.48-next.1", + "version": "0.3.51-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/app-backend/src/alpha.ts b/plugins/app-backend/src/alpha.ts index edb5c8caf5..830e3587dc 100644 --- a/plugins/app-backend/src/alpha.ts +++ b/plugins/app-backend/src/alpha.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { appPlugin } from './service/appPlugin'; +export { appPlugin as default } from './service/appPlugin'; diff --git a/plugins/app-backend/src/lib/assets/StaticAssetsStore.test.ts b/plugins/app-backend/src/lib/assets/StaticAssetsStore.test.ts index 8a0935d4e3..68fe4e2287 100644 --- a/plugins/app-backend/src/lib/assets/StaticAssetsStore.test.ts +++ b/plugins/app-backend/src/lib/assets/StaticAssetsStore.test.ts @@ -37,7 +37,7 @@ jest.setTimeout(60_000); describe('StaticAssetsStore', () => { const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], }); it.each(databases.eachSupportedId())( @@ -153,14 +153,18 @@ describe('StaticAssetsStore', () => { content: async () => Buffer.alloc(0), }, ]); - + // interval check for postgresql + let hourPast = `now() + interval '-3600 seconds'`; + if (knex.client.config.client.includes('mysql')) { + hourPast = `date_sub(now(), interval 3600 second)`; + } else if (knex.client.config.client.includes('sqlite3')) { + hourPast = `datetime('now', '-3600 seconds')`; + } // Rewrite modified time of "old" to be 1h in the past const updated = await knex('static_assets_cache') .where({ path: 'old' }) .update({ - last_modified_at: knex.client.config.client.includes('sqlite3') - ? knex.raw(`datetime('now', '-3600 seconds')`) - : knex.raw(`now() + interval '-3600 seconds'`), + last_modified_at: knex.raw(hourPast), }); expect(updated).toBe(1); diff --git a/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts b/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts index 6905d63779..9f6ada717d 100644 --- a/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts +++ b/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts @@ -138,14 +138,20 @@ export class StaticAssetsStore implements StaticAssetProvider { */ async trimAssets(options: { maxAgeSeconds: number }) { const { maxAgeSeconds } = options; + let lastModifiedInterval = this.#db.raw( + `now() + interval '${-maxAgeSeconds} seconds'`, + ); + if (this.#db.client.config.client.includes('mysql')) { + lastModifiedInterval = this.#db.raw( + `date_sub(now(), interval ${maxAgeSeconds} second)`, + ); + } else if (this.#db.client.config.client.includes('sqlite3')) { + lastModifiedInterval = this.#db.raw(`datetime('now', ?)`, [ + `-${maxAgeSeconds} seconds`, + ]); + } await this.#db('static_assets_cache') - .where( - 'last_modified_at', - '<=', - this.#db.client.config.client.includes('sqlite3') - ? this.#db.raw(`datetime('now', ?)`, [`-${maxAgeSeconds} seconds`]) - : this.#db.raw(`now() + interval '${-maxAgeSeconds} seconds'`), - ) + .where('last_modified_at', '<=', lastModifiedInterval) .delete(); } } diff --git a/plugins/app-backend/src/service/appPlugin.test.ts b/plugins/app-backend/src/service/appPlugin.test.ts index 37d38663ef..b6a972b4c7 100644 --- a/plugins/app-backend/src/service/appPlugin.test.ts +++ b/plugins/app-backend/src/service/appPlugin.test.ts @@ -39,8 +39,8 @@ describe('appPlugin', () => { it('boots', async () => { const { server } = await startTestBackend({ - features: [appPlugin()], - services: [ + features: [ + appPlugin(), mockServices.rootConfig.factory({ data: { app: { diff --git a/plugins/app-node/CHANGELOG.md b/plugins/app-node/CHANGELOG.md new file mode 100644 index 0000000000..537e4915d9 --- /dev/null +++ b/plugins/app-node/CHANGELOG.md @@ -0,0 +1,44 @@ +# @backstage/plugin-app-node + +## 0.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.2 + +## 0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.1 + +## 0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + +## 0.1.0 + +### Minor Changes + +- 9fbe95ef6503: Added the `app` plugin node library, initially providing an extension point that can be used to configure a static fallback handler. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0 + +## 0.1.0-next.0 + +### Minor Changes + +- 9fbe95ef6503: Added the `app` plugin node library, initially providing an extension point that can be used to configure a static fallback handler. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 diff --git a/plugins/app-node/catalog-info.yaml b/plugins/app-node/catalog-info.yaml new file mode 100644 index 0000000000..6624153c6c --- /dev/null +++ b/plugins/app-node/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-app-node + title: '@backstage/plugin-app-node' + description: Node.js library for the app plugin +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/plugins/app-node/package.json b/plugins/app-node/package.json index c971bdb628..f0f415f901 100644 --- a/plugins/app-node/package.json +++ b/plugins/app-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-app-node", "description": "Node.js library for the app plugin", - "version": "0.0.0", + "version": "0.1.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/auth-backend-module-gcp-iap-provider/.eslintrc.js b/plugins/auth-backend-module-gcp-iap-provider/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md new file mode 100644 index 0000000000..32fdb2473a --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md @@ -0,0 +1,35 @@ +# @backstage/plugin-auth-backend-module-gcp-iap-provider + +## 0.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.1.0-next.0 + +### Minor Changes + +- 8513cd7d00e3: New module for `@backstage/plugin-auth-backend` that adds a GCP IAP auth provider. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 diff --git a/plugins/auth-backend-module-gcp-iap-provider/README.md b/plugins/auth-backend-module-gcp-iap-provider/README.md new file mode 100644 index 0000000000..a5f3ce1d4c --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/README.md @@ -0,0 +1,5 @@ +# Auth Backend Module - GCP IAP Provider + +## Links + +- [The Backstage homepage](https://backstage.io) diff --git a/plugins/auth-backend-module-gcp-iap-provider/api-report.md b/plugins/auth-backend-module-gcp-iap-provider/api-report.md new file mode 100644 index 0000000000..821aa8e437 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/api-report.md @@ -0,0 +1,47 @@ +## API Report File for "@backstage/plugin-auth-backend-module-gcp-iap-provider" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { JsonPrimitive } from '@backstage/types'; +import { ProxyAuthenticator } from '@backstage/plugin-auth-node'; +import { SignInResolverFactory } from '@backstage/plugin-auth-node'; + +// @public (undocumented) +const authModuleGcpIapProvider: () => BackendFeature; +export default authModuleGcpIapProvider; + +// @public (undocumented) +export const gcpIapAuthenticator: ProxyAuthenticator< + { + jwtHeader: string; + tokenValidator: (token: string) => Promise; + }, + { + iapToken: GcpIapTokenInfo; + } +>; + +// @public +export type GcpIapResult = { + iapToken: GcpIapTokenInfo; +}; + +// @public +export namespace gcpIapSignInResolvers { + const emailMatchingUserEntityAnnotation: SignInResolverFactory< + GcpIapResult, + unknown + >; +} + +// @public +export type GcpIapTokenInfo = { + sub: string; + email: string; + [key: string]: JsonPrimitive; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/auth-backend-module-gcp-iap-provider/catalog-info.yaml b/plugins/auth-backend-module-gcp-iap-provider/catalog-info.yaml new file mode 100644 index 0000000000..ef604027df --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth-backend-module-gcp-iap-provider + title: '@backstage/plugin-auth-backend-module-gcp-iap-provider' + description: A GCP IAP auth provider module for the Backstage auth backend +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/auth-backend-module-gcp-iap-provider/config.d.ts b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts new file mode 100644 index 0000000000..945378ada6 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts @@ -0,0 +1,39 @@ +/* + * 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 { + auth?: { + providers?: { + /** + * Configuration for the Google Cloud Platform Identity-Aware Proxy (IAP) auth provider. + */ + gcpIap?: { + [authEnv: string]: { + /** + * The audience to use when validating incoming JWT tokens. + * See https://backstage.io/docs/auth/google/gcp-iap-auth + */ + audience: string; + + /** + * The name of the header to read the JWT token from, defaults to `'x-goog-iap-jwt-assertion'`. + */ + jwtHeader?: string; + }; + }; + }; + }; +} diff --git a/plugins/auth-backend-module-gcp-iap-provider/package.json b/plugins/auth-backend-module-gcp-iap-provider/package.json new file mode 100644 index 0000000000..8b2111cfb1 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-auth-backend-module-gcp-iap-provider", + "description": "A GCP IAP auth provider module for the Backstage auth backend", + "version": "0.1.0-next.2", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/auth-backend-module-gcp-iap-provider" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "@backstage/types": "workspace:^", + "google-auth-library": "^8.0.0" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "express": "^4.18.2", + "msw": "^1.0.0", + "supertest": "^6.1.3" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.test.ts b/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.test.ts new file mode 100644 index 0000000000..51efbe1a04 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.test.ts @@ -0,0 +1,91 @@ +/* + * 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 { mockServices } from '@backstage/backend-test-utils'; +import { Request } from 'express'; +import { gcpIapAuthenticator } from './authenticator'; + +beforeEach(() => { + jest.clearAllMocks(); +}); +jest.mock('./helpers', () => ({ + createTokenValidator() { + return async () => ({ sub: 's', email: 'e' }); + }, +})); + +describe('GcpIapProvider', () => { + it('should find default JWT header', async () => { + const ctx = await gcpIapAuthenticator.initialize({ + config: mockServices.rootConfig({ data: { audience: 'my-audience' } }), + }); + await expect( + gcpIapAuthenticator.authenticate( + { + req: { + header(name: string) { + return name === 'x-goog-iap-jwt-assertion' + ? 'my-token' + : undefined; + }, + } as Request, + }, + ctx, + ), + ).resolves.toEqual({ result: { iapToken: { sub: 's', email: 'e' } } }); + }); + + it('should find custom JWT header', async () => { + const jwtHeader = 'x-custom-header'; + const ctx = await gcpIapAuthenticator.initialize({ + config: mockServices.rootConfig({ + data: { audience: 'my-audience', jwtHeader }, + }), + }); + await expect( + gcpIapAuthenticator.authenticate( + { + req: { + header(name: string) { + return name === jwtHeader ? 'my-token' : undefined; + }, + } as Request, + }, + ctx, + ), + ).resolves.toEqual({ result: { iapToken: { sub: 's', email: 'e' } } }); + }); + + it('should throw if header is missing', async () => { + const ctx = await gcpIapAuthenticator.initialize({ + config: mockServices.rootConfig({ + data: { audience: 'my-audience' }, + }), + }); + await expect( + gcpIapAuthenticator.authenticate( + { + req: { + header(_name: string) { + return undefined; + }, + } as Request, + }, + ctx, + ), + ).rejects.toThrow('Missing Google IAP header'); + }); +}); diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.ts b/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.ts new file mode 100644 index 0000000000..ca104ded86 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/src/authenticator.ts @@ -0,0 +1,52 @@ +/* + * 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 { AuthenticationError } from '@backstage/errors'; +import { createProxyAuthenticator } from '@backstage/plugin-auth-node'; +import { createTokenValidator } from './helpers'; +import { GcpIapResult } from './types'; + +/** + * The header name used by the IAP. + */ +const DEFAULT_IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion'; + +/** @public */ +export const gcpIapAuthenticator = createProxyAuthenticator({ + defaultProfileTransform: async (result: GcpIapResult) => { + return { profile: { email: result.iapToken.email } }; + }, + async initialize({ config }) { + const audience = config.getString('audience'); + const jwtHeader = + config.getOptionalString('jwtHeader') ?? DEFAULT_IAP_JWT_HEADER; + + const tokenValidator = createTokenValidator(audience); + + return { jwtHeader, tokenValidator }; + }, + async authenticate({ req }, { jwtHeader, tokenValidator }) { + const token = req.header(jwtHeader); + + if (!token || typeof token !== 'string') { + throw new AuthenticationError('Missing Google IAP header'); + } + + const iapToken = await tokenValidator(token); + + return { result: { iapToken } }; + }, +}); diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/helpers.test.ts b/plugins/auth-backend-module-gcp-iap-provider/src/helpers.test.ts new file mode 100644 index 0000000000..4079f8f7d3 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/src/helpers.test.ts @@ -0,0 +1,124 @@ +/* + * 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 { OAuth2Client } from 'google-auth-library'; +import { createTokenValidator } from './helpers'; + +const mockJwt = 'a.b.c'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('helpers', () => { + describe('createTokenValidator', () => { + it('runs the happy path', async () => { + const mockClient = { + getIapPublicKeys: async () => ({ pubkeys: '' }), + verifySignedJwtWithCertsAsync: async () => ({ + getPayload: () => ({ sub: 's', email: 'e@mail.com' }), + }), + }; + const validator = createTokenValidator( + 'a', + mockClient as unknown as OAuth2Client, + ); + await expect(validator(mockJwt)).resolves.toMatchObject({ + sub: 's', + email: 'e@mail.com', + }); + }); + + it('throws if listing keys fail', async () => { + const mockClient = { + getIapPublicKeys: async () => { + throw new Error('NOPE'); + }, + }; + const validator = createTokenValidator( + 'a', + mockClient as unknown as OAuth2Client, + ); + await expect(validator(mockJwt)).rejects.toThrow( + 'Unable to list Google IAP token verification keys, Error: NOPE', + ); + }); + + it('throws if the verifying signature fails', async () => { + const mockClient = { + getIapPublicKeys: async () => ({ pubkeys: '' }), + verifySignedJwtWithCertsAsync: async () => { + throw new Error('NOPE'); + }, + }; + const validator = createTokenValidator( + 'a', + mockClient as unknown as OAuth2Client, + ); + await expect(validator(mockJwt)).rejects.toThrow( + 'Google IAP token verification failed, Error: NOPE', + ); + }); + + it('rejects empty payload', async () => { + const mockClient = { + getIapPublicKeys: async () => ({ pubkeys: '' }), + verifySignedJwtWithCertsAsync: async () => ({ + getPayload: () => undefined, + }), + }; + const validator = createTokenValidator( + 'a', + mockClient as unknown as OAuth2Client, + ); + await expect(validator(mockJwt)).rejects.toThrow( + 'Google IAP token verification failed, token had no payload', + ); + }); + + it('rejects payload without subject', async () => { + const mockClient = { + getIapPublicKeys: async () => ({ pubkeys: '' }), + verifySignedJwtWithCertsAsync: async () => ({ + getPayload: () => ({ email: 'e@mail.com' }), + }), + }; + const validator = createTokenValidator( + 'a', + mockClient as unknown as OAuth2Client, + ); + await expect(validator(mockJwt)).rejects.toThrow( + 'Google IAP token payload is missing subject claim', + ); + }); + + it('rejects payload without email', async () => { + const mockClient = { + getIapPublicKeys: async () => ({ pubkeys: '' }), + verifySignedJwtWithCertsAsync: async () => ({ + getPayload: () => ({ sub: 's' }), + }), + }; + const validator = createTokenValidator( + 'a', + mockClient as unknown as OAuth2Client, + ); + await expect(validator(mockJwt)).rejects.toThrow( + 'Google IAP token payload is missing email claim', + ); + }); + }); +}); diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/helpers.ts b/plugins/auth-backend-module-gcp-iap-provider/src/helpers.ts new file mode 100644 index 0000000000..54db7117f9 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/src/helpers.ts @@ -0,0 +1,67 @@ +/* + * 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 { AuthenticationError } from '@backstage/errors'; +import { OAuth2Client } from 'google-auth-library'; +import { GcpIapTokenInfo } from './types'; + +export function createTokenValidator( + audience: string, + providedClient?: OAuth2Client, +): (token: string) => Promise { + const client = providedClient ?? new OAuth2Client(); + + return async function tokenValidator(token) { + // TODO(freben): Rate limit the public key reads. It may be sensible to + // cache these for some reasonable time rather than asking for the public + // keys on every single sign-in. But since the rate of events here is so + // slow, I decided to keep it simple for now. + const response = await client.getIapPublicKeys().catch(error => { + throw new AuthenticationError( + `Unable to list Google IAP token verification keys, ${error}`, + ); + }); + const ticket = await client + .verifySignedJwtWithCertsAsync(token, response.pubkeys, audience, [ + 'https://cloud.google.com/iap', + ]) + .catch(error => { + throw new AuthenticationError( + `Google IAP token verification failed, ${error}`, + ); + }); + + const payload = ticket.getPayload(); + if (!payload) { + throw new AuthenticationError( + 'Google IAP token verification failed, token had no payload', + ); + } + + if (!payload.sub) { + throw new AuthenticationError( + 'Google IAP token payload is missing subject claim', + ); + } + if (!payload.email) { + throw new AuthenticationError( + 'Google IAP token payload is missing email claim', + ); + } + + return payload as unknown as GcpIapTokenInfo; + }; +} diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/index.ts b/plugins/auth-backend-module-gcp-iap-provider/src/index.ts new file mode 100644 index 0000000000..0cbd54d027 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/src/index.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. + */ + +export { gcpIapAuthenticator } from './authenticator'; +export { authModuleGcpIapProvider as default } from './module'; +export { gcpIapSignInResolvers } from './resolvers'; +export { type GcpIapResult, type GcpIapTokenInfo } from './types'; diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/module.ts b/plugins/auth-backend-module-gcp-iap-provider/src/module.ts new file mode 100644 index 0000000000..90771eecb9 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/src/module.ts @@ -0,0 +1,49 @@ +/* + * 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 { createBackendModule } from '@backstage/backend-plugin-api'; +import { + authProvidersExtensionPoint, + commonSignInResolvers, + createProxyAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { gcpIapAuthenticator } from './authenticator'; +import { gcpIapSignInResolvers } from './resolvers'; + +/** @public */ +export const authModuleGcpIapProvider = createBackendModule({ + pluginId: 'auth', + moduleId: 'gcpIapProvider', + register(reg) { + reg.registerInit({ + deps: { + providers: authProvidersExtensionPoint, + }, + async init({ providers }) { + providers.registerProvider({ + providerId: 'gcpIap', + factory: createProxyAuthProviderFactory({ + authenticator: gcpIapAuthenticator, + signInResolverFactories: { + ...gcpIapSignInResolvers, + ...commonSignInResolvers, + }, + }), + }); + }, + }); + }, +}); diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts b/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts new file mode 100644 index 0000000000..32ab2b6cc7 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts @@ -0,0 +1,49 @@ +/* + * 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 { + createSignInResolverFactory, + SignInInfo, +} from '@backstage/plugin-auth-node'; +import { GcpIapResult } from './types'; + +/** + * Available sign-in resolvers for the Google auth provider. + * + * @public + */ +export namespace gcpIapSignInResolvers { + /** + * Looks up the user by matching their email to the `google.com/email` annotation. + */ + export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({ + create() { + return async (info: SignInInfo, ctx) => { + const email = info.result.iapToken.email; + + if (!email) { + throw new Error('Google IAP sign-in result is missing email'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'google.com/email': email, + }, + }); + }; + }, + }); +} diff --git a/plugins/auth-backend-module-gcp-iap-provider/src/types.ts b/plugins/auth-backend-module-gcp-iap-provider/src/types.ts new file mode 100644 index 0000000000..a967a4d579 --- /dev/null +++ b/plugins/auth-backend-module-gcp-iap-provider/src/types.ts @@ -0,0 +1,50 @@ +/* + * 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 { JsonPrimitive } from '@backstage/types'; + +/** + * The data extracted from an IAP token. + * + * @public + */ +export type GcpIapTokenInfo = { + /** + * The unique, stable identifier for the user. + */ + sub: string; + /** + * User email address. + */ + email: string; + /** + * Other fields. + */ + [key: string]: JsonPrimitive; +}; + +/** + * The result of the initial auth challenge. This is the input to the auth + * callbacks. + * + * @public + */ +export type GcpIapResult = { + /** + * The data extracted from the IAP token header. + */ + iapToken: GcpIapTokenInfo; +}; diff --git a/plugins/auth-backend-module-github-provider/.eslintrc.js b/plugins/auth-backend-module-github-provider/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth-backend-module-github-provider/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend-module-github-provider/CHANGELOG.md b/plugins/auth-backend-module-github-provider/CHANGELOG.md new file mode 100644 index 0000000000..d347796a43 --- /dev/null +++ b/plugins/auth-backend-module-github-provider/CHANGELOG.md @@ -0,0 +1,32 @@ +# @backstage/plugin-auth-backend-module-github-provider + +## 0.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + +## 0.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + +## 0.1.0-next.0 + +### Minor Changes + +- 23af27f5ce79: New module for `@backstage/plugin-auth-backend` that adds a GitHub auth provider. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 diff --git a/plugins/auth-backend-module-github-provider/README.md b/plugins/auth-backend-module-github-provider/README.md new file mode 100644 index 0000000000..7928ce1034 --- /dev/null +++ b/plugins/auth-backend-module-github-provider/README.md @@ -0,0 +1,8 @@ +# Auth Module: GitHub Provider + +This module provides an GitHub auth provider implementation for `@backstage/plugin-auth-backend`. + +## Links + +- [Backstage](https://backstage.io) +- [Repository](https://github.com/backstage/backstage/tree/master/plugins/auth-backend-module-github-provider) diff --git a/plugins/auth-backend-module-github-provider/api-report.md b/plugins/auth-backend-module-github-provider/api-report.md new file mode 100644 index 0000000000..3f2c5030d2 --- /dev/null +++ b/plugins/auth-backend-module-github-provider/api-report.md @@ -0,0 +1,30 @@ +## API Report File for "@backstage/plugin-auth-backend-module-github-provider" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { OAuthAuthenticator } from '@backstage/plugin-auth-node'; +import { OAuthAuthenticatorResult } from '@backstage/plugin-auth-node'; +import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node'; +import { PassportProfile } from '@backstage/plugin-auth-node'; +import { SignInResolverFactory } from '@backstage/plugin-auth-node'; + +// @public (undocumented) +const authModuleGithubProvider: () => BackendFeature; +export default authModuleGithubProvider; + +// @public (undocumented) +export const githubAuthenticator: OAuthAuthenticator< + PassportOAuthAuthenticatorHelper, + PassportProfile +>; + +// @public +export namespace githubSignInResolvers { + const usernameMatchingUserEntityName: SignInResolverFactory< + OAuthAuthenticatorResult, + unknown + >; +} +``` diff --git a/plugins/auth-backend-module-github-provider/catalog-info.yaml b/plugins/auth-backend-module-github-provider/catalog-info.yaml new file mode 100644 index 0000000000..8eda78fdaa --- /dev/null +++ b/plugins/auth-backend-module-github-provider/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth-backend-module-github-provider + title: '@backstage/plugin-auth-backend-module-github-provider' + description: The github-provider backend module for the auth plugin. +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/auth-backend-module-github-provider/dev/index.ts b/plugins/auth-backend-module-github-provider/dev/index.ts new file mode 100644 index 0000000000..d3c18c1d48 --- /dev/null +++ b/plugins/auth-backend-module-github-provider/dev/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); + +backend.add(import('@backstage/plugin-auth-backend')); +backend.add(import('../src')); + +backend.start(); diff --git a/plugins/auth-backend-module-github-provider/package.json b/plugins/auth-backend-module-github-provider/package.json new file mode 100644 index 0000000000..ec57147a4b --- /dev/null +++ b/plugins/auth-backend-module-github-provider/package.json @@ -0,0 +1,41 @@ +{ + "name": "@backstage/plugin-auth-backend-module-github-provider", + "description": "The github-provider backend module for the auth plugin.", + "version": "0.1.0-next.2", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "passport-github2": "^0.1.12" + }, + "devDependencies": { + "@backstage/backend-defaults": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-auth-backend": "workspace:^", + "supertest": "^6.3.3" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/auth-backend-module-github-provider/src/authenticator.test.ts b/plugins/auth-backend-module-github-provider/src/authenticator.test.ts new file mode 100644 index 0000000000..dc5c7208ea --- /dev/null +++ b/plugins/auth-backend-module-github-provider/src/authenticator.test.ts @@ -0,0 +1,142 @@ +/* + * 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 { + PassportOAuthAuthenticatorHelper, + PassportProfile, +} from '@backstage/plugin-auth-node'; +import { githubAuthenticator } from './authenticator'; + +describe('githubAuthenticator', () => { + it('should store access token without expiration as refresh token', async () => { + await expect( + githubAuthenticator.authenticate( + {} as any, + { + authenticate: async _input => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + }, + }), + } as PassportOAuthAuthenticatorHelper, + ), + ).resolves.toEqual({ + fullProfile: { id: 'id' }, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + refreshToken: 'access-token.my-token', + }, + }); + }); + + it('should not use access token as refresh token if it expires', async () => { + await expect( + githubAuthenticator.authenticate( + {} as any, + { + authenticate: async _input => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + expiresInSeconds: 3, + }, + }), + } as PassportOAuthAuthenticatorHelper, + ), + ).resolves.toEqual({ + fullProfile: { id: 'id' }, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + expiresInSeconds: 3, + }, + }); + }); + + it('should not store access token without expiration if a refresh token is provided', async () => { + await expect( + githubAuthenticator.authenticate( + {} as any, + { + authenticate: async _input => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + refreshToken: 'my-refresh-token', + }, + }), + } as PassportOAuthAuthenticatorHelper, + ), + ).resolves.toEqual({ + fullProfile: { id: 'id' }, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + refreshToken: 'my-refresh-token', + }, + }); + }); + + it('should refresh with access token', async () => { + await expect( + githubAuthenticator.refresh( + { + refreshToken: 'access-token.my-token', + req: {} as any, + scope: 'user:read', + }, + { + fetchProfile: async _input => ({ id: 'id' } as PassportProfile), + } as PassportOAuthAuthenticatorHelper, + ), + ).resolves.toEqual({ + fullProfile: { id: 'id' }, + session: { + accessToken: 'my-token', + scope: 'user:read', + tokenType: 'bearer', + refreshToken: 'access-token.my-token', + }, + }); + }); + + it('should refresh with refresh token', async () => { + const res = {}; + await expect( + githubAuthenticator.refresh( + { + refreshToken: 'my-refresh-token', + req: {} as any, + scope: 'user:read', + }, + { + refresh: async _input => res as any, + } as PassportOAuthAuthenticatorHelper, + ), + ).resolves.toBe(res); + }); +}); diff --git a/plugins/auth-backend-module-github-provider/src/authenticator.ts b/plugins/auth-backend-module-github-provider/src/authenticator.ts new file mode 100644 index 0000000000..f59ac3d727 --- /dev/null +++ b/plugins/auth-backend-module-github-provider/src/authenticator.ts @@ -0,0 +1,125 @@ +/* + * 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 { Strategy as GithubStrategy } from 'passport-github2'; +import { + createOAuthAuthenticator, + PassportOAuthAuthenticatorHelper, + PassportOAuthDoneCallback, + PassportProfile, +} from '@backstage/plugin-auth-node'; + +const ACCESS_TOKEN_PREFIX = 'access-token.'; + +/** @public */ +export const githubAuthenticator = createOAuthAuthenticator({ + defaultProfileTransform: + PassportOAuthAuthenticatorHelper.defaultProfileTransform, + initialize({ callbackUrl, config }) { + const clientId = config.getString('clientId'); + const clientSecret = config.getString('clientSecret'); + const enterpriseInstanceUrl = config + .getOptionalString('enterpriseInstanceUrl') + ?.replace(/\/$/, ''); + const authorizationUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/authorize` + : undefined; + const tokenUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/access_token` + : undefined; + const userProfileUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/api/v3/user` + : undefined; + + return PassportOAuthAuthenticatorHelper.from( + new GithubStrategy( + { + clientID: clientId, + clientSecret: clientSecret, + callbackURL: callbackUrl, + tokenURL: tokenUrl, + userProfileURL: userProfileUrl, + authorizationURL: authorizationUrl, + }, + ( + accessToken: string, + refreshToken: string, + params: any, + fullProfile: PassportProfile, + done: PassportOAuthDoneCallback, + ) => { + done( + undefined, + { fullProfile, params, accessToken }, + { refreshToken }, + ); + }, + ), + ); + }, + + async start(input, helper) { + return helper.start(input, { + accessType: 'offline', + prompt: 'consent', + }); + }, + + async authenticate(input, helper) { + const { fullProfile, session } = await helper.authenticate(input); + + // If we do not have a real refresh token and we have a non-expiring + // access token, then we use that as our refresh token. + if (!session.refreshToken && !session.expiresInSeconds) { + session.refreshToken = ACCESS_TOKEN_PREFIX + session.accessToken; + } + return { fullProfile, session }; + }, + + async refresh(input, helper) { + // This is the OAuth App flow. A non-expiring access token is stored in the + // refresh token cookie. We use that token to fetch the user profile and + // refresh the Backstage session when needed. + if (input.refreshToken?.startsWith(ACCESS_TOKEN_PREFIX)) { + const accessToken = input.refreshToken.slice(ACCESS_TOKEN_PREFIX.length); + + const fullProfile = await helper + .fetchProfile(accessToken) + .catch(error => { + if (error.oauthError?.statusCode === 401) { + throw new Error('Invalid access token'); + } + throw error; + }); + + return { + fullProfile, + session: { + accessToken, + tokenType: 'bearer', + scope: input.scope, + refreshToken: input.refreshToken, + // No expiration + }, + }; + } + + // This is the App flow, which is close to a standard OAuth refresh flow. It has a + // pretty long session expiration, and it also ignores the requested scope, instead + // just allowing access to whatever is configured as part of the app installation. + return helper.refresh(input); + }, +}); diff --git a/plugins/auth-backend-module-github-provider/src/index.ts b/plugins/auth-backend-module-github-provider/src/index.ts new file mode 100644 index 0000000000..646ba7c92a --- /dev/null +++ b/plugins/auth-backend-module-github-provider/src/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** + * The github-provider backend module for the auth plugin. + * + * @packageDocumentation + */ + +export { githubAuthenticator } from './authenticator'; +export { authModuleGithubProvider as default } from './module'; +export { githubSignInResolvers } from './resolvers'; diff --git a/plugins/auth-backend-module-github-provider/src/module.test.ts b/plugins/auth-backend-module-github-provider/src/module.test.ts new file mode 100644 index 0000000000..437e002bc3 --- /dev/null +++ b/plugins/auth-backend-module-github-provider/src/module.test.ts @@ -0,0 +1,77 @@ +/* + * 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 { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { authModuleGithubProvider } from './module'; +import request from 'supertest'; +import { decodeOAuthState } from '@backstage/plugin-auth-node'; + +describe('authModuleGithubProvider', () => { + it('should start', async () => { + const { server } = await startTestBackend({ + features: [ + import('@backstage/plugin-auth-backend'), + authModuleGithubProvider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + github: { + development: { + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + }, + }, + }, + }, + }, + }), + ], + }); + + const agent = request.agent(server); + + const res = await agent.get('/api/auth/github/start?env=development'); + + expect(res.status).toEqual(302); + + const nonceCookie = agent.jar.getCookie('github-nonce', { + domain: 'localhost', + path: '/api/auth/github/handler', + script: false, + secure: false, + }); + expect(nonceCookie).toBeDefined(); + + const startUrl = new URL(res.get('location')); + expect(startUrl.origin).toBe('https://github.com'); + expect(startUrl.pathname).toBe('/login/oauth/authorize'); + expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + response_type: 'code', + client_id: 'my-client-id', + redirect_uri: `http://localhost:${server.port()}/api/auth/github/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-github-provider/src/module.ts b/plugins/auth-backend-module-github-provider/src/module.ts new file mode 100644 index 0000000000..54286c94a7 --- /dev/null +++ b/plugins/auth-backend-module-github-provider/src/module.ts @@ -0,0 +1,48 @@ +/* + * 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 { createBackendModule } from '@backstage/backend-plugin-api'; +import { + authProvidersExtensionPoint, + commonSignInResolvers, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { githubAuthenticator } from './authenticator'; +import { githubSignInResolvers } from './resolvers'; + +/** @public */ +export const authModuleGithubProvider = createBackendModule({ + pluginId: 'auth', + moduleId: 'github-provider', + register(reg) { + reg.registerInit({ + deps: { + providers: authProvidersExtensionPoint, + }, + async init({ providers }) { + providers.registerProvider({ + providerId: 'github', + factory: createOAuthProviderFactory({ + authenticator: githubAuthenticator, + signInResolverFactories: { + ...githubSignInResolvers, + ...commonSignInResolvers, + }, + }), + }); + }, + }); + }, +}); diff --git a/plugins/auth-backend-module-github-provider/src/resolvers.ts b/plugins/auth-backend-module-github-provider/src/resolvers.ts new file mode 100644 index 0000000000..496080a33c --- /dev/null +++ b/plugins/auth-backend-module-github-provider/src/resolvers.ts @@ -0,0 +1,50 @@ +/* + * 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 { + createSignInResolverFactory, + OAuthAuthenticatorResult, + PassportProfile, + SignInInfo, +} from '@backstage/plugin-auth-node'; + +/** + * Available sign-in resolvers for the GitHub auth provider. + * + * @public + */ +export namespace githubSignInResolvers { + /** + * Looks up the user by matching their GitHub username to the entity name. + */ + export const usernameMatchingUserEntityName = createSignInResolverFactory({ + create() { + return async ( + info: SignInInfo>, + ctx, + ) => { + const { fullProfile } = info.result; + + const userId = fullProfile.username; + if (!userId) { + throw new Error(`GitHub user profile does not contain a username`); + } + + return ctx.signInWithCatalogUser({ entityRef: { name: userId } }); + }; + }, + }); +} diff --git a/plugins/auth-backend-module-gitlab-provider/.eslintrc.js b/plugins/auth-backend-module-gitlab-provider/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md new file mode 100644 index 0000000000..54c2240c17 --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md @@ -0,0 +1,23 @@ +# @backstage/plugin-auth-backend-module-gitlab-provider + +## 0.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + +## 0.1.0-next.0 + +### Minor Changes + +- 080cc7794700: New module for `@backstage/plugin-auth-backend` that adds a GitLab auth provider. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 diff --git a/plugins/auth-backend-module-gitlab-provider/README.md b/plugins/auth-backend-module-gitlab-provider/README.md new file mode 100644 index 0000000000..b9eb561990 --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/README.md @@ -0,0 +1,8 @@ +# Auth Module: GitLab Provider + +This module provides an GitLab auth provider implementation for `@backstage/plugin-auth-backend`. + +## Links + +- [Repository](https://gitlab.com/backstage/backstage/tree/master/plugins/auth-backend-module-gitlab-provider) +- [Backstage Project Homepage](https://backstage.io) diff --git a/plugins/auth-backend-module-gitlab-provider/api-report.md b/plugins/auth-backend-module-gitlab-provider/api-report.md new file mode 100644 index 0000000000..d6ec7ee82a --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/api-report.md @@ -0,0 +1,30 @@ +## API Report File for "@backstage/plugin-auth-backend-module-gitlab-provider" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { OAuthAuthenticator } from '@backstage/plugin-auth-node'; +import { OAuthAuthenticatorResult } from '@backstage/plugin-auth-node'; +import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node'; +import { PassportProfile } from '@backstage/plugin-auth-node'; +import { SignInResolverFactory } from '@backstage/plugin-auth-node'; + +// @public (undocumented) +const authModuleGitlabProvider: () => BackendFeature; +export default authModuleGitlabProvider; + +// @public (undocumented) +export const gitlabAuthenticator: OAuthAuthenticator< + PassportOAuthAuthenticatorHelper, + PassportProfile +>; + +// @public +export namespace gitlabSignInResolvers { + const usernameMatchingUserEntityName: SignInResolverFactory< + OAuthAuthenticatorResult, + unknown + >; +} +``` diff --git a/plugins/auth-backend-module-gitlab-provider/catalog-info.yaml b/plugins/auth-backend-module-gitlab-provider/catalog-info.yaml new file mode 100644 index 0000000000..55086778e4 --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth-backend-module-gitlab-provider + title: '@backstage/plugin-auth-backend-module-gitlab-provider' + description: The gitlab-provider backend module for the auth plugin. +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/auth-backend-module-gitlab-provider/config.d.ts b/plugins/auth-backend-module-gitlab-provider/config.d.ts new file mode 100644 index 0000000000..d7f04d872a --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/config.d.ts @@ -0,0 +1,34 @@ +/* + * 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 { + auth?: { + providers?: { + /** @visibility frontend */ + gitlab?: { + [authEnv: string]: { + clientId: string; + /** + * @visibility secret + */ + clientSecret: string; + audience?: string; + callbackUrl?: string; + }; + }; + }; + }; +} diff --git a/plugins/auth-backend-module-gitlab-provider/dev/index.ts b/plugins/auth-backend-module-gitlab-provider/dev/index.ts new file mode 100644 index 0000000000..d3c18c1d48 --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/dev/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); + +backend.add(import('@backstage/plugin-auth-backend')); +backend.add(import('../src')); + +backend.start(); diff --git a/plugins/auth-backend-module-gitlab-provider/package.json b/plugins/auth-backend-module-gitlab-provider/package.json new file mode 100644 index 0000000000..090dfe6dfb --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/package.json @@ -0,0 +1,45 @@ +{ + "name": "@backstage/plugin-auth-backend-module-gitlab-provider", + "description": "The gitlab-provider backend module for the auth plugin.", + "version": "0.1.0-next.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "express": "^4.18.2", + "passport": "^0.6.0", + "passport-gitlab2": "^5.0.0" + }, + "devDependencies": { + "@backstage/backend-defaults": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-auth-backend": "workspace:^", + "supertest": "^6.3.3" + }, + "configSchema": "config.d.ts", + "files": [ + "dist", + "config.d.ts" + ] +} diff --git a/plugins/auth-backend-module-gitlab-provider/src/authenticator.ts b/plugins/auth-backend-module-gitlab-provider/src/authenticator.ts new file mode 100644 index 0000000000..9cd5857b0d --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/src/authenticator.ts @@ -0,0 +1,77 @@ +/* + * 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 { Strategy as GitlabStrategy } from 'passport-gitlab2'; +import { + createOAuthAuthenticator, + PassportOAuthAuthenticatorHelper, + PassportOAuthDoneCallback, + PassportProfile, +} from '@backstage/plugin-auth-node'; + +/** @public */ +export const gitlabAuthenticator = createOAuthAuthenticator({ + defaultProfileTransform: + PassportOAuthAuthenticatorHelper.defaultProfileTransform, + initialize({ callbackUrl, config }) { + const clientId = config.getString('clientId'); + const clientSecret = config.getString('clientSecret'); + const baseUrl = + config.getOptionalString('audience') || 'https://gitlab.com'; + + return PassportOAuthAuthenticatorHelper.from( + new GitlabStrategy( + { + clientID: clientId, + clientSecret: clientSecret, + callbackURL: callbackUrl, + baseURL: baseUrl, + authorizationURL: `${baseUrl}/oauth/authorize`, + tokenURL: `${baseUrl}/oauth/token`, + profileURL: `${baseUrl}/api/v4/user`, + }, + ( + accessToken: string, + refreshToken: string, + params: any, + fullProfile: PassportProfile, + done: PassportOAuthDoneCallback, + ) => { + done( + undefined, + { fullProfile, params, accessToken }, + { refreshToken }, + ); + }, + ), + ); + }, + + async start(input, helper) { + return helper.start(input, { + accessType: 'offline', + prompt: 'consent', + }); + }, + + async authenticate(input, helper) { + return helper.authenticate(input); + }, + + async refresh(input, helper) { + return helper.refresh(input); + }, +}); diff --git a/plugins/auth-backend-module-gitlab-provider/src/index.ts b/plugins/auth-backend-module-gitlab-provider/src/index.ts new file mode 100644 index 0000000000..c581b76005 --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/src/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** + * The gitlab-provider backend module for the auth plugin. + * + * @packageDocumentation + */ + +export { gitlabAuthenticator } from './authenticator'; +export { authModuleGitlabProvider as default } from './module'; +export { gitlabSignInResolvers } from './resolvers'; diff --git a/plugins/auth-backend-module-gitlab-provider/src/module.test.ts b/plugins/auth-backend-module-gitlab-provider/src/module.test.ts new file mode 100644 index 0000000000..da34d64d03 --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/src/module.test.ts @@ -0,0 +1,78 @@ +/* + * 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 { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { authModuleGitlabProvider } from './module'; +import request from 'supertest'; +import { decodeOAuthState } from '@backstage/plugin-auth-node'; + +describe('authModuleGitlabProvider', () => { + it('should start', async () => { + const { server } = await startTestBackend({ + features: [ + import('@backstage/plugin-auth-backend'), + authModuleGitlabProvider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + gitlab: { + development: { + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + }, + }, + }, + }, + }, + }), + ], + }); + + const agent = request.agent(server); + + const res = await agent.get('/api/auth/gitlab/start?env=development'); + + expect(res.status).toEqual(302); + + const nonceCookie = agent.jar.getCookie('gitlab-nonce', { + domain: 'localhost', + path: '/api/auth/gitlab/handler', + script: false, + secure: false, + }); + expect(nonceCookie).toBeDefined(); + + const startUrl = new URL(res.get('location')); + expect(startUrl.origin).toBe('https://gitlab.com'); + expect(startUrl.pathname).toBe('/oauth/authorize'); + expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + response_type: 'code', + scope: 'read_user', + client_id: 'my-client-id', + redirect_uri: `http://localhost:${server.port()}/api/auth/gitlab/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-gitlab-provider/src/module.ts b/plugins/auth-backend-module-gitlab-provider/src/module.ts new file mode 100644 index 0000000000..790d522494 --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/src/module.ts @@ -0,0 +1,48 @@ +/* + * 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 { createBackendModule } from '@backstage/backend-plugin-api'; +import { + authProvidersExtensionPoint, + commonSignInResolvers, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { gitlabAuthenticator } from './authenticator'; +import { gitlabSignInResolvers } from './resolvers'; + +/** @public */ +export const authModuleGitlabProvider = createBackendModule({ + pluginId: 'auth', + moduleId: 'gitlab-provider', + register(reg) { + reg.registerInit({ + deps: { + providers: authProvidersExtensionPoint, + }, + async init({ providers }) { + providers.registerProvider({ + providerId: 'gitlab', + factory: createOAuthProviderFactory({ + authenticator: gitlabAuthenticator, + signInResolverFactories: { + ...gitlabSignInResolvers, + ...commonSignInResolvers, + }, + }), + }); + }, + }); + }, +}); diff --git a/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts b/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts new file mode 100644 index 0000000000..755ed08aa0 --- /dev/null +++ b/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts @@ -0,0 +1,50 @@ +/* + * 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 { + createSignInResolverFactory, + OAuthAuthenticatorResult, + PassportProfile, + SignInInfo, +} from '@backstage/plugin-auth-node'; + +/** + * Available sign-in resolvers for the GitLab auth provider. + * + * @public + */ +export namespace gitlabSignInResolvers { + /** + * Looks up the user by matching their GitLab username to the entity name. + */ + export const usernameMatchingUserEntityName = createSignInResolverFactory({ + create() { + return async ( + info: SignInInfo>, + ctx, + ) => { + const { result } = info; + + const id = result.fullProfile.username; + if (!id) { + throw new Error(`GitLab user profile does not contain a username`); + } + + return ctx.signInWithCatalogUser({ entityRef: { name: id } }); + }; + }, + }); +} diff --git a/plugins/auth-backend/src/providers/gitlab/types.d.ts b/plugins/auth-backend-module-gitlab-provider/src/types.d.ts similarity index 100% rename from plugins/auth-backend/src/providers/gitlab/types.d.ts rename to plugins/auth-backend-module-gitlab-provider/src/types.d.ts diff --git a/plugins/auth-backend-module-google-provider/.eslintrc.js b/plugins/auth-backend-module-google-provider/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth-backend-module-google-provider/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend-module-google-provider/CHANGELOG.md b/plugins/auth-backend-module-google-provider/CHANGELOG.md new file mode 100644 index 0000000000..e74380aa3a --- /dev/null +++ b/plugins/auth-backend-module-google-provider/CHANGELOG.md @@ -0,0 +1,29 @@ +# @backstage/plugin-auth-backend-module-google-provider + +## 0.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + +## 0.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + +## 0.1.0-next.0 + +### Minor Changes + +- 8513cd7d00e3: New module for `@backstage/plugin-auth-backend` that adds a Google auth provider. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 diff --git a/plugins/auth-backend-module-google-provider/README.md b/plugins/auth-backend-module-google-provider/README.md new file mode 100644 index 0000000000..633dd13717 --- /dev/null +++ b/plugins/auth-backend-module-google-provider/README.md @@ -0,0 +1,8 @@ +# Auth Module: Google Provider + +This module provides a Google auth provider implementation for `@backstage/plugin-auth-backend`. + +## Links + +- [Backstage](https://backstage.io) +- [Repository](https://github.com/backstage/backstage/tree/master/plugins/auth-backend-module-google-provider) diff --git a/plugins/auth-backend-module-google-provider/api-report.md b/plugins/auth-backend-module-google-provider/api-report.md new file mode 100644 index 0000000000..7fab5d33a9 --- /dev/null +++ b/plugins/auth-backend-module-google-provider/api-report.md @@ -0,0 +1,32 @@ +## API Report File for "@backstage/plugin-auth-backend-module-google-provider" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { OAuthAuthenticator } from '@backstage/plugin-auth-node'; +import { OAuthAuthenticatorResult } from '@backstage/plugin-auth-node'; +import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node'; +import { PassportProfile } from '@backstage/plugin-auth-node'; +import { SignInResolverFactory } from '@backstage/plugin-auth-node'; + +// @public (undocumented) +const authModuleGoogleProvider: () => BackendFeature; +export default authModuleGoogleProvider; + +// @public (undocumented) +export const googleAuthenticator: OAuthAuthenticator< + PassportOAuthAuthenticatorHelper, + PassportProfile +>; + +// @public +export namespace googleSignInResolvers { + const emailMatchingUserEntityAnnotation: SignInResolverFactory< + OAuthAuthenticatorResult, + unknown + >; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/auth-backend-module-google-provider/catalog-info.yaml b/plugins/auth-backend-module-google-provider/catalog-info.yaml new file mode 100644 index 0000000000..0897a12168 --- /dev/null +++ b/plugins/auth-backend-module-google-provider/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth-backend-module-google-provider + title: '@backstage/plugin-auth-backend-module-google-provider' + description: A Google auth provider module for the Backstage auth backend +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/auth-backend-module-google-provider/config.d.ts b/plugins/auth-backend-module-google-provider/config.d.ts new file mode 100644 index 0000000000..6a1716c5bf --- /dev/null +++ b/plugins/auth-backend-module-google-provider/config.d.ts @@ -0,0 +1,33 @@ +/* + * 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 auth plugin */ + auth?: { + providers?: { + google?: { + [authEnv: string]: { + clientId: string; + /** + * @visibility secret + */ + clientSecret: string; + callbackUrl?: string; + }; + }; + }; + }; +} diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json new file mode 100644 index 0000000000..9c8f6201e2 --- /dev/null +++ b/plugins/auth-backend-module-google-provider/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-auth-backend-module-google-provider", + "description": "A Google auth provider module for the Backstage auth backend", + "version": "0.1.0-next.2", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/auth-backend-module-google-provider" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "google-auth-library": "^8.0.0", + "passport-google-oauth20": "^2.0.0" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-auth-backend": "workspace:^", + "@types/passport-google-oauth20": "^2.0.3", + "msw": "^1.0.0", + "supertest": "^6.1.3" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/auth-backend-module-google-provider/src/authenticator.ts b/plugins/auth-backend-module-google-provider/src/authenticator.ts new file mode 100644 index 0000000000..a428519fee --- /dev/null +++ b/plugins/auth-backend-module-google-provider/src/authenticator.ts @@ -0,0 +1,86 @@ +/* + * 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 { + PassportOAuthAuthenticatorHelper, + PassportOAuthDoneCallback, + PassportProfile, + createOAuthAuthenticator, +} from '@backstage/plugin-auth-node'; +import { OAuth2Client } from 'google-auth-library'; +import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; + +/** @public */ +export const googleAuthenticator = createOAuthAuthenticator({ + defaultProfileTransform: + PassportOAuthAuthenticatorHelper.defaultProfileTransform, + initialize({ callbackUrl, config }) { + const clientId = config.getString('clientId'); + const clientSecret = config.getString('clientSecret'); + + return PassportOAuthAuthenticatorHelper.from( + new GoogleStrategy( + { + clientID: clientId, + clientSecret: clientSecret, + callbackURL: callbackUrl, + passReqToCallback: false, + }, + ( + accessToken: string, + refreshToken: string, + params: any, + fullProfile: PassportProfile, + done: PassportOAuthDoneCallback, + ) => { + done( + undefined, + { + fullProfile, + params, + accessToken, + }, + { + refreshToken, + }, + ); + }, + ), + ); + }, + + async start(input, helper) { + return helper.start(input, { + accessType: 'offline', + prompt: 'consent', + }); + }, + + async authenticate(input, helper) { + return helper.authenticate(input); + }, + + async refresh(input, helper) { + return helper.refresh(input); + }, + + async logout(input) { + if (input.refreshToken) { + const oauthClient = new OAuth2Client(); + await oauthClient.revokeToken(input.refreshToken); + } + }, +}); diff --git a/plugins/auth-backend-module-google-provider/src/index.ts b/plugins/auth-backend-module-google-provider/src/index.ts new file mode 100644 index 0000000000..96d3db9821 --- /dev/null +++ b/plugins/auth-backend-module-google-provider/src/index.ts @@ -0,0 +1,19 @@ +/* + * 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 { googleAuthenticator } from './authenticator'; +export { authModuleGoogleProvider as default } from './module'; +export { googleSignInResolvers } from './resolvers'; diff --git a/plugins/auth-backend-module-google-provider/src/module.test.ts b/plugins/auth-backend-module-google-provider/src/module.test.ts new file mode 100644 index 0000000000..59066acd4c --- /dev/null +++ b/plugins/auth-backend-module-google-provider/src/module.test.ts @@ -0,0 +1,79 @@ +/* + * 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 { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { authModuleGoogleProvider } from './module'; +import request from 'supertest'; +import { decodeOAuthState } from '@backstage/plugin-auth-node'; + +describe('authModuleGoogleProvider', () => { + it('should start', async () => { + const { server } = await startTestBackend({ + features: [ + import('@backstage/plugin-auth-backend'), + authModuleGoogleProvider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + google: { + development: { + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + }, + }, + }, + }, + }, + }), + ], + }); + + const agent = request.agent(server); + + const res = await agent.get('/api/auth/google/start?env=development'); + + expect(res.status).toBe(302); + + const nonceCookie = agent.jar.getCookie('google-nonce', { + domain: 'localhost', + path: '/api/auth/google/handler', + script: false, + secure: false, + }); + expect(nonceCookie).toBeDefined(); + + const startUrl = new URL(res.get('location')); + expect(startUrl.origin).toBe('https://accounts.google.com'); + expect(startUrl.pathname).toBe('/o/oauth2/v2/auth'); + expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + access_type: 'offline', + prompt: 'consent', + response_type: 'code', + client_id: 'my-client-id', + redirect_uri: `http://localhost:${server.port()}/api/auth/google/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-google-provider/src/module.ts b/plugins/auth-backend-module-google-provider/src/module.ts new file mode 100644 index 0000000000..0354f99880 --- /dev/null +++ b/plugins/auth-backend-module-google-provider/src/module.ts @@ -0,0 +1,49 @@ +/* + * 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 { createBackendModule } from '@backstage/backend-plugin-api'; +import { + authProvidersExtensionPoint, + commonSignInResolvers, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { googleAuthenticator } from './authenticator'; +import { googleSignInResolvers } from './resolvers'; + +/** @public */ +export const authModuleGoogleProvider = createBackendModule({ + pluginId: 'auth', + moduleId: 'googleProvider', + register(reg) { + reg.registerInit({ + deps: { + providers: authProvidersExtensionPoint, + }, + async init({ providers }) { + providers.registerProvider({ + providerId: 'google', + factory: createOAuthProviderFactory({ + authenticator: googleAuthenticator, + signInResolverFactories: { + ...googleSignInResolvers, + ...commonSignInResolvers, + }, + }), + }); + }, + }); + }, +}); diff --git a/plugins/auth-backend-module-google-provider/src/resolvers.ts b/plugins/auth-backend-module-google-provider/src/resolvers.ts new file mode 100644 index 0000000000..b19fc4d49f --- /dev/null +++ b/plugins/auth-backend-module-google-provider/src/resolvers.ts @@ -0,0 +1,53 @@ +/* + * 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 { + createSignInResolverFactory, + OAuthAuthenticatorResult, + PassportProfile, + SignInInfo, +} from '@backstage/plugin-auth-node'; + +/** + * Available sign-in resolvers for the Google auth provider. + * + * @public + */ +export namespace googleSignInResolvers { + /** + * Looks up the user by matching their email to the `google.com/email` annotation. + */ + export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({ + create() { + return async ( + info: SignInInfo>, + ctx, + ) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('Google profile contained no email'); + } + + return ctx.signInWithCatalogUser({ + annotations: { + 'google.com/email': profile.email, + }, + }); + }; + }, + }); +} diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 478570bb31..f3b0c5b87c 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,92 @@ # @backstage/plugin-auth-backend +## 0.18.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.1.0-next.2 + - @backstage/plugin-auth-backend-module-github-provider@0.1.0-next.2 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.0-next.1 + - @backstage/plugin-auth-backend-module-google-provider@0.1.0-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.18.9-next.1 + +### Patch Changes + +- 080cc7794700: Migrated the GitLab auth provider to be implemented using the new `@backstage/plugin-auth-backend-module-gitlab-provider` module. +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.0-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.0-next.1 + - @backstage/plugin-auth-backend-module-google-provider@0.1.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.1.0-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.18.8-next.0 + +### Patch Changes + +- 7944d43f4790: Added `authPlugin` export for the new backend system. The plugin does not include any built-in auth providers, they must instead be added by installing additional modules, for example `authModuleGoogleProvider` from `@backstage/plugin-auth-backend-module-google-provider`. +- 8513cd7d00e3: Deprecated several exports that are now available from `@backstage/plugin-auth-node` instead. +- 7944d43f4790: Added the ability to disable the built-in auth providers by passing `disableDefaultProviderFactories` to `createRouter`. +- 7944d43f4790: The algorithm used when generating Backstage tokens can be configured via `auth.identityTokenAlgorithm`. +- Updated dependencies + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.1.0-next.0 + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/plugin-auth-backend-module-google-provider@0.1.0-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## 0.18.6 + +### Patch Changes + +- 16452cd007ae: Updated `frameHandler` to return `undefined` when using the redirect flow instead of returning `postMessageReponse` which was causing errors +- 9dad4b0e61bd: Updated config schema to match what was being used in code +- bb70a9c3886a: Add frontend visibility to provider objects in `auth` config. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.18.6-next.2 + +### Patch Changes + +- 16452cd007ae: Updated `frameHandler` to return `undefined` when using the redirect flow instead of returning `postMessageReponse` which was causing errors +- bb70a9c3886a: Add frontend visibility to provider objects in `auth` config. +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + ## 0.18.6-next.1 ### Patch Changes diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 80db2e0b71..6372c2ee60 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -5,98 +5,74 @@ ```ts /// -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; +import { AuthProviderConfig as AuthProviderConfig_2 } from '@backstage/plugin-auth-node'; +import { AuthProviderFactory as AuthProviderFactory_2 } from '@backstage/plugin-auth-node'; +import { AuthProviderRouteHandlers as AuthProviderRouteHandlers_2 } from '@backstage/plugin-auth-node'; +import { AuthResolverCatalogUserQuery as AuthResolverCatalogUserQuery_2 } from '@backstage/plugin-auth-node'; +import { AuthResolverContext as AuthResolverContext_2 } from '@backstage/plugin-auth-node'; +import { BackendFeature } from '@backstage/backend-plugin-api'; import { BackstageSignInResult } from '@backstage/plugin-auth-node'; import { CacheService } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; +import { ClientAuthResponse } from '@backstage/plugin-auth-node'; import { Config } from '@backstage/config'; +import { CookieConfigurer as CookieConfigurer_2 } from '@backstage/plugin-auth-node'; +import { decodeOAuthState } from '@backstage/plugin-auth-node'; +import { encodeOAuthState } from '@backstage/plugin-auth-node'; import { Entity } from '@backstage/catalog-model'; import express from 'express'; -import { GetEntitiesRequest } from '@backstage/catalog-client'; +import { GcpIapResult as GcpIapResult_2 } from '@backstage/plugin-auth-backend-module-gcp-iap-provider'; +import { GcpIapTokenInfo as GcpIapTokenInfo_2 } from '@backstage/plugin-auth-backend-module-gcp-iap-provider'; import { IncomingHttpHeaders } from 'http'; -import { JsonValue } from '@backstage/types'; -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { OAuthEnvironmentHandler as OAuthEnvironmentHandler_2 } from '@backstage/plugin-auth-node'; +import { OAuthState as OAuthState_2 } from '@backstage/plugin-auth-node'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { prepareBackstageIdentityResponse as prepareBackstageIdentityResponse_2 } from '@backstage/plugin-auth-node'; import { Profile } from 'passport'; +import { ProfileInfo as ProfileInfo_2 } from '@backstage/plugin-auth-node'; +import { SignInInfo as SignInInfo_2 } from '@backstage/plugin-auth-node'; +import { SignInResolver as SignInResolver_2 } from '@backstage/plugin-auth-node'; import { TokenManager } from '@backstage/backend-common'; +import { TokenParams as TokenParams_2 } from '@backstage/plugin-auth-node'; import { TokenSet } from 'openid-client'; import { UserEntity } from '@backstage/catalog-model'; import { UserinfoResponse } from 'openid-client'; +import { WebMessageResponse as WebMessageResponse_2 } from '@backstage/plugin-auth-node'; -// @public +// @public @deprecated export type AuthHandler = ( input: TAuthResult, context: AuthResolverContext, ) => Promise; -// @public +// @public @deprecated export type AuthHandlerResult = { profile: ProfileInfo; }; -// @public (undocumented) -export type AuthProviderConfig = { - baseUrl: string; - appUrl: string; - isOriginAllowed: (origin: string) => boolean; - cookieConfigurer?: CookieConfigurer; -}; - -// @public (undocumented) -export type AuthProviderFactory = (options: { - providerId: string; - globalConfig: AuthProviderConfig; - config: Config; - logger: Logger; - resolverContext: AuthResolverContext; -}) => AuthProviderRouteHandlers; - // @public -export interface AuthProviderRouteHandlers { - frameHandler(req: express.Request, res: express.Response): Promise; - logout?(req: express.Request, res: express.Response): Promise; - refresh?(req: express.Request, res: express.Response): Promise; - start(req: express.Request, res: express.Response): Promise; -} +const authPlugin: () => BackendFeature; +export default authPlugin; -// @public -export type AuthResolverCatalogUserQuery = - | { - entityRef: - | string - | { - kind?: string; - namespace?: string; - name: string; - }; - } - | { - annotations: Record; - } - | { - filter: Exclude; - }; +// @public @deprecated (undocumented) +export type AuthProviderConfig = AuthProviderConfig_2; -// @public -export type AuthResolverContext = { - issueToken(params: TokenParams): Promise<{ - token: string; - }>; - findCatalogUser(query: AuthResolverCatalogUserQuery): Promise<{ - entity: Entity; - }>; - signInWithCatalogUser( - query: AuthResolverCatalogUserQuery, - ): Promise; -}; +// @public @deprecated (undocumented) +export type AuthProviderFactory = AuthProviderFactory_2; -// @public (undocumented) -export type AuthResponse = { - providerInfo: ProviderInfo; - profile: ProfileInfo; - backstageIdentity?: BackstageIdentityResponse; -}; +// @public @deprecated (undocumented) +export type AuthProviderRouteHandlers = AuthProviderRouteHandlers_2; + +// @public @deprecated (undocumented) +export type AuthResolverCatalogUserQuery = AuthResolverCatalogUserQuery_2; + +// @public @deprecated (undocumented) +export type AuthResolverContext = AuthResolverContext_2; + +// @public @deprecated (undocumented) +export type AuthResponse = ClientAuthResponse; // @public (undocumented) export type AwsAlbResult = { @@ -151,7 +127,7 @@ export class CatalogIdentityClient { findUser(query: { annotations: Record }): Promise; resolveCatalogMembership(query: { entityRefs: string[]; - logger?: Logger; + logger?: LoggerService; }): Promise; } @@ -191,18 +167,8 @@ export type CloudflareAccessResult = { token: string; }; -// @public -export type CookieConfigurer = (ctx: { - providerId: string; - baseUrl: string; - callbackUrl: string; - appOrigin: string; -}) => { - domain: string; - path: string; - secure: boolean; - sameSite?: 'none' | 'lax' | 'strict'; -}; +// @public @deprecated (undocumented) +export type CookieConfigurer = CookieConfigurer_2; // @public export function createAuthProviderIntegration< @@ -235,23 +201,17 @@ export type EasyAuthResult = { accessToken?: string; }; -// @public (undocumented) -export const encodeState: (state: OAuthState) => string; +// @public @deprecated (undocumented) +export const encodeState: typeof encodeOAuthState; -// @public (undocumented) +// @public @deprecated (undocumented) export const ensuresXRequestedWith: (req: express.Request) => boolean; -// @public -export type GcpIapResult = { - iapToken: GcpIapTokenInfo; -}; +// @public @deprecated +export type GcpIapResult = GcpIapResult_2; -// @public -export type GcpIapTokenInfo = { - sub: string; - email: string; - [key: string]: JsonValue; -}; +// @public @deprecated +export type GcpIapTokenInfo = GcpIapTokenInfo_2; // @public export function getDefaultOwnershipEntityRefs(entity: Entity): string[]; @@ -276,7 +236,7 @@ export type OAuth2ProxyResult = { getHeader(name: string): string | undefined; }; -// @public (undocumented) +// @public @deprecated (undocumented) export class OAuthAdapter implements AuthProviderRouteHandlers { constructor(handlers: OAuthHandlers, options: OAuthAdapterOptions); // (undocumented) @@ -298,7 +258,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { start(req: express.Request, res: express.Response): Promise; } -// @public (undocumented) +// @public @deprecated (undocumented) export type OAuthAdapterOptions = { providerId: string; persistScopes?: boolean; @@ -309,25 +269,10 @@ export type OAuthAdapterOptions = { callbackUrl: string; }; -// @public (undocumented) -export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers { - constructor(handlers: Map); - // (undocumented) - frameHandler(req: express.Request, res: express.Response): Promise; - // (undocumented) - logout(req: express.Request, res: express.Response): Promise; - // (undocumented) - static mapConfig( - config: Config, - factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers, - ): OAuthEnvironmentHandler; - // (undocumented) - refresh(req: express.Request, res: express.Response): Promise; - // (undocumented) - start(req: express.Request, res: express.Response): Promise; -} +// @public @deprecated (undocumented) +export const OAuthEnvironmentHandler: typeof OAuthEnvironmentHandler_2; -// @public +// @public @deprecated (undocumented) export interface OAuthHandlers { handler(req: express.Request): Promise<{ response: OAuthResponse; @@ -341,12 +286,12 @@ export interface OAuthHandlers { start(req: OAuthStartRequest): Promise; } -// @public (undocumented) +// @public @deprecated (undocumented) export type OAuthLogoutRequest = express.Request<{}> & { refreshToken: string; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type OAuthProviderInfo = { accessToken: string; idToken?: string; @@ -354,59 +299,53 @@ export type OAuthProviderInfo = { scope: string; }; -// @public +// @public @deprecated export type OAuthProviderOptions = { clientId: string; clientSecret: string; callbackUrl: string; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type OAuthRefreshRequest = express.Request<{}> & { scope: string; refreshToken: string; }; -// @public +// @public @deprecated (undocumented) export type OAuthResponse = { profile: ProfileInfo; providerInfo: OAuthProviderInfo; backstageIdentity?: BackstageSignInResult; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type OAuthResult = { fullProfile: Profile; params: { id_token?: string; scope: string; + token_type?: string; expires_in: number; }; accessToken: string; refreshToken?: string; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type OAuthStartRequest = express.Request<{}> & { scope: string; state: OAuthState; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type OAuthStartResponse = { url: string; status?: number; }; -// @public (undocumented) -export type OAuthState = { - nonce: string; - env: string; - origin?: string; - scope?: string; - redirectUrl?: string; - flow?: string; -}; +// @public @deprecated (undocumented) +export type OAuthState = OAuthState_2; // @public export type OidcAuthResult = { @@ -414,24 +353,18 @@ export type OidcAuthResult = { userinfo: UserinfoResponse; }; -// @public (undocumented) +// @public @deprecated (undocumented) export const postMessageResponse: ( res: express.Response, appOrigin: string, response: WebMessageResponse, ) => void; -// @public -export function prepareBackstageIdentityResponse( - result: BackstageSignInResult, -): BackstageIdentityResponse; +// @public @deprecated (undocumented) +export const prepareBackstageIdentityResponse: typeof prepareBackstageIdentityResponse_2; -// @public -export type ProfileInfo = { - email?: string; - displayName?: string; - picture?: string; -}; +// @public @deprecated (undocumented) +export type ProfileInfo = ProfileInfo_2; // @public (undocumented) export type ProviderFactories = { @@ -452,7 +385,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: never; }>; auth0: Readonly<{ @@ -467,7 +400,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: never; }>; awsAlb: Readonly<{ @@ -480,7 +413,7 @@ export const providers: Readonly<{ }; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: never; }>; bitbucket: Readonly<{ @@ -495,7 +428,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: Readonly<{ usernameMatchingUserEntityAnnotation(): SignInResolver; userIdMatchingUserEntityAnnotation(): SignInResolver; @@ -513,7 +446,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: Readonly<{ emailMatchingUserEntityProfileEmail: () => SignInResolver; }>; @@ -525,18 +458,18 @@ export const providers: Readonly<{ resolver: SignInResolver; }; cache?: CacheService | undefined; - }) => AuthProviderFactory; + }) => AuthProviderFactory_2; resolvers: Readonly<{ emailMatchingUserEntityProfileEmail: () => SignInResolver; }>; }>; gcpIap: Readonly<{ create: (options: { - authHandler?: AuthHandler | undefined; + authHandler?: AuthHandler | undefined; signIn: { - resolver: SignInResolver; + resolver: SignInResolver; }; - }) => AuthProviderFactory; + }) => AuthProviderFactory_2; resolvers: never; }>; github: Readonly<{ @@ -546,15 +479,15 @@ export const providers: Readonly<{ authHandler?: AuthHandler | undefined; signIn?: | { - resolver: SignInResolver; + resolver: SignInResolver_2; } | undefined; stateEncoder?: StateEncoder | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: Readonly<{ - usernameMatchingUserEntityName: () => SignInResolver; + usernameMatchingUserEntityName: () => SignInResolver_2; }>; }>; gitlab: Readonly<{ @@ -569,7 +502,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: never; }>; google: Readonly<{ @@ -584,11 +517,11 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: Readonly<{ - emailLocalPartMatchingUserEntityName: () => SignInResolver; - emailMatchingUserEntityProfileEmail: () => SignInResolver; - emailMatchingUserEntityAnnotation(): SignInResolver; + emailMatchingUserEntityProfileEmail: () => SignInResolver_2; + emailLocalPartMatchingUserEntityName: () => SignInResolver_2; + emailMatchingUserEntityAnnotation: () => SignInResolver_2; }>; }>; microsoft: Readonly<{ @@ -603,7 +536,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: Readonly<{ emailLocalPartMatchingUserEntityName: () => SignInResolver; emailMatchingUserEntityProfileEmail: () => SignInResolver; @@ -622,7 +555,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: never; }>; oauth2Proxy: Readonly<{ @@ -631,7 +564,7 @@ export const providers: Readonly<{ signIn: { resolver: SignInResolver>; }; - }) => AuthProviderFactory; + }) => AuthProviderFactory_2; resolvers: never; }>; oidc: Readonly<{ @@ -646,7 +579,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: Readonly<{ emailLocalPartMatchingUserEntityName: () => SignInResolver; emailMatchingUserEntityProfileEmail: () => SignInResolver; @@ -664,7 +597,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: Readonly<{ emailLocalPartMatchingUserEntityName: () => SignInResolver; emailMatchingUserEntityProfileEmail: () => SignInResolver; @@ -683,7 +616,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: never; }>; saml: Readonly<{ @@ -698,7 +631,7 @@ export const providers: Readonly<{ | undefined; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: Readonly<{ nameIdMatchingUserEntityName(): SignInResolver; }>; @@ -713,13 +646,13 @@ export const providers: Readonly<{ }; } | undefined, - ) => AuthProviderFactory; + ) => AuthProviderFactory_2; resolvers: never; }>; }>; -// @public (undocumented) -export const readState: (stateString: string) => OAuthState; +// @public @deprecated (undocumented) +export const readState: typeof decodeOAuthState; // @public (undocumented) export interface RouterOptions { @@ -730,9 +663,11 @@ export interface RouterOptions { // (undocumented) database: PluginDatabaseManager; // (undocumented) + disableDefaultProviderFactories?: boolean; + // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) - logger: Logger; + logger: LoggerService; // (undocumented) providerFactories?: ProviderFactories; // (undocumented) @@ -746,42 +681,23 @@ export type SamlAuthResult = { fullProfile: any; }; -// @public -export type SignInInfo = { - profile: ProfileInfo; - result: TAuthResult; -}; +// @public @deprecated (undocumented) +export type SignInInfo = SignInInfo_2; -// @public -export type SignInResolver = ( - info: SignInInfo, - context: AuthResolverContext, -) => Promise; +// @public @deprecated (undocumented) +export type SignInResolver = SignInResolver_2; -// @public (undocumented) +// @public @deprecated (undocumented) export type StateEncoder = (req: OAuthStartRequest) => Promise<{ encodedState: string; }>; -// @public -export type TokenParams = { - claims: { - sub: string; - ent?: string[]; - } & Record; -}; +// @public @deprecated (undocumented) +export type TokenParams = TokenParams_2; -// @public (undocumented) +// @public @deprecated (undocumented) export const verifyNonce: (req: express.Request, providerId: string) => void; -// @public -export type WebMessageResponse = - | { - type: 'authorization_response'; - response: AuthResponse; - } - | { - type: 'authorization_response'; - error: Error; - }; +// @public @deprecated (undocumented) +export type WebMessageResponse = WebMessageResponse_2; ``` diff --git a/plugins/auth-backend/architecture.drawio.svg b/plugins/auth-backend/architecture.drawio.svg new file mode 100644 index 0000000000..d709935e3e --- /dev/null +++ b/plugins/auth-backend/architecture.drawio.svg @@ -0,0 +1,262 @@ + + + + + + + +
+
+
+ PassportStrategy +
+
+
+
+ + PassportStrategy + +
+
+ + + + + + + + +
+
+
+ OAuthAuthenticator +
+
+
+
+ + OAuthAuthenticator + +
+
+ + + + + + + + + + + + +
+
+
+ ProxyProviderFactory +
+
+
+
+ + ProxyProviderFactory + +
+
+ + + + + + + + + + + + + + + + +
+
+
+ OAuthProviderFactory +
+
+
+
+ + OAuthProviderFactory + +
+
+ + + + +
+
+
+ ProfileTransform +
+
+
+
+ + ProfileTransform + +
+
+ + + + +
+
+
+ ProxyAuthenticator +
+
+
+
+ + ProxyAuthenticator + +
+
+ + + + +
+
+
+ AccessCheck +
+
+
+
+ + AccessCheck + +
+
+ + + + +
+
+
+ SignInResolver +
+
+
+
+ + SignInResolver + +
+
+ + + + + + +
+
+
+ authModule*Provider +
+
+
+
+ + authModule*Provider + +
+
+ + + + +
+
+
+ OAuthEnvironmentHandler +
+
+
+
+ + OAuthEnvironmentHandler + +
+
+ + + + +
+
+
+ OAuthAdapter +
+
+
+
+ + OAuthAdapter + +
+
+ + + + +
+
+
+ PassportOAuthAuthenticatorHelper +
+
+
+
+ + PassportOAuthAuthenticatorHelper + +
+
+ + + + + + +
+
+
+ authModule*Provider +
+
+
+
+ + authModule*Provider + +
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/plugins/auth-backend/catalog-info.yaml b/plugins/auth-backend/catalog-info.yaml new file mode 100644 index 0000000000..38ac5f0753 --- /dev/null +++ b/plugins/auth-backend/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth-backend + title: '@backstage/plugin-auth-backend' + description: A Backstage backend plugin that handles authentication +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: maintainers diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index 4ebf7c9de1..e719468ac1 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -31,6 +31,16 @@ export interface Config { secret?: string; }; + /** + * JWS "alg" (Algorithm) Header Parameter value. Defaults to ES256. + * Must match one of the algorithms defined for IdentityClient. + * When setting a different algorithm, check if the `key` field + * of the `signing_keys` table can fit the length of the generated keys. + * If not, add a knex migration file in the migrations folder. + * More info on supported algorithms: https://github.com/panva/jose + */ + identityTokenAlgorithm?: string; + /** To control how to store JWK data in auth-backend */ keyStore?: { provider?: 'database' | 'memory' | 'firestore'; @@ -86,18 +96,6 @@ export interface Config { }; }; /** @visibility frontend */ - gitlab?: { - [authEnv: string]: { - clientId: string; - /** - * @visibility secret - */ - clientSecret: string; - audience?: string; - callbackUrl?: string; - }; - }; - /** @visibility frontend */ saml?: { entryPoint: string; logoutUrl?: string; diff --git a/plugins/auth-backend/dev/index.ts b/plugins/auth-backend/dev/index.ts new file mode 100644 index 0000000000..e1fac31318 --- /dev/null +++ b/plugins/auth-backend/dev/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); + +backend.add(import('../src')); +backend.add(import('@backstage/plugin-auth-backend-module-google-provider')); + +backend.start(); diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 2a3b80de6a..73bd3aa2ea 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend", "description": "A Backstage backend plugin that handles authentication", - "version": "0.18.6-next.1", + "version": "0.18.9-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,11 +33,17 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/plugin-auth-backend-module-gcp-iap-provider": "workspace:^", + "@backstage/plugin-auth-backend-module-github-provider": "workspace:^", + "@backstage/plugin-auth-backend-module-gitlab-provider": "workspace:^", + "@backstage/plugin-auth-backend-module-google-provider": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", + "@backstage/plugin-catalog-node": "workspace:^", "@backstage/types": "workspace:^", "@davidzemon/passport-okta-oauth": "^0.0.5", "@google-cloud/firestore": "^6.0.0", @@ -77,6 +83,7 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-defaults": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/body-parser": "^1.19.0", diff --git a/plugins/auth-backend/src/authPlugin.test.ts b/plugins/auth-backend/src/authPlugin.test.ts new file mode 100644 index 0000000000..d8a15eba3e --- /dev/null +++ b/plugins/auth-backend/src/authPlugin.test.ts @@ -0,0 +1,45 @@ +/* + * 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 { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import request from 'supertest'; +import { authPlugin } from './authPlugin'; + +describe('authPlugin', () => { + it('should provide an OpenID configuration', async () => { + const { server } = await startTestBackend({ + features: [ + authPlugin, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + }, + }), + ], + }); + + const res = await request(server).get( + '/api/auth/.well-known/openid-configuration', + ); + expect(res.status).toBe(200); + expect(res.body).toMatchObject({ + claims_supported: ['sub'], + issuer: `http://localhost:${server.port()}/api/auth`, + }); + }); +}); diff --git a/plugins/auth-backend/src/authPlugin.ts b/plugins/auth-backend/src/authPlugin.ts new file mode 100644 index 0000000000..0522424a99 --- /dev/null +++ b/plugins/auth-backend/src/authPlugin.ts @@ -0,0 +1,82 @@ +/* + * 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 { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { + AuthProviderFactory, + authProvidersExtensionPoint, +} from '@backstage/plugin-auth-node'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; +import { createRouter } from './service/router'; + +/** + * Auth plugin + * + * @public + */ +export const authPlugin = createBackendPlugin({ + pluginId: 'auth', + register(reg) { + const providers = new Map(); + + reg.registerExtensionPoint(authProvidersExtensionPoint, { + registerProvider({ providerId, factory }) { + if (providers.has(providerId)) { + throw new Error( + `Auth provider '${providerId}' was already registered`, + ); + } + providers.set(providerId, factory); + }, + }); + + reg.registerInit({ + deps: { + httpRouter: coreServices.httpRouter, + logger: coreServices.logger, + config: coreServices.rootConfig, + database: coreServices.database, + discovery: coreServices.discovery, + tokenManager: coreServices.tokenManager, + catalogApi: catalogServiceRef, + }, + async init({ + httpRouter, + logger, + config, + database, + discovery, + tokenManager, + catalogApi, + }) { + const router = await createRouter({ + logger, + config, + database, + discovery, + tokenManager, + catalogApi, + providerFactories: Object.fromEntries(providers), + disableDefaultProviderFactories: true, + }); + httpRouter.use(router); + }, + }); + }, +}); diff --git a/plugins/auth-backend/src/identity/FirestoreKeyStore.ts b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts index a5479a6470..b5f4f5dcfb 100644 --- a/plugins/auth-backend/src/identity/FirestoreKeyStore.ts +++ b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { DocumentData, Firestore, @@ -57,7 +57,7 @@ export class FirestoreKeyStore implements KeyStore { static async verifyConnection( keyStore: FirestoreKeyStore, - logger?: Logger, + logger?: LoggerService, ): Promise { try { await keyStore.verify(); diff --git a/plugins/auth-backend/src/identity/KeyStores.ts b/plugins/auth-backend/src/identity/KeyStores.ts index 4cd8f91842..cc35c0d84e 100644 --- a/plugins/auth-backend/src/identity/KeyStores.ts +++ b/plugins/auth-backend/src/identity/KeyStores.ts @@ -15,7 +15,7 @@ */ import { pickBy } from 'lodash'; -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; @@ -26,7 +26,7 @@ import { MemoryKeyStore } from './MemoryKeyStore'; import { KeyStore } from './types'; type Options = { - logger: Logger; + logger: LoggerService; database: AuthDatabase; }; diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts index f80b79b6bc..ca16f8821f 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -18,14 +18,14 @@ import { AuthenticationError } from '@backstage/errors'; import { exportJWK, generateKeyPair, importJWK, JWK, SignJWT } from 'jose'; import { DateTime } from 'luxon'; import { v4 as uuid } from 'uuid'; -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { AnyJWK, KeyStore, TokenIssuer, TokenParams } from './types'; const MS_IN_S = 1000; type Options = { - logger: Logger; + logger: LoggerService; /** Value of the issuer claim in issued tokens */ issuer: string; /** Key store used for storing signing keys */ @@ -57,7 +57,7 @@ type Options = { */ export class TokenFactory implements TokenIssuer { private readonly issuer: string; - private readonly logger: Logger; + private readonly logger: LoggerService; private readonly keyStore: KeyStore; private readonly keyDurationSeconds: number; private readonly algorithm: string; diff --git a/plugins/auth-backend/src/identity/types.ts b/plugins/auth-backend/src/identity/types.ts index e325f74c81..fcfc0345cc 100644 --- a/plugins/auth-backend/src/identity/types.ts +++ b/plugins/auth-backend/src/identity/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/types'; +import { TokenParams as _TokenParams } from '@backstage/plugin-auth-node'; /** Represents any form of serializable JWK */ export interface AnyJWK extends Record { @@ -25,26 +25,10 @@ export interface AnyJWK extends Record { } /** - * Parameters used to issue new ID Tokens - * * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export type TokenParams = { - /** - * The claims that will be embedded within the token. At a minimum, this should include - * the subject claim, `sub`. It is common to also list entity ownership relations in the - * `ent` list. Additional claims may also be added at the developer's discretion except - * for the following list, which will be overwritten by the TokenIssuer: `iss`, `aud`, - * `iat`, and `exp`. The Backstage team also maintains the right add new claims in the future - * without listing the change as a "breaking change". - */ - claims: { - /** The token subject, i.e. User ID */ - sub: string; - /** A list of entity references that the user claims ownership through */ - ent?: string[]; - } & Record; -}; +export type TokenParams = _TokenParams; /** * A TokenIssuer is able to issue verifiable ID Tokens on demand. diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index f8a8580359..3943387e15 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -20,6 +20,7 @@ * @packageDocumentation */ +export { authPlugin as default } from './authPlugin'; export * from './service/router'; export type { TokenParams } from './identity'; export * from './providers'; diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index 494e8b4d38..ff983e27a5 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { ConflictError, NotFoundError } from '@backstage/errors'; import { CatalogApi } from '@backstage/catalog-client'; import { @@ -78,7 +78,7 @@ export class CatalogIdentityClient { */ async resolveCatalogMembership(query: { entityRefs: string[]; - logger?: Logger; + logger?: LoggerService; }): Promise { const { entityRefs, logger } = query; const resolvedEntityRefs = entityRefs diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts index 94ac4fba15..2f1770a3d4 100644 --- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts @@ -24,7 +24,10 @@ export const safelyEncodeURIComponent = (value: string) => { return encodeURIComponent(value).replace(/'/g, '%27'); }; -/** @public */ +/** + * @public + * @deprecated Use `sendWebMessageResponse` from `@backstage/plugin-auth-node` instead + */ export const postMessageResponse = ( res: express.Response, appOrigin: string, @@ -69,7 +72,10 @@ export const postMessageResponse = ( res.end(``); }; -/** @public */ +/** + * @public + * @deprecated Use inline logic to check that the `X-Requested-With` header is set to `'XMLHttpRequest'` instead. + */ export const ensuresXRequestedWith = (req: express.Request) => { const requiredHeader = req.header('X-Requested-With'); if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') { diff --git a/plugins/auth-backend/src/lib/flow/types.ts b/plugins/auth-backend/src/lib/flow/types.ts index f67198a1b0..36f3033b21 100644 --- a/plugins/auth-backend/src/lib/flow/types.ts +++ b/plugins/auth-backend/src/lib/flow/types.ts @@ -14,20 +14,10 @@ * limitations under the License. */ -import { AuthResponse } from '../../providers/types'; +import { WebMessageResponse as _WebMessageResponse } from '@backstage/plugin-auth-node'; /** - * Payload sent as a post message after the auth request is complete. - * If successful then has a valid payload with Auth information else contains an error. - * * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export type WebMessageResponse = - | { - type: 'authorization_response'; - response: AuthResponse; - } - | { - type: 'authorization_response'; - error: Error; - }; +export type WebMessageResponse = _WebMessageResponse; diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.test.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.test.ts new file mode 100644 index 0000000000..5c5de6a890 --- /dev/null +++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.test.ts @@ -0,0 +1,61 @@ +/* + * 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 { AuthResolverContext } from '@backstage/plugin-auth-node'; +import { AuthHandler } from '../../providers'; +import { OAuthResult } from '../oauth'; +import { PassportProfile } from '../passport/types'; +import { adaptLegacyOAuthHandler } from './adaptLegacyOAuthHandler'; + +describe('adaptLegacyOAuthHandler', () => { + it('should pass through undefined', () => { + expect(adaptLegacyOAuthHandler(undefined)).toBeUndefined(); + }); + + it('should convert an old auth handler to a new profile transform', () => { + const authHandler: AuthHandler = jest.fn(); + const profileTransform = adaptLegacyOAuthHandler(authHandler); + + profileTransform?.( + { + fullProfile: { id: 'id' } as PassportProfile, + session: { + accessToken: 'token', + expiresInSeconds: 3, + scope: 'sco pe', + tokenType: 'bear', + idToken: 'id-token', + refreshToken: 'refresh-token', + }, + }, + { ctx: 'ctx' } as unknown as AuthResolverContext, + ); + + expect(authHandler).toHaveBeenCalledWith( + { + fullProfile: { id: 'id' }, + accessToken: 'token', + params: { + scope: 'sco pe', + id_token: 'id-token', + expires_in: 3, + token_type: 'bear', + }, + }, + { ctx: 'ctx' }, + ); + }); +}); diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.ts new file mode 100644 index 0000000000..3b8eca0a95 --- /dev/null +++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.ts @@ -0,0 +1,46 @@ +/* + * 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 { + OAuthAuthenticatorResult, + ProfileTransform, +} from '@backstage/plugin-auth-node'; +import { AuthHandler } from '../../providers'; +import { OAuthResult } from '../oauth'; +import { PassportProfile } from '../passport/types'; + +/** @internal */ +export function adaptLegacyOAuthHandler( + authHandler?: AuthHandler, +): ProfileTransform> | undefined { + return ( + authHandler && + (async (result, ctx) => + authHandler( + { + fullProfile: result.fullProfile, + accessToken: result.session.accessToken, + params: { + scope: result.session.scope, + id_token: result.session.idToken, + token_type: result.session.tokenType, + expires_in: result.session.expiresInSeconds!, + }, + }, + ctx, + )) + ); +} diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.test.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.test.ts new file mode 100644 index 0000000000..749cf96cbb --- /dev/null +++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { + AuthResolverContext, + PassportProfile, +} from '@backstage/plugin-auth-node'; +import { adaptLegacyOAuthSignInResolver } from './adaptLegacyOAuthSignInResolver'; + +describe('adaptLegacyOAuthSignInResolver', () => { + it('should pass through undefined', () => { + expect(adaptLegacyOAuthSignInResolver(undefined)).toBeUndefined(); + }); + + it('should convert a legacy resolver to a new one', () => { + const legacyResolver = jest.fn(); + + const newResolver = adaptLegacyOAuthSignInResolver(legacyResolver); + + newResolver?.( + { + profile: { email: 'em@i.l' }, + result: { + fullProfile: { id: 'id' } as PassportProfile, + session: { + accessToken: 'token', + expiresInSeconds: 3, + scope: 'sco pe', + tokenType: 'bear', + idToken: 'id-token', + refreshToken: 'refresh-token', + }, + }, + }, + { ctx: 'ctx' } as unknown as AuthResolverContext, + ); + + expect(legacyResolver).toHaveBeenCalledWith( + { + profile: { email: 'em@i.l' }, + result: { + fullProfile: { id: 'id' }, + accessToken: 'token', + refreshToken: 'refresh-token', + params: { + scope: 'sco pe', + id_token: 'id-token', + expires_in: 3, + token_type: 'bear', + }, + }, + }, + { ctx: 'ctx' }, + ); + }); +}); diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.ts new file mode 100644 index 0000000000..e671a62d52 --- /dev/null +++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.ts @@ -0,0 +1,49 @@ +/* + * 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 { + OAuthAuthenticatorResult, + PassportProfile, + SignInResolver, +} from '@backstage/plugin-auth-node'; +import { OAuthResult } from '../oauth'; + +/** @internal */ +export function adaptLegacyOAuthSignInResolver( + signInResolver?: SignInResolver, +): SignInResolver> | undefined { + return ( + signInResolver && + (async (input, ctx) => + signInResolver( + { + profile: input.profile, + result: { + fullProfile: input.result.fullProfile, + accessToken: input.result.session.accessToken, + refreshToken: input.result.session.refreshToken, + params: { + scope: input.result.session.scope, + id_token: input.result.session.idToken, + token_type: input.result.session.tokenType, + expires_in: input.result.session.expiresInSeconds!, + }, + }, + }, + ctx, + )) + ); +} diff --git a/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.test.ts b/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.test.ts new file mode 100644 index 0000000000..521dcf6395 --- /dev/null +++ b/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.test.ts @@ -0,0 +1,85 @@ +/* + * 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 { + AuthResolverContext, + PassportProfile, +} from '@backstage/plugin-auth-node'; +import { adaptOAuthSignInResolverToLegacy } from './adaptOAuthSignInResolverToLegacy'; + +describe('adaptOAuthSignInResolverToLegacy', () => { + it('should pass through an empty object', () => { + const legacyResolvers = adaptOAuthSignInResolverToLegacy({}); + expect(legacyResolvers).toEqual({}); + + // @ts-expect-error + legacyResolvers.missing?.(); + }); + + it('should adapt a collection of sign-in resolvers', () => { + const resolverA = jest.fn(); + const resolverB = jest.fn(); + + const legacyResolvers = adaptOAuthSignInResolverToLegacy({ + resolverA, + resolverB, + }); + + const legacyResolverA = legacyResolvers.resolverA(); + legacyResolverA( + { + profile: { email: 'em@i.l' }, + result: { + fullProfile: { id: 'id' } as PassportProfile, + accessToken: 'token', + refreshToken: 'refresh-token', + params: { + scope: 'sco pe', + id_token: 'id-token', + expires_in: 3, + token_type: 'bear', + }, + }, + }, + { ctx: 'ctx' } as unknown as AuthResolverContext, + ); + + expect(resolverA).toHaveBeenCalledWith( + { + profile: { email: 'em@i.l' }, + result: { + fullProfile: { id: 'id' } as PassportProfile, + session: { + accessToken: 'token', + expiresInSeconds: 3, + scope: 'sco pe', + tokenType: 'bear', + idToken: 'id-token', + refreshToken: 'refresh-token', + }, + }, + }, + { ctx: 'ctx' }, + ); + + expect(resolverB).not.toHaveBeenCalled(); + legacyResolvers.resolverB()( + { profile: {}, result: { params: {} } } as any, + {} as any, + ); + expect(resolverB).toHaveBeenCalled(); + }); +}); diff --git a/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.ts b/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.ts new file mode 100644 index 0000000000..8be6049348 --- /dev/null +++ b/plugins/auth-backend/src/lib/legacy/adaptOAuthSignInResolverToLegacy.ts @@ -0,0 +1,55 @@ +/* + * 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 { + OAuthAuthenticatorResult, + PassportProfile, + SignInResolver, +} from '@backstage/plugin-auth-node'; +import { OAuthResult } from '../oauth'; + +/** @internal */ +export function adaptOAuthSignInResolverToLegacy< + TKeys extends string, +>(resolvers: { + [key in TKeys]: SignInResolver>; +}): { [key in TKeys]: () => SignInResolver } { + const legacyResolvers = {} as { + [key in TKeys]: () => SignInResolver; + }; + for (const name of Object.keys(resolvers) as TKeys[]) { + const resolver = resolvers[name]; + legacyResolvers[name] = () => async (input, ctx) => + resolver( + { + profile: input.profile, + result: { + fullProfile: input.result.fullProfile, + session: { + accessToken: input.result.accessToken, + expiresInSeconds: input.result.params.expires_in, + scope: input.result.params.scope, + idToken: input.result.params.id_token, + tokenType: input.result.params.token_type ?? 'bearer', + refreshToken: input.result.refreshToken, + }, + }, + }, + ctx, + ); + } + return legacyResolvers; +} diff --git a/plugins/auth-backend/src/lib/legacy/index.ts b/plugins/auth-backend/src/lib/legacy/index.ts new file mode 100644 index 0000000000..8bd8b5c62e --- /dev/null +++ b/plugins/auth-backend/src/lib/legacy/index.ts @@ -0,0 +1,19 @@ +/* + * 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 { adaptLegacyOAuthHandler } from './adaptLegacyOAuthHandler'; +export { adaptLegacyOAuthSignInResolver } from './adaptLegacyOAuthSignInResolver'; +export { adaptOAuthSignInResolverToLegacy } from './adaptOAuthSignInResolverToLegacy'; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index fb0750f0cb..9b4afb4b2b 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -50,7 +50,10 @@ import { prepareBackstageIdentityResponse } from '../../providers/prepareBacksta export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; -/** @public */ +/** + * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead + */ export type OAuthAdapterOptions = { providerId: string; persistScopes?: boolean; @@ -61,7 +64,10 @@ export type OAuthAdapterOptions = { callbackUrl: string; }; -/** @public */ +/** + * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead + */ export class OAuthAdapter implements AuthProviderRouteHandlers { static fromConfig( config: AuthProviderConfig, diff --git a/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts b/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts index 1c6d45d0a4..c9244eb29e 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts @@ -14,84 +14,10 @@ * limitations under the License. */ -import express from 'express'; -import { Config } from '@backstage/config'; -import { InputError, NotFoundError } from '@backstage/errors'; -import { readState } from './helpers'; -import { AuthProviderRouteHandlers } from '../../providers/types'; +import { OAuthEnvironmentHandler as _OAuthEnvironmentHandler } from '@backstage/plugin-auth-node'; -/** @public */ -export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers { - static mapConfig( - config: Config, - factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers, - ) { - const envs = config.keys(); - const handlers = new Map(); - - for (const env of envs) { - const envConfig = config.getConfig(env); - const handler = factoryFunc(envConfig); - handlers.set(env, handler); - } - - return new OAuthEnvironmentHandler(handlers); - } - - constructor( - private readonly handlers: Map, - ) {} - - async start(req: express.Request, res: express.Response): Promise { - const provider = this.getProviderForEnv(req); - await provider.start(req, res); - } - - async frameHandler( - req: express.Request, - res: express.Response, - ): Promise { - const provider = this.getProviderForEnv(req); - await provider.frameHandler(req, res); - } - - async refresh(req: express.Request, res: express.Response): Promise { - const provider = this.getProviderForEnv(req); - await provider.refresh?.(req, res); - } - - async logout(req: express.Request, res: express.Response): Promise { - const provider = this.getProviderForEnv(req); - await provider.logout?.(req, res); - } - - private getRequestFromEnv(req: express.Request): string | undefined { - const reqEnv = req.query.env?.toString(); - if (reqEnv) { - return reqEnv; - } - const stateParams = req.query.state?.toString(); - if (!stateParams) { - return undefined; - } - const env = readState(stateParams).env; - return env; - } - - private getProviderForEnv(req: express.Request): AuthProviderRouteHandlers { - const env: string | undefined = this.getRequestFromEnv(req); - - if (!env) { - throw new InputError(`Must specify 'env' query to select environment`); - } - - const handler = this.handlers.get(env); - if (!handler) { - throw new NotFoundError( - `No configuration available for the '${env}' environment of this provider.`, - ); - } - - return handler; - } -} +/** + * @public + * @deprecated import from `@backstage/plugin-auth-node` instead + */ +export const OAuthEnvironmentHandler = _OAuthEnvironmentHandler; diff --git a/plugins/auth-backend/src/lib/oauth/helpers.test.ts b/plugins/auth-backend/src/lib/oauth/helpers.test.ts index 58a3dcab05..c8db34e2cc 100644 --- a/plugins/auth-backend/src/lib/oauth/helpers.test.ts +++ b/plugins/auth-backend/src/lib/oauth/helpers.test.ts @@ -76,7 +76,7 @@ describe('OAuthProvider Utils', () => { } as unknown as express.Request; expect(() => { verifyNonce(mockRequest, 'providera'); - }).toThrow('Invalid state passed via request'); + }).toThrow('OAuth state is invalid, missing env'); }); it('should throw error if nonce mismatch', () => { diff --git a/plugins/auth-backend/src/lib/oauth/helpers.ts b/plugins/auth-backend/src/lib/oauth/helpers.ts index 2b4e50b477..5e67b072e3 100644 --- a/plugins/auth-backend/src/lib/oauth/helpers.ts +++ b/plugins/auth-backend/src/lib/oauth/helpers.ts @@ -16,36 +16,28 @@ import express from 'express'; import { OAuthState } from './types'; -import pickBy from 'lodash/pickBy'; import { CookieConfigurer } from '../../providers/types'; +import { + decodeOAuthState, + encodeOAuthState, +} from '@backstage/plugin-auth-node'; -/** @public */ -export const readState = (stateString: string): OAuthState => { - const state = Object.fromEntries( - new URLSearchParams(Buffer.from(stateString, 'hex').toString('utf-8')), - ); - if ( - !state.nonce || - !state.env || - state.nonce?.length === 0 || - state.env?.length === 0 - ) { - throw Error(`Invalid state passed via request`); - } +/** + * @public + * @deprecated Use `decodeOAuthState` from `@backstage/plugin-auth-node` instead + */ +export const readState = decodeOAuthState; - return state as OAuthState; -}; +/** + * @public + * @deprecated Use `encodeOAuthState` from `@backstage/plugin-auth-node` instead + */ +export const encodeState = encodeOAuthState; -/** @public */ -export const encodeState = (state: OAuthState): string => { - const stateString = new URLSearchParams( - pickBy(state, value => value !== undefined), - ).toString(); - - return Buffer.from(stateString, 'utf-8').toString('hex'); -}; - -/** @public */ +/** + * @public + * @deprecated Use inline logic to make sure the session and state nonce matches instead. + */ export const verifyNonce = (req: express.Request, providerId: string) => { const cookieNonce = req.cookies[`${providerId}-nonce`]; const state: OAuthState = readState(req.query.state?.toString() ?? ''); diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index e960af2988..b7205c9b85 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -16,13 +16,17 @@ import express from 'express'; import { Profile as PassportProfile } from 'passport'; -import { BackstageSignInResult } from '@backstage/plugin-auth-node'; +import { + BackstageSignInResult, + OAuthState as _OAuthState, +} from '@backstage/plugin-auth-node'; import { OAuthStartResponse, ProfileInfo } from '../../providers/types'; /** * Common options for passport.js-based OAuth providers * * @public + * @deprecated No longer in use */ export type OAuthProviderOptions = { /** @@ -39,12 +43,16 @@ export type OAuthProviderOptions = { callbackUrl: string; }; -/** @public */ +/** + * @public + * @deprecated Use `OAuthAuthenticatorResult` from `@backstage/plugin-auth-node` instead + */ export type OAuthResult = { fullProfile: PassportProfile; params: { id_token?: string; scope: string; + token_type?: string; expires_in: number; }; accessToken: string; @@ -52,9 +60,8 @@ export type OAuthResult = { }; /** - * The expected response from an OAuth flow. - * * @public + * @deprecated Use `ClientAuthResponse` from `@backstage/plugin-auth-node` instead */ export type OAuthResponse = { profile: ProfileInfo; @@ -62,7 +69,10 @@ export type OAuthResponse = { backstageIdentity?: BackstageSignInResult; }; -/** @public */ +/** + * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead + */ export type OAuthProviderInfo = { /** * An access token issued for the signed in user. @@ -82,41 +92,41 @@ export type OAuthProviderInfo = { scope: string; }; -/** @public */ -export type OAuthState = { - /* A type for the serialized value in the `state` parameter of the OAuth authorization flow - */ - nonce: string; - env: string; - origin?: string; - scope?: string; - redirectUrl?: string; - flow?: string; -}; +/** + * @public + * @deprecated import from `@backstage/plugin-auth-node` instead + */ +export type OAuthState = _OAuthState; -/** @public */ +/** + * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead + */ export type OAuthStartRequest = express.Request<{}> & { scope: string; state: OAuthState; }; -/** @public */ +/** + * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead + */ export type OAuthRefreshRequest = express.Request<{}> & { scope: string; refreshToken: string; }; -/** @public */ +/** + * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead + */ export type OAuthLogoutRequest = express.Request<{}> & { refreshToken: string; }; /** - * Any OAuth provider needs to implement this interface which has provider specific - * handlers for different methods to perform authentication, get access tokens, - * refresh tokens and perform sign out. - * * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead */ export interface OAuthHandlers { /** diff --git a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts index cbc2439563..7d22526193 100644 --- a/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts +++ b/plugins/auth-backend/src/lib/resolvers/CatalogAuthResolverContext.ts @@ -24,7 +24,7 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { ConflictError, InputError, NotFoundError } from '@backstage/errors'; -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { TokenIssuer, TokenParams } from '../../identity/types'; import { AuthResolverContext } from '../../providers'; import { AuthResolverCatalogUserQuery } from '../../providers/types'; @@ -54,7 +54,7 @@ export function getDefaultOwnershipEntityRefs(entity: Entity) { */ export class CatalogAuthResolverContext implements AuthResolverContext { static create(options: { - logger: Logger; + logger: LoggerService; catalogApi: CatalogApi; tokenIssuer: TokenIssuer; tokenManager: TokenManager; @@ -73,7 +73,7 @@ export class CatalogAuthResolverContext implements AuthResolverContext { } private constructor( - public readonly logger: Logger, + public readonly logger: LoggerService, public readonly tokenIssuer: TokenIssuer, public readonly catalogIdentityClient: CatalogIdentityClient, private readonly catalogApi: CatalogApi, diff --git a/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts b/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts deleted file mode 100644 index 0162366f61..0000000000 --- a/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ConflictError } from '@backstage/errors'; -import { OAuth2Client } from 'google-auth-library'; -import { createTokenValidator, parseRequestToken } from './helpers'; - -const validJwt = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzcyI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.T2BNS4G-6RoiFnXc8Q8TiwdWzTpNitY8jcsGM3N3-Yo'; - -beforeEach(() => { - jest.clearAllMocks(); -}); - -describe('helpers', () => { - describe('createTokenValidator', () => { - it('runs the happy path', async () => { - const mockClient = { - getIapPublicKeys: async () => ({ pubkeys: '' }), - verifySignedJwtWithCertsAsync: async () => ({ - getPayload: () => ({ sub: 's', email: 'e@mail.com' }), - }), - }; - const validator = createTokenValidator( - 'a', - mockClient as unknown as OAuth2Client, - ); - await expect(validator(validJwt)).resolves.toMatchObject({ - sub: 's', - email: 'e@mail.com', - }); - }); - - it('throws if the client throws', async () => { - const mockClient = { - getIapPublicKeys: async () => { - throw new TypeError('bam'); - }, - }; - const validator = createTokenValidator( - 'a', - mockClient as unknown as OAuth2Client, - ); - await expect(validator(validJwt)).rejects.toThrow(TypeError); - }); - - it('rejects empty payload', async () => { - const mockClient = { - getIapPublicKeys: async () => ({ pubkeys: '' }), - verifySignedJwtWithCertsAsync: async () => ({ - getPayload: () => undefined, - }), - }; - const validator = createTokenValidator( - 'a', - mockClient as unknown as OAuth2Client, - ); - await expect(validator(validJwt)).rejects.toMatchObject({ - name: 'TypeError', - message: 'Token had no payload', - }); - }); - }); - - describe('parseRequestToken', () => { - it('runs the happy path', async () => { - await expect( - parseRequestToken( - validJwt, - async () => ({ sub: 's', email: 'e@mail.com' } as any), - ), - ).resolves.toMatchObject({ - iapToken: { - sub: 's', - email: 'e@mail.com', - }, - }); - }); - - it('rejects bad tokens', async () => { - await expect( - parseRequestToken(7, undefined as any), - ).rejects.toMatchObject({ - name: 'AuthenticationError', - message: 'Missing Google IAP header', - }); - await expect( - parseRequestToken(undefined, undefined as any), - ).rejects.toMatchObject({ - name: 'AuthenticationError', - message: 'Missing Google IAP header', - }); - await expect( - parseRequestToken('', undefined as any), - ).rejects.toMatchObject({ - name: 'AuthenticationError', - message: 'Missing Google IAP header', - }); - }); - - it('translates validator errors', async () => { - await expect( - parseRequestToken(validJwt, async () => { - throw new ConflictError('Ouch'); - }), - ).rejects.toMatchObject({ - name: 'AuthenticationError', - message: 'Google IAP token verification failed, ConflictError: Ouch', - }); - }); - - it('rejects bad token payloads', async () => { - await expect( - parseRequestToken(validJwt, async () => ({ sub: 'a' } as any)), - ).rejects.toMatchObject({ - name: 'AuthenticationError', - message: 'Google IAP token payload is missing sub and/or email claim', - }); - }); - }); -}); diff --git a/plugins/auth-backend/src/providers/gcp-iap/helpers.ts b/plugins/auth-backend/src/providers/gcp-iap/helpers.ts deleted file mode 100644 index 26d9a8c904..0000000000 --- a/plugins/auth-backend/src/providers/gcp-iap/helpers.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AuthenticationError } from '@backstage/errors'; -import { OAuth2Client, TokenPayload } from 'google-auth-library'; -import { AuthHandler } from '../types'; -import { GcpIapResult } from './types'; - -export function createTokenValidator( - audience: string, - mockClient?: OAuth2Client, -): (token: string) => Promise { - const client = mockClient ?? new OAuth2Client(); - - return async function tokenValidator(token) { - // TODO(freben): Rate limit the public key reads. It may be sensible to - // cache these for some reasonable time rather than asking for the public - // keys on every single sign-in. But since the rate of events here is so - // slow, I decided to keep it simple for now. - const response = await client.getIapPublicKeys(); - const ticket = await client.verifySignedJwtWithCertsAsync( - token, - response.pubkeys, - audience, - ['https://cloud.google.com/iap'], - ); - - const payload = ticket.getPayload(); - if (!payload) { - throw new TypeError('Token had no payload'); - } - - return payload; - }; -} - -export async function parseRequestToken( - jwtToken: unknown, - tokenValidator: (token: string) => Promise, -): Promise { - if (typeof jwtToken !== 'string' || !jwtToken) { - throw new AuthenticationError('Missing Google IAP header'); - } - - let payload: TokenPayload; - try { - payload = await tokenValidator(jwtToken); - } catch (e) { - throw new AuthenticationError(`Google IAP token verification failed, ${e}`); - } - - if (!payload.sub || !payload.email) { - throw new AuthenticationError( - 'Google IAP token payload is missing sub and/or email claim', - ); - } - - return { - iapToken: { - ...payload, - sub: payload.sub, - email: payload.email, - }, - }; -} - -export const defaultAuthHandler: AuthHandler = async ({ - iapToken, -}) => ({ profile: { email: iapToken.email } }); diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts deleted file mode 100644 index c01db3a853..0000000000 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 express from 'express'; -import request from 'supertest'; -import { AuthResolverContext } from '../types'; -import { GcpIapProvider } from './provider'; -import { DEFAULT_IAP_JWT_HEADER } from './types'; - -beforeEach(() => { - jest.clearAllMocks(); -}); - -describe('GcpIapProvider', () => { - const authHandler = jest.fn(); - const signInResolver = jest.fn(); - const tokenValidator = jest.fn(); - - it.each([undefined, 'x-custom-header'])( - 'runs the happy path', - async jwtHeader => { - const provider = new GcpIapProvider({ - authHandler, - signInResolver, - tokenValidator, - resolverContext: {} as AuthResolverContext, - jwtHeader: jwtHeader, - }); - - // { "sub": "user:default/me", "ent": ["group:default/home"] } - const backstageToken = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvbWUiLCJlbnQiOlsiZ3JvdXA6ZGVmYXVsdC9ob21lIl19.CbmAKzFErGmtsnpRxyPc7dHv7WEjb5lY6206YCzR_Rc'; - const iapToken = { sub: 's', email: 'e@mail.com' }; - - authHandler.mockResolvedValueOnce({ email: 'e@mail.com' }); - signInResolver.mockResolvedValueOnce({ token: backstageToken }); - tokenValidator.mockResolvedValueOnce(iapToken); - - const app = express(); - app.use('/refresh', provider.refresh.bind(provider)); - - const header = jwtHeader || DEFAULT_IAP_JWT_HEADER; - const response = await request(app).get('/refresh').set(header, 'token'); - - expect(response.status).toBe(200); - expect(response.get('content-type')).toBe( - 'application/json; charset=utf-8', - ); - expect(response.body).toEqual({ - backstageIdentity: { - token: backstageToken, - identity: { - type: 'user', - userEntityRef: 'user:default/me', - ownershipEntityRefs: ['group:default/home'], - }, - }, - providerInfo: { iapToken }, - }); - }, - ); -}); diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.ts index 7e77853737..db4d9195b2 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.ts @@ -14,70 +14,11 @@ * limitations under the License. */ -import express from 'express'; -import { TokenPayload } from 'google-auth-library'; +import { gcpIapAuthenticator } from '@backstage/plugin-auth-backend-module-gcp-iap-provider'; +import { createProxyAuthProviderFactory } from '@backstage/plugin-auth-node'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; -import { - AuthHandler, - AuthProviderRouteHandlers, - AuthResolverContext, - SignInResolver, -} from '../types'; -import { - createTokenValidator, - defaultAuthHandler, - parseRequestToken, -} from './helpers'; -import { GcpIapResponse, GcpIapResult, DEFAULT_IAP_JWT_HEADER } from './types'; - -export class GcpIapProvider implements AuthProviderRouteHandlers { - private readonly authHandler: AuthHandler; - private readonly signInResolver: SignInResolver; - private readonly tokenValidator: (token: string) => Promise; - private readonly resolverContext: AuthResolverContext; - private readonly jwtHeader: string; - - constructor(options: { - authHandler: AuthHandler; - signInResolver: SignInResolver; - tokenValidator: (token: string) => Promise; - resolverContext: AuthResolverContext; - jwtHeader?: string; - }) { - this.authHandler = options.authHandler; - this.signInResolver = options.signInResolver; - this.tokenValidator = options.tokenValidator; - this.resolverContext = options.resolverContext; - this.jwtHeader = options?.jwtHeader || DEFAULT_IAP_JWT_HEADER; - } - - async start() {} - - async frameHandler() {} - - async refresh(req: express.Request, res: express.Response): Promise { - const result = await parseRequestToken( - req.header(this.jwtHeader), - this.tokenValidator, - ); - - const { profile } = await this.authHandler(result, this.resolverContext); - - const backstageIdentity = await this.signInResolver( - { profile, result }, - this.resolverContext, - ); - - const response: GcpIapResponse = { - providerInfo: { iapToken: result.iapToken }, - profile, - backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity), - }; - - res.json(response); - } -} +import { AuthHandler, SignInResolver } from '../types'; +import { GcpIapResult } from './types'; /** * Auth provider integration for Google Identity-Aware Proxy auth @@ -104,21 +45,10 @@ export const gcpIap = createAuthProviderIntegration({ resolver: SignInResolver; }; }) { - return ({ config, resolverContext }) => { - const audience = config.getString('audience'); - const jwtHeader = config.getOptionalString('jwtHeader'); - - const authHandler = options.authHandler ?? defaultAuthHandler; - const signInResolver = options.signIn.resolver; - const tokenValidator = createTokenValidator(audience); - - return new GcpIapProvider({ - authHandler, - signInResolver, - tokenValidator, - resolverContext, - jwtHeader, - }); - }; + return createProxyAuthProviderFactory({ + authenticator: gcpIapAuthenticator, + profileTransform: options?.authHandler, + signInResolver: options?.signIn?.resolver, + }); }, }); diff --git a/plugins/auth-backend/src/providers/gcp-iap/types.ts b/plugins/auth-backend/src/providers/gcp-iap/types.ts index 6519de64ac..b1f69318fc 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/types.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/types.ts @@ -14,58 +14,24 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/types'; -import { AuthResponse } from '../types'; - -/** - * The header name used by the IAP. - */ -export const DEFAULT_IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion'; +import { + GcpIapTokenInfo as _GcpIapTokenInfo, + GcpIapResult as _GcpIapResult, +} from '@backstage/plugin-auth-backend-module-gcp-iap-provider'; /** * The data extracted from an IAP token. * * @public + * @deprecated import from `@backstage/plugin-auth-backend-module-gcp-iap-provider` instead */ -export type GcpIapTokenInfo = { - /** - * The unique, stable identifier for the user. - */ - sub: string; - /** - * User email address. - */ - email: string; - /** - * Other fields. - */ - [key: string]: JsonValue; -}; +export type GcpIapTokenInfo = _GcpIapTokenInfo; /** * The result of the initial auth challenge. This is the input to the auth * callbacks. * * @public + * @deprecated import from `@backstage/plugin-auth-backend-module-gcp-iap-provider` instead */ -export type GcpIapResult = { - /** - * The data extracted from the IAP token header. - */ - iapToken: GcpIapTokenInfo; -}; - -/** - * The provider info to return to the frontend. - */ -export type GcpIapProviderInfo = { - /** - * The data extracted from the IAP token header. - */ - iapToken: GcpIapTokenInfo; -}; - -/** - * The shape of the response to return to callers. - */ -export type GcpIapResponse = AuthResponse; +export type GcpIapResult = _GcpIapResult; diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts deleted file mode 100644 index ff750223d3..0000000000 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ /dev/null @@ -1,410 +0,0 @@ -/* - * 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 { Profile as PassportProfile } from 'passport'; -import { GithubAuthProvider, GithubOAuthResult, github } from './provider'; -import * as helpers from '../../lib/passport/PassportStrategyHelper'; -import { makeProfileInfo } from '../../lib/passport/PassportStrategyHelper'; -import { OAuthStartRequest, encodeState } from '../../lib/oauth'; -import { AuthResolverContext } from '../types'; - -jest.mock('../../lib/passport/PassportStrategyHelper', () => { - return { - ...jest.requireActual('../../lib/passport/PassportStrategyHelper'), - executeFrameHandlerStrategy: jest.fn(), - executeRefreshTokenStrategy: jest.fn(), - executeFetchUserProfileStrategy: jest.fn(), - }; -}); - -const mockFrameHandler = jest.spyOn( - helpers, - 'executeFrameHandlerStrategy', -) as unknown as jest.MockedFunction< - () => Promise<{ - result: GithubOAuthResult; - privateInfo: { refreshToken?: string }; - }> ->; - -describe('GithubAuthProvider', () => { - const provider = new GithubAuthProvider({ - resolverContext: { - signInWithCatalogUser: jest.fn(({ entityRef }) => ({ - token: `token-for-user:${entityRef.name}`, - })), - } as unknown as AuthResolverContext, - signInResolver: github.resolvers.usernameMatchingUserEntityName(), - authHandler: async ({ fullProfile }) => ({ - profile: makeProfileInfo(fullProfile), - }), - stateEncoder: async (req: OAuthStartRequest) => ({ - encodedState: encodeState(req.state), - }), - callbackUrl: 'mock', - clientId: 'mock', - clientSecret: 'mock', - }); - - describe('should transform to type OAuthResponse', () => { - it('when all fields are present, it should be able to map them', async () => { - const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const fullProfile = { - id: 'uid-123', - username: 'jimmymarkum', - provider: 'github', - displayName: 'Jimmy Markum', - emails: [ - { - value: 'jimmymarkum@gmail.com', - }, - ], - photos: [ - { - value: - 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', - }, - ], - }; - - const params = { - scope: 'read:scope', - }; - - const expected = { - backstageIdentity: { - token: 'token-for-user:jimmymarkum', - }, - providerInfo: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - scope: 'read:scope', - expiresInSeconds: 3600, - }, - profile: { - email: 'jimmymarkum@gmail.com', - displayName: 'Jimmy Markum', - picture: - 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', - }, - }; - - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile, accessToken, params }, - privateInfo: {}, - }); - const { response } = await provider.handler({} as any); - expect(response).toEqual(expected); - }); - - it('when "email" is missing, it should be able to create the profile without it', async () => { - const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const fullProfile = { - id: 'uid-123', - username: 'jimmymarkum', - provider: 'github', - displayName: 'Jimmy Markum', - emails: null, - photos: [ - { - value: - 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', - }, - ], - } as unknown as PassportProfile; - - const params = { - scope: 'read:scope', - }; - - const expected = { - backstageIdentity: { - token: 'token-for-user:jimmymarkum', - }, - providerInfo: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - scope: 'read:scope', - expiresInSeconds: 3600, - }, - profile: { - displayName: 'Jimmy Markum', - picture: - 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', - }, - }; - - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile, accessToken, params }, - privateInfo: {}, - }); - const { response } = await provider.handler({} as any); - expect(response).toEqual(expected); - }); - - it('when "displayName" is missing, it should be able to create the profile and map "displayName" with "username"', async () => { - const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const fullProfile = { - id: 'uid-123', - username: 'jimmymarkum', - provider: 'github', - displayName: null, - emails: null, - photos: [ - { - value: - 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', - }, - ], - } as unknown as PassportProfile; - - const params = { - scope: 'read:scope', - }; - const expected = { - backstageIdentity: { - token: 'token-for-user:jimmymarkum', - }, - providerInfo: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - scope: 'read:scope', - expiresInSeconds: 3600, - }, - profile: { - displayName: 'jimmymarkum', - picture: - 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', - }, - }; - - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile, accessToken, params }, - privateInfo: {}, - }); - const { response } = await provider.handler({} as any); - expect(response).toEqual(expected); - }); - - it('when "photos" is missing, it should be able to create the profile without it', async () => { - const accessToken = - 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe'; - const fullProfile = { - id: 'ipd12039', - username: 'daveboyle', - provider: 'github', - displayName: 'Dave Boyle', - emails: [ - { - value: 'daveboyle@github.org', - }, - ], - }; - - const params = { - scope: 'read:user', - }; - - const expected = { - backstageIdentity: { - token: 'token-for-user:daveboyle', - }, - providerInfo: { - accessToken: - 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', - scope: 'read:user', - expiresInSeconds: 3600, - }, - profile: { - displayName: 'Dave Boyle', - email: 'daveboyle@github.org', - }, - }; - - mockFrameHandler.mockResolvedValueOnce({ - result: { fullProfile, accessToken, params }, - privateInfo: {}, - }); - const { response } = await provider.handler({} as any); - expect(response).toEqual(expected); - }); - - it('should forward a refresh token', async () => { - mockFrameHandler.mockResolvedValueOnce({ - result: { - fullProfile: { - id: 'ipd12039', - username: 'daveboyle', - provider: 'github', - displayName: 'Dave Boyle', - }, - accessToken: 'a.b.c', - params: { - scope: 'read:user', - expires_in: '123', - }, - }, - privateInfo: { refreshToken: 'refresh-me' }, - }); - - const response = await provider.handler({} as any); - - expect(response).toEqual({ - response: { - backstageIdentity: { - token: 'token-for-user:daveboyle', - }, - providerInfo: { - accessToken: 'a.b.c', - scope: 'read:user', - expiresInSeconds: 123, - }, - profile: { - displayName: 'Dave Boyle', - }, - }, - refreshToken: 'refresh-me', - }); - }); - - it('should fail if username is not available', async () => { - mockFrameHandler.mockResolvedValueOnce({ - result: { - fullProfile: { - id: 'ipd12039', - provider: 'github', - displayName: 'Dave Boyle', - }, - accessToken: 'a.b.c', - params: { - scope: 'read:user', - expires_in: '123', - }, - }, - privateInfo: { refreshToken: 'refresh-me' }, - }); - - await expect(provider.handler({} as any)).rejects.toThrow( - 'GitHub user profile does not contain a username', - ); - }); - - it('should forward a new refresh token on refresh', async () => { - const mockRefreshToken = jest.spyOn( - helpers, - 'executeRefreshTokenStrategy', - ) as unknown as jest.MockedFunction<() => Promise<{}>>; - - mockRefreshToken.mockResolvedValueOnce({ - accessToken: 'a.b.c', - refreshToken: 'dont-forget-to-send-refresh', - params: { - id_token: 'my-id', - expires_in: '123', - scope: 'read_user', - }, - }); - - const mockUserProfile = jest.spyOn( - helpers, - 'executeFetchUserProfileStrategy', - ) as unknown as jest.MockedFunction<() => Promise>; - - mockUserProfile.mockResolvedValueOnce({ - id: 'mockid', - username: 'mockuser', - provider: 'github', - displayName: 'Mocked User', - emails: [ - { - value: 'mockuser@gmail.com', - }, - ], - }); - - const result = await provider.refresh({ scope: 'actual-scope' } as any); - - expect(result).toEqual({ - response: { - backstageIdentity: { - token: 'token-for-user:mockuser', - }, - profile: { - displayName: 'Mocked User', - email: 'mockuser@gmail.com', - picture: undefined, - }, - providerInfo: { - accessToken: 'a.b.c', - expiresInSeconds: 123, - scope: 'actual-scope', - }, - }, - refreshToken: 'dont-forget-to-send-refresh', - }); - - mockRefreshToken.mockRestore(); - mockUserProfile.mockRestore(); - }); - - it('should use access token as refresh token', async () => { - const mockUserProfile = jest.spyOn( - helpers, - 'executeFetchUserProfileStrategy', - ) as unknown as jest.MockedFunction<() => Promise>; - - mockUserProfile.mockResolvedValueOnce({ - id: 'mockid', - username: 'mockuser', - provider: 'github', - displayName: 'Mocked User', - emails: [ - { - value: 'mockuser@gmail.com', - }, - ], - }); - - const result = await provider.refresh({ - refreshToken: 'access-token.le-token', - scope: 'the-scope', - } as any); - - expect(mockUserProfile).toHaveBeenCalledTimes(1); - expect(mockUserProfile).toHaveBeenCalledWith( - expect.anything(), - 'le-token', - ); - expect(result).toEqual({ - response: { - backstageIdentity: { - token: 'token-for-user:mockuser', - }, - profile: { - displayName: 'Mocked User', - email: 'mockuser@gmail.com', - picture: undefined, - }, - providerInfo: { - accessToken: 'le-token', - expiresInSeconds: 3600, - scope: 'the-scope', - }, - }, - refreshToken: 'access-token.le-token', - }); - - mockUserProfile.mockRestore(); - }); - }); -}); diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 11a71ca018..51595b0428 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -14,41 +14,16 @@ * limitations under the License. */ -import express from 'express'; import { Profile as PassportProfile } from 'passport'; -import { Strategy as GithubStrategy } from 'passport-github2'; -import { - executeFetchUserProfileStrategy, - executeFrameHandlerStrategy, - executeRedirectStrategy, - executeRefreshTokenStrategy, - makeProfileInfo, - PassportDoneCallback, -} from '../../lib/passport'; -import { - OAuthStartResponse, - AuthHandler, - SignInResolver, - StateEncoder, - AuthResolverContext, -} from '../types'; -import { - OAuthAdapter, - OAuthProviderOptions, - OAuthHandlers, - OAuthEnvironmentHandler, - OAuthStartRequest, - encodeState, - OAuthRefreshRequest, -} from '../../lib/oauth'; +import { AuthHandler, StateEncoder } from '../types'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { BACKSTAGE_SESSION_EXPIRATION } from '../../lib/session'; - -const ACCESS_TOKEN_PREFIX = 'access-token.'; - -type PrivateInfo = { - refreshToken?: string; -}; +import { + createOAuthProviderFactory, + OAuthAuthenticatorResult, + ProfileTransform, + SignInResolver, +} from '@backstage/plugin-auth-node'; +import { githubAuthenticator } from '@backstage/plugin-auth-backend-module-github-provider'; /** @public */ export type GithubOAuthResult = { @@ -62,170 +37,6 @@ export type GithubOAuthResult = { refreshToken?: string; }; -export type GithubAuthProviderOptions = OAuthProviderOptions & { - tokenUrl?: string; - userProfileUrl?: string; - authorizationUrl?: string; - signInResolver?: SignInResolver; - authHandler: AuthHandler; - stateEncoder: StateEncoder; - resolverContext: AuthResolverContext; -}; - -export class GithubAuthProvider implements OAuthHandlers { - private readonly _strategy: GithubStrategy; - private readonly signInResolver?: SignInResolver; - private readonly authHandler: AuthHandler; - private readonly resolverContext: AuthResolverContext; - private readonly stateEncoder: StateEncoder; - - constructor(options: GithubAuthProviderOptions) { - this.signInResolver = options.signInResolver; - this.authHandler = options.authHandler; - this.stateEncoder = options.stateEncoder; - this.resolverContext = options.resolverContext; - this._strategy = new GithubStrategy( - { - clientID: options.clientId, - clientSecret: options.clientSecret, - callbackURL: options.callbackUrl, - tokenURL: options.tokenUrl, - userProfileURL: options.userProfileUrl, - authorizationURL: options.authorizationUrl, - }, - ( - accessToken: any, - refreshToken: any, - params: any, - fullProfile: any, - done: PassportDoneCallback, - ) => { - done(undefined, { fullProfile, params, accessToken }, { refreshToken }); - }, - ); - } - - async start(req: OAuthStartRequest): Promise { - return await executeRedirectStrategy(req, this._strategy, { - scope: req.scope, - state: (await this.stateEncoder(req)).encodedState, - }); - } - - async handler(req: express.Request) { - const { result, privateInfo } = await executeFrameHandlerStrategy< - GithubOAuthResult, - PrivateInfo - >(req, this._strategy); - - let refreshToken = privateInfo.refreshToken; - - // If we do not have a real refresh token and we have a non-expiring - // access token, then we use that as our refresh token. - if (!refreshToken && !result.params.expires_in) { - refreshToken = ACCESS_TOKEN_PREFIX + result.accessToken; - } - - return { - response: await this.handleResult(result), - refreshToken, - }; - } - - async refresh(req: OAuthRefreshRequest) { - // We've enable persisting scope in the OAuth provider, so scope here will - // be whatever was stored in the cookie - const { scope, refreshToken } = req; - - // This is the OAuth App flow. A non-expiring access token is stored in the - // refresh token cookie. We use that token to fetch the user profile and - // refresh the Backstage session when needed. - if (refreshToken?.startsWith(ACCESS_TOKEN_PREFIX)) { - const accessToken = refreshToken.slice(ACCESS_TOKEN_PREFIX.length); - - const fullProfile = await executeFetchUserProfileStrategy( - this._strategy, - accessToken, - ).catch(error => { - if (error.oauthError?.statusCode === 401) { - throw new Error('Invalid access token'); - } - throw error; - }); - - return { - response: await this.handleResult({ - fullProfile, - params: { scope }, - accessToken, - }), - refreshToken, - }; - } - - // This is the App flow, which is close to a standard OAuth refresh flow. It has a - // pretty long session expiration, and it also ignores the requested scope, instead - // just allowing access to whatever is configured as part of the app installation. - const result = await executeRefreshTokenStrategy( - this._strategy, - refreshToken, - scope, - ); - return { - response: await this.handleResult({ - fullProfile: await executeFetchUserProfileStrategy( - this._strategy, - result.accessToken, - ), - params: { ...result.params, scope }, - accessToken: result.accessToken, - }), - refreshToken: result.refreshToken, - }; - } - - private async handleResult(result: GithubOAuthResult) { - const { profile } = await this.authHandler(result, this.resolverContext); - - const expiresInStr = result.params.expires_in; - let expiresInSeconds = - expiresInStr === undefined ? undefined : Number(expiresInStr); - - let backstageIdentity = undefined; - - if (this.signInResolver) { - backstageIdentity = await this.signInResolver( - { - result, - profile, - }, - this.resolverContext, - ); - - // GitHub sessions last longer than Backstage sessions, so if we're using - // GitHub for sign-in, then we need to expire the sessions earlier - if (expiresInSeconds) { - expiresInSeconds = Math.min( - expiresInSeconds, - BACKSTAGE_SESSION_EXPIRATION, - ); - } else { - expiresInSeconds = BACKSTAGE_SESSION_EXPIRATION; - } - } - - return { - backstageIdentity, - providerInfo: { - accessToken: result.accessToken, - scope: result.params.scope, - expiresInSeconds, - }, - profile, - }; - } -} - /** * Auth provider integration for GitHub auth * @@ -267,60 +78,55 @@ export const github = createAuthProviderIntegration({ */ stateEncoder?: StateEncoder; }) { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const enterpriseInstanceUrl = envConfig - .getOptionalString('enterpriseInstanceUrl') - ?.replace(/\/$/, ''); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const authorizationUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/authorize` - : undefined; - const tokenUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/access_token` - : undefined; - const userProfileUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/api/v3/user` - : undefined; - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; - - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile }) => ({ - profile: makeProfileInfo(fullProfile), - }); - - const stateEncoder: StateEncoder = - options?.stateEncoder ?? - (async ( - req: OAuthStartRequest, - ): Promise<{ encodedState: string }> => { - return { encodedState: encodeState(req.state) }; - }); - - const provider = new GithubAuthProvider({ - clientId, - clientSecret, - callbackUrl, - tokenUrl, - userProfileUrl, - authorizationUrl, - signInResolver: options?.signIn?.resolver, - authHandler, - stateEncoder, - resolverContext, - }); - - return OAuthAdapter.fromConfig(globalConfig, provider, { - persistScopes: true, - providerId, - callbackUrl, - }); - }); + const authHandler = options?.authHandler; + const signInResolver = options?.signIn?.resolver; + return createOAuthProviderFactory({ + authenticator: githubAuthenticator, + profileTransform: + authHandler && + ((async (result, ctx) => + authHandler!( + { + fullProfile: result.fullProfile, + accessToken: result.session.accessToken, + params: { + scope: result.session.scope, + expires_in: result.session.expiresInSeconds + ? String(result.session.expiresInSeconds) + : '', + refresh_token_expires_in: result.session + .refreshTokenExpiresInSeconds + ? String(result.session.refreshTokenExpiresInSeconds) + : '', + }, + }, + ctx, + )) as ProfileTransform>), + signInResolver: + signInResolver && + ((async ({ profile, result }, ctx) => + signInResolver( + { + profile: profile, + result: { + fullProfile: result.fullProfile, + accessToken: result.session.accessToken, + refreshToken: result.session.refreshToken, + params: { + scope: result.session.scope, + expires_in: result.session.expiresInSeconds + ? String(result.session.expiresInSeconds) + : '', + refresh_token_expires_in: result.session + .refreshTokenExpiresInSeconds + ? String(result.session.refreshTokenExpiresInSeconds) + : '', + }, + }, + }, + ctx, + )) as SignInResolver>), + }); }, resolvers: { /** diff --git a/plugins/auth-backend/src/providers/gitlab/provider.test.ts b/plugins/auth-backend/src/providers/gitlab/provider.test.ts deleted file mode 100644 index f9e1442661..0000000000 --- a/plugins/auth-backend/src/providers/gitlab/provider.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -/* - * 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 { - GitlabAuthProvider, - gitlabUsernameEntityNameSignInResolver, -} from './provider'; -import * as helpers from '../../lib/passport/PassportStrategyHelper'; -import { PassportProfile } from '../../lib/passport/types'; -import { OAuthResult } from '../../lib/oauth'; -import { AuthResolverContext } from '../types'; - -jest.mock('../../lib/passport/PassportStrategyHelper', () => { - return { - executeFrameHandlerStrategy: jest.fn(), - executeRefreshTokenStrategy: jest.fn(), - executeFetchUserProfileStrategy: jest.fn(), - }; -}); - -const mockFrameHandler = jest.spyOn( - helpers, - 'executeFrameHandlerStrategy', -) as unknown as jest.MockedFunction<() => Promise<{ result: OAuthResult }>>; - -describe('GitlabAuthProvider', () => { - const provider = new GitlabAuthProvider({ - clientId: 'mock', - clientSecret: 'mock', - callbackUrl: 'mock', - baseUrl: 'mock', - resolverContext: { - signInWithCatalogUser: jest.fn(async ({ entityRef }) => ({ - token: `token-for-user:${entityRef.name}`, - })), - } as unknown as AuthResolverContext, - authHandler: async ({ fullProfile }) => ({ - profile: { - email: fullProfile.emails![0]!.value, - displayName: fullProfile.displayName, - picture: 'http://gitlab.com/lols', - }, - }), - signInResolver: gitlabUsernameEntityNameSignInResolver, - }); - - it('should transform to type OAuthResponse', async () => { - const tests = [ - { - input: { - result: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - fullProfile: { - id: 'uid-123', - username: 'jimmymarkum', - provider: 'gitlab', - displayName: 'Jimmy Markum', - emails: [ - { - value: 'jimmymarkum@gmail.com', - }, - ], - avatarUrl: - 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', - }, - params: { - scope: 'user_read write_repository', - expires_in: 100, - }, - }, - privateInfo: { - refreshToken: 'gacn9jdgm19me19xasczxcm9n7', - }, - }, - expect: { - backstageIdentity: { - token: 'token-for-user:jimmymarkum', - }, - providerInfo: { - accessToken: '19xasczxcm9n7gacn9jdgm19me', - expiresInSeconds: 100, - scope: 'user_read write_repository', - idToken: undefined, - }, - profile: { - email: 'jimmymarkum@gmail.com', - displayName: 'Jimmy Markum', - picture: 'http://gitlab.com/lols', - }, - }, - }, - { - input: { - result: { - accessToken: - 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', - fullProfile: { - id: 'ipd12039', - username: 'daveboyle', - provider: 'gitlab', - displayName: 'Dave Boyle', - emails: [ - { - value: 'daveboyle@gitlab.org', - }, - ], - }, - params: { - scope: 'read_repository', - expires_in: 200, - }, - }, - privateInfo: { - refreshToken: 'gacn96f3y6y5jdgm19mec348nqrty719xasczf356yxcm9n7', - }, - }, - expect: { - backstageIdentity: { - token: 'token-for-user:daveboyle', - }, - providerInfo: { - accessToken: - 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', - expiresInSeconds: 200, - idToken: undefined, - scope: 'read_repository', - }, - profile: { - displayName: 'Dave Boyle', - email: 'daveboyle@gitlab.org', - picture: 'http://gitlab.com/lols', - }, - }, - }, - ]; - - for (const test of tests) { - mockFrameHandler.mockResolvedValueOnce(test.input); - const { response } = await provider.handler({} as any); - expect(response).toEqual(test.expect); - } - }); - - it('should forward a new refresh token on refresh', async () => { - const mockRefreshToken = jest.spyOn( - helpers, - 'executeRefreshTokenStrategy', - ) as unknown as jest.MockedFunction<() => Promise<{}>>; - - mockRefreshToken.mockResolvedValueOnce({ - accessToken: 'a.b.c', - refreshToken: 'dont-forget-to-send-refresh', - params: { - id_token: 'my-id', - scope: 'read_user', - }, - }); - - const mockUserProfile = jest.spyOn( - helpers, - 'executeFetchUserProfileStrategy', - ) as unknown as jest.MockedFunction<() => Promise>; - - mockUserProfile.mockResolvedValueOnce({ - id: 'uid-my-id', - username: 'mockuser', - provider: 'gitlab', - displayName: 'Mocked User', - emails: [ - { - value: 'mockuser@gmail.com', - }, - ], - }); - - const result = await provider.refresh({} as any); - - expect(result).toEqual({ - response: { - backstageIdentity: { - token: 'token-for-user:mockuser', - }, - profile: { - displayName: 'Mocked User', - email: 'mockuser@gmail.com', - picture: 'http://gitlab.com/lols', - }, - providerInfo: { - accessToken: 'a.b.c', - idToken: 'my-id', - scope: 'read_user', - }, - }, - refreshToken: 'dont-forget-to-send-refresh', - }); - }); -}); diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 31efafa4ee..2551d334ec 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -14,172 +14,15 @@ * limitations under the License. */ -import express from 'express'; -import { Strategy as GitlabStrategy } from 'passport-gitlab2'; -import { - executeRedirectStrategy, - executeFrameHandlerStrategy, - executeRefreshTokenStrategy, - executeFetchUserProfileStrategy, - makeProfileInfo, - PassportDoneCallback, -} from '../../lib/passport'; -import { - OAuthStartResponse, - SignInResolver, - AuthHandler, - AuthResolverContext, -} from '../types'; -import { - OAuthAdapter, - OAuthProviderOptions, - OAuthHandlers, - OAuthResponse, - OAuthEnvironmentHandler, - OAuthStartRequest, - OAuthRefreshRequest, - encodeState, - OAuthResult, -} from '../../lib/oauth'; +import { SignInResolver, AuthHandler } from '../types'; +import { OAuthResult } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; - -type PrivateInfo = { - refreshToken: string; -}; - -export type GitlabAuthProviderOptions = OAuthProviderOptions & { - baseUrl: string; - signInResolver?: SignInResolver; - authHandler: AuthHandler; - resolverContext: AuthResolverContext; -}; - -export const gitlabUsernameEntityNameSignInResolver: SignInResolver< - OAuthResult -> = async (info, ctx) => { - const { result } = info; - - const id = result.fullProfile.username; - if (!id) { - throw new Error(`GitLab user profile does not contain a username`); - } - - return ctx.signInWithCatalogUser({ entityRef: { name: id } }); -}; - -export const gitlabDefaultAuthHandler: AuthHandler = async ({ - fullProfile, - params, -}) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), -}); - -export class GitlabAuthProvider implements OAuthHandlers { - private readonly _strategy: GitlabStrategy; - private readonly signInResolver?: SignInResolver; - private readonly authHandler: AuthHandler; - private readonly resolverContext: AuthResolverContext; - - constructor(options: GitlabAuthProviderOptions) { - this.resolverContext = options.resolverContext; - this.authHandler = options.authHandler; - this.signInResolver = options.signInResolver; - - this._strategy = new GitlabStrategy( - { - clientID: options.clientId, - clientSecret: options.clientSecret, - callbackURL: options.callbackUrl, - baseURL: options.baseUrl, - authorizationURL: `${options.baseUrl}/oauth/authorize`, - tokenURL: `${options.baseUrl}/oauth/token`, - profileURL: `${options.baseUrl}/api/v4/user`, - }, - ( - accessToken: any, - refreshToken: any, - params: any, - fullProfile: any, - done: PassportDoneCallback, - ) => { - done( - undefined, - { fullProfile, params, accessToken }, - { - refreshToken, - }, - ); - }, - ); - } - - async start(req: OAuthStartRequest): Promise { - return await executeRedirectStrategy(req, this._strategy, { - scope: req.scope, - state: encodeState(req.state), - }); - } - - async handler(req: express.Request) { - const { result, privateInfo } = await executeFrameHandlerStrategy< - OAuthResult, - PrivateInfo - >(req, this._strategy); - - return { - response: await this.handleResult(result), - refreshToken: privateInfo.refreshToken, - }; - } - - async refresh(req: OAuthRefreshRequest) { - const { accessToken, refreshToken, params } = - await executeRefreshTokenStrategy( - this._strategy, - req.refreshToken, - req.scope, - ); - - const fullProfile = await executeFetchUserProfileStrategy( - this._strategy, - accessToken, - ); - return { - response: await this.handleResult({ - fullProfile, - params, - accessToken, - }), - refreshToken, - }; - } - - private async handleResult(result: OAuthResult): Promise { - const { profile } = await this.authHandler(result, this.resolverContext); - - const response: OAuthResponse = { - providerInfo: { - idToken: result.params.id_token, - accessToken: result.accessToken, - scope: result.params.scope, - expiresInSeconds: result.params.expires_in, - }, - profile, - }; - - if (this.signInResolver) { - response.backstageIdentity = await this.signInResolver( - { - result, - profile, - }, - this.resolverContext, - ); - } - - return response; - } -} +import { createOAuthProviderFactory } from '@backstage/plugin-auth-node'; +import { + adaptLegacyOAuthHandler, + adaptLegacyOAuthSignInResolver, +} from '../../lib/legacy'; +import { gitlabAuthenticator } from '@backstage/plugin-auth-backend-module-gitlab-provider'; /** * Auth provider integration for GitLab auth @@ -201,34 +44,10 @@ export const gitlab = createAuthProviderIntegration({ resolver: SignInResolver; }; }) { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const audience = envConfig.getOptionalString('audience'); - const baseUrl = audience || 'https://gitlab.com'; - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; - - const authHandler: AuthHandler = - options?.authHandler ?? gitlabDefaultAuthHandler; - - const provider = new GitlabAuthProvider({ - clientId, - clientSecret, - callbackUrl, - baseUrl, - authHandler, - signInResolver: options?.signIn?.resolver, - resolverContext, - }); - - return OAuthAdapter.fromConfig(globalConfig, provider, { - providerId, - callbackUrl, - }); - }); + return createOAuthProviderFactory({ + authenticator: gitlabAuthenticator, + profileTransform: adaptLegacyOAuthHandler(options?.authHandler), + signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver), + }); }, }); diff --git a/plugins/auth-backend/src/providers/google/provider.test.ts b/plugins/auth-backend/src/providers/google/provider.test.ts index 3c0b367be2..abde2b66d1 100644 --- a/plugins/auth-backend/src/providers/google/provider.test.ts +++ b/plugins/auth-backend/src/providers/google/provider.test.ts @@ -14,74 +14,57 @@ * limitations under the License. */ -import { GoogleAuthProvider } from './provider'; -import * as helpers from '../../lib/passport/PassportStrategyHelper'; -import { OAuthResult } from '../../lib/oauth'; -import { AuthResolverContext } from '../types'; +import { googleAuthenticator } from '@backstage/plugin-auth-backend-module-google-provider'; +import { createOAuthProviderFactory } from '@backstage/plugin-auth-node'; +import { google } from './provider'; -jest.mock('../../lib/passport/PassportStrategyHelper', () => { - return { - executeFrameHandlerStrategy: jest.fn(), - executeRefreshTokenStrategy: jest.fn(), - executeFetchUserProfileStrategy: jest.fn(), - }; -}); - -const mockFrameHandler = jest.spyOn( - helpers, - 'executeFrameHandlerStrategy', -) as unknown as jest.MockedFunction< - () => Promise<{ result: OAuthResult; privateInfo: any }> ->; +jest.mock('@backstage/plugin-auth-node', () => ({ + ...jest.requireActual('@backstage/plugin-auth-node'), + createOAuthProviderFactory: jest.fn(() => 'provider-factory'), +})); describe('createGoogleProvider', () => { - it('should auth', async () => { - const provider = new GoogleAuthProvider({ - resolverContext: {} as AuthResolverContext, - authHandler: async ({ fullProfile }) => ({ - profile: { - email: fullProfile.emails![0]!.value, - displayName: fullProfile.displayName, - picture: 'http://google.com/lols', - }, - }), - clientId: 'mock', - clientSecret: 'mock', - callbackUrl: 'mock', - }); + afterEach(() => jest.clearAllMocks()); - mockFrameHandler.mockResolvedValueOnce({ - result: { - fullProfile: { - emails: [{ value: 'conrad@example.com' }], - displayName: 'Conrad', - id: 'conrad', - provider: 'google', - }, - params: { - id_token: 'idToken', - scope: 'scope', - expires_in: 123, - }, - accessToken: 'accessToken', - }, - privateInfo: { - refreshToken: 'wacka', - }, + it('should be created', async () => { + expect(google.create()).toBe('provider-factory'); + + expect(createOAuthProviderFactory).toHaveBeenCalledWith({ + authenticator: googleAuthenticator, }); - const { response } = await provider.handler({} as any); - expect(response).toEqual({ - providerInfo: { - accessToken: 'accessToken', - expiresInSeconds: 123, - idToken: 'idToken', - scope: 'scope', - }, - profile: { - email: 'conrad@example.com', - displayName: 'Conrad', - picture: 'http://google.com/lols', - }, + }); + + it('should be created with sign-in resolver', async () => { + expect(google.create({ signIn: { resolver: jest.fn() } })).toBe( + 'provider-factory', + ); + + expect(createOAuthProviderFactory).toHaveBeenCalledWith({ + authenticator: googleAuthenticator, + signInResolver: expect.any(Function), + }); + }); + + it('should be created with sign-in resolver and auth handler', async () => { + expect( + google.create({ + signIn: { resolver: jest.fn() }, + authHandler: jest.fn(), + }), + ).toBe('provider-factory'); + + expect(createOAuthProviderFactory).toHaveBeenCalledWith({ + authenticator: googleAuthenticator, + signInResolver: expect.any(Function), + profileTransform: expect.any(Function), + }); + }); + + it('should have resolvers', () => { + expect(google.resolvers).toEqual({ + emailLocalPartMatchingUserEntityName: expect.any(Function), + emailMatchingUserEntityAnnotation: expect.any(Function), + emailMatchingUserEntityProfileEmail: expect.any(Function), }); }); }); diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index afe5c27f6b..d467977094 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -14,166 +14,22 @@ * limitations under the License. */ -import express from 'express'; -import passport from 'passport'; -import { OAuth2Client } from 'google-auth-library'; -import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; import { - encodeState, - OAuthAdapter, - OAuthEnvironmentHandler, - OAuthHandlers, - OAuthProviderOptions, - OAuthRefreshRequest, - OAuthResponse, - OAuthResult, - OAuthStartRequest, - OAuthLogoutRequest, -} from '../../lib/oauth'; + googleAuthenticator, + googleSignInResolvers, +} from '@backstage/plugin-auth-backend-module-google-provider'; import { - executeFetchUserProfileStrategy, - executeFrameHandlerStrategy, - executeRedirectStrategy, - executeRefreshTokenStrategy, - makeProfileInfo, - PassportDoneCallback, -} from '../../lib/passport'; + commonSignInResolvers, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; import { - AuthHandler, - AuthResolverContext, - OAuthStartResponse, - SignInResolver, -} from '../types'; + adaptLegacyOAuthHandler, + adaptLegacyOAuthSignInResolver, + adaptOAuthSignInResolverToLegacy, +} from '../../lib/legacy'; +import { OAuthResult } from '../../lib/oauth'; import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { - commonByEmailLocalPartResolver, - commonByEmailResolver, -} from '../resolvers'; - -type PrivateInfo = { - refreshToken: string; -}; - -type Options = OAuthProviderOptions & { - signInResolver?: SignInResolver; - authHandler: AuthHandler; - resolverContext: AuthResolverContext; -}; - -export class GoogleAuthProvider implements OAuthHandlers { - private readonly strategy: GoogleStrategy; - private readonly signInResolver?: SignInResolver; - private readonly authHandler: AuthHandler; - private readonly resolverContext: AuthResolverContext; - - constructor(options: Options) { - this.authHandler = options.authHandler; - this.signInResolver = options.signInResolver; - this.resolverContext = options.resolverContext; - this.strategy = new GoogleStrategy( - { - clientID: options.clientId, - clientSecret: options.clientSecret, - callbackURL: options.callbackUrl, - passReqToCallback: false, - }, - ( - accessToken: any, - refreshToken: any, - params: any, - fullProfile: passport.Profile, - done: PassportDoneCallback, - ) => { - done( - undefined, - { - fullProfile, - params, - accessToken, - refreshToken, - }, - { - refreshToken, - }, - ); - }, - ); - } - - async start(req: OAuthStartRequest): Promise { - return await executeRedirectStrategy(req, this.strategy, { - accessType: 'offline', - prompt: 'consent', - scope: req.scope, - state: encodeState(req.state), - }); - } - - async handler(req: express.Request) { - const { result, privateInfo } = await executeFrameHandlerStrategy< - OAuthResult, - PrivateInfo - >(req, this.strategy); - - return { - response: await this.handleResult(result), - refreshToken: privateInfo.refreshToken, - }; - } - - async logout(req: OAuthLogoutRequest) { - const oauthClient = new OAuth2Client(); - await oauthClient.revokeToken(req.refreshToken); - } - - async refresh(req: OAuthRefreshRequest) { - const { accessToken, refreshToken, params } = - await executeRefreshTokenStrategy( - this.strategy, - req.refreshToken, - req.scope, - ); - const fullProfile = await executeFetchUserProfileStrategy( - this.strategy, - accessToken, - ); - - return { - response: await this.handleResult({ - fullProfile, - params, - accessToken, - }), - refreshToken, - }; - } - - private async handleResult(result: OAuthResult) { - const { profile } = await this.authHandler(result, this.resolverContext); - - const response: OAuthResponse = { - providerInfo: { - idToken: result.params.id_token, - accessToken: result.accessToken, - scope: result.params.scope, - expiresInSeconds: result.params.expires_in, - }, - profile, - }; - - if (this.signInResolver) { - response.backstageIdentity = await this.signInResolver( - { - result, - profile, - }, - this.resolverContext, - ); - } - - return response; - } -} +import { AuthHandler, SignInResolver } from '../types'; /** * Auth provider integration for Google auth @@ -198,62 +54,18 @@ export const google = createAuthProviderIntegration({ resolver: SignInResolver; }; }) { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; - - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); - - const provider = new GoogleAuthProvider({ - clientId, - clientSecret, - callbackUrl, - signInResolver: options?.signIn?.resolver, - authHandler, - resolverContext, - }); - - return OAuthAdapter.fromConfig(globalConfig, provider, { - providerId, - callbackUrl, - }); - }); - }, - resolvers: { - /** - * Looks up the user by matching their email local part to the entity name. - */ - emailLocalPartMatchingUserEntityName: () => commonByEmailLocalPartResolver, - /** - * Looks up the user by matching their email to the entity email. - */ - emailMatchingUserEntityProfileEmail: () => commonByEmailResolver, - /** - * Looks up the user by matching their email to the `google.com/email` annotation. - */ - emailMatchingUserEntityAnnotation(): SignInResolver { - return async (info, ctx) => { - const { profile } = info; - - if (!profile.email) { - throw new Error('Google profile contained no email'); - } - - return ctx.signInWithCatalogUser({ - annotations: { - 'google.com/email': profile.email, - }, - }); - }; - }, + return createOAuthProviderFactory({ + authenticator: googleAuthenticator, + profileTransform: adaptLegacyOAuthHandler(options?.authHandler), + signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver), + }); }, + resolvers: adaptOAuthSignInResolverToLegacy({ + emailLocalPartMatchingUserEntityName: + commonSignInResolvers.emailLocalPartMatchingUserEntityName(), + emailMatchingUserEntityProfileEmail: + commonSignInResolvers.emailMatchingUserEntityProfileEmail(), + emailMatchingUserEntityAnnotation: + googleSignInResolvers.emailMatchingUserEntityAnnotation(), + }), }); diff --git a/plugins/auth-backend/src/providers/microsoft/provider.test.ts b/plugins/auth-backend/src/providers/microsoft/provider.test.ts index 6f8df2d4df..7bbfd085d6 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.test.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.test.ts @@ -30,6 +30,10 @@ describe('MicrosoftAuthProvider', () => { const state = Buffer.from( `nonce=${encodeURIComponent(nonce)}&env=development`, ).toString('hex'); + const mockBackstageToken = `header.${Buffer.from( + JSON.stringify({ sub: 'user:default/mock' }), + 'utf8', + ).toString('base64')}.backstage`; const server = setupServer(); const microsoftApi = new FakeMicrosoftAPI(); @@ -45,6 +49,9 @@ describe('MicrosoftAuthProvider', () => { }, })({ providerId: 'microsoft', + baseUrl: 'http://backstage.test/api/auth', + appUrl: 'http://backstage.test', + isOriginAllowed: _ => true, globalConfig: { baseUrl: 'http://backstage.test/api/auth', appUrl: 'http://backstage.test', @@ -61,10 +68,9 @@ describe('MicrosoftAuthProvider', () => { resolverContext: { issueToken: jest.fn(), findCatalogUser: jest.fn(), - signInWithCatalogUser: _ => - Promise.resolve({ - token: 'header.e30K.backstage', - }), + signInWithCatalogUser: async _ => ({ + token: mockBackstageToken, + }), } as AuthResolverContext, }) as AuthProviderRouteHandlers; @@ -206,8 +212,12 @@ describe('MicrosoftAuthProvider', () => { displayName: 'Conrad', }, backstageIdentity: { - token: 'header.e30K.backstage', - identity: { type: 'user', ownershipEntityRefs: [] }, + token: mockBackstageToken, + identity: { + type: 'user', + userEntityRef: 'user:default/mock', + ownershipEntityRefs: [], + }, }, }, }), @@ -327,8 +337,12 @@ describe('MicrosoftAuthProvider', () => { displayName: 'Conrad', }, backstageIdentity: { - token: 'header.e30K.backstage', - identity: { type: 'user', ownershipEntityRefs: [] }, + token: mockBackstageToken, + identity: { + type: 'user', + userEntityRef: 'user:default/mock', + ownershipEntityRefs: [], + }, }, }, }), @@ -427,7 +441,7 @@ describe('MicrosoftAuthProvider', () => { expect(response.json).toHaveBeenCalledWith( expect.objectContaining({ backstageIdentity: expect.objectContaining({ - token: 'header.e30K.backstage', + token: mockBackstageToken, }), }), ); diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index bc581f6d10..8fc8459f10 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -47,7 +47,7 @@ import { commonByEmailLocalPartResolver, commonByEmailResolver, } from '../resolvers'; -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import fetch from 'node-fetch'; import { decodeJwt } from 'jose'; import { Profile as PassportProfile } from 'passport'; @@ -60,7 +60,7 @@ type PrivateInfo = { type Options = OAuthProviderOptions & { signInResolver?: SignInResolver; authHandler: AuthHandler; - logger: Logger; + logger: LoggerService; resolverContext: AuthResolverContext; authorizationUrl?: string; tokenUrl?: string; @@ -70,7 +70,7 @@ export class MicrosoftAuthProvider implements OAuthHandlers { private readonly _strategy: MicrosoftStrategy; private readonly signInResolver?: SignInResolver; private readonly authHandler: AuthHandler; - private readonly logger: Logger; + private readonly logger: LoggerService; private readonly resolverContext: AuthResolverContext; constructor(options: Options) { diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts index 0279604f2d..f585d4831f 100644 --- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts +++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.test.ts @@ -22,7 +22,7 @@ jest.mock('@backstage/catalog-client'); import { AuthenticationError } from '@backstage/errors'; import express from 'express'; import * as jose from 'jose'; -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { AuthHandler, AuthResolverContext, SignInResolver } from '../types'; import { oauth2Proxy, @@ -36,7 +36,7 @@ describe('Oauth2ProxyAuthProvider', () => { 'eyblob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iLCJlbnQiOlsidXNlcjpkZWZhdWx0L2ppbW15bWFya3VtIl19.eyblob'; let provider: Oauth2ProxyAuthProvider; - let logger: jest.Mocked; + let logger: jest.Mocked; let signInResolver: jest.MockedFunction< SignInResolver> >; @@ -53,7 +53,7 @@ describe('Oauth2ProxyAuthProvider', () => { >; authHandler = jest.fn(); signInResolver = jest.fn(); - logger = { error: jest.fn() } as unknown as jest.Mocked; + logger = { error: jest.fn() } as unknown as jest.Mocked; mockResponse = { status: jest.fn(), diff --git a/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts index 761618c225..1fa8f4a2fa 100644 --- a/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts +++ b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts @@ -14,34 +14,11 @@ * limitations under the License. */ -import { - BackstageIdentityResponse, - BackstageSignInResult, -} from '@backstage/plugin-auth-node'; - -function parseJwtPayload(token: string) { - const [_header, payload, _signature] = token.split('.'); - return JSON.parse(Buffer.from(payload, 'base64').toString()); -} +import { prepareBackstageIdentityResponse as _prepareBackstageIdentityResponse } from '@backstage/plugin-auth-node'; /** - * Parses a Backstage-issued token and decorates the - * {@link @backstage/plugin-auth-node#BackstageIdentityResponse} with identity information sourced from the - * token. - * * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export function prepareBackstageIdentityResponse( - result: BackstageSignInResult, -): BackstageIdentityResponse { - const { sub, ent } = parseJwtPayload(result.token); - - return { - ...result, - identity: { - type: 'user', - userEntityRef: sub, - ownershipEntityRefs: ent ?? [], - }, - }; -} +export const prepareBackstageIdentityResponse = + _prepareBackstageIdentityResponse; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 1459cff484..354387153c 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -14,126 +14,42 @@ * limitations under the License. */ -import { GetEntitiesRequest } from '@backstage/catalog-client'; -import { Entity } from '@backstage/catalog-model'; -import { Config } from '@backstage/config'; import { - BackstageIdentityResponse, - BackstageSignInResult, + AuthProviderConfig as _AuthProviderConfig, + AuthProviderRouteHandlers as _AuthProviderRouteHandlers, + AuthProviderFactory as _AuthProviderFactory, + AuthResolverCatalogUserQuery as _AuthResolverCatalogUserQuery, + AuthResolverContext as _AuthResolverContext, + ClientAuthResponse as _ClientAuthResponse, + CookieConfigurer as _CookieConfigurer, + ProfileInfo as _ProfileInfo, + SignInInfo as _SignInInfo, + SignInResolver as _SignInResolver, } from '@backstage/plugin-auth-node'; -import express from 'express'; -import { Logger } from 'winston'; -import { TokenParams } from '../identity/types'; import { OAuthStartRequest } from '../lib/oauth/types'; /** - * A query for a single user in the catalog. - * - * If `entityRef` is used, the default kind is `'User'`. - * - * If `annotations` are used, all annotations must be present and - * match the provided value exactly. Only entities of kind `'User'` will be considered. - * - * If `filter` are used they are passed on as they are to the `CatalogApi`. - * - * Regardless of the query method, the query must match exactly one entity - * in the catalog, or an error will be thrown. - * * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export type AuthResolverCatalogUserQuery = - | { - entityRef: - | string - | { - kind?: string; - namespace?: string; - name: string; - }; - } - | { - annotations: Record; - } - | { - filter: Exclude; - }; +export type AuthResolverCatalogUserQuery = _AuthResolverCatalogUserQuery; /** - * The context that is used for auth processing. - * * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export type AuthResolverContext = { - /** - * Issues a Backstage token using the provided parameters. - */ - issueToken(params: TokenParams): Promise<{ token: string }>; - - /** - * Finds a single user in the catalog using the provided query. - * - * See {@link AuthResolverCatalogUserQuery} for details. - */ - findCatalogUser( - query: AuthResolverCatalogUserQuery, - ): Promise<{ entity: Entity }>; - - /** - * Finds a single user in the catalog using the provided query, and then - * issues an identity for that user using default ownership resolution. - * - * See {@link AuthResolverCatalogUserQuery} for details. - */ - signInWithCatalogUser( - query: AuthResolverCatalogUserQuery, - ): Promise; -}; +export type AuthResolverContext = _AuthResolverContext; /** - * The callback used to resolve the cookie configuration for auth providers that use cookies. * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export type CookieConfigurer = (ctx: { - /** ID of the auth provider that this configuration applies to */ - providerId: string; - /** The externally reachable base URL of the auth-backend plugin */ - baseUrl: string; - /** The configured callback URL of the auth provider */ - callbackUrl: string; - /** The origin URL of the app */ - appOrigin: string; -}) => { - domain: string; - path: string; - secure: boolean; - sameSite?: 'none' | 'lax' | 'strict'; -}; +export type CookieConfigurer = _CookieConfigurer; -/** @public */ -export type AuthProviderConfig = { - /** - * The protocol://domain[:port] where the app is hosted. This is used to construct the - * callbackURL to redirect to once the user signs in to the auth provider. - */ - baseUrl: string; - - /** - * The base URL of the app as provided by app.baseUrl - */ - appUrl: string; - - /** - * A function that is called to check whether an origin is allowed to receive the authentication result. - */ - isOriginAllowed: (origin: string) => boolean; - - /** - * The function used to resolve cookie configuration based on the auth provider options. - */ - cookieConfigurer?: CookieConfigurer; -}; - -/** @public */ +/** + * @public + * @deprecated Use `createOAuthAuthenticator` from `@backstage/plugin-auth-node` instead + */ export type OAuthStartResponse = { /** * URL to redirect to @@ -146,138 +62,53 @@ export type OAuthStartResponse = { }; /** - * Any Auth provider needs to implement this interface which handles the routes in the - * auth backend. Any auth API requests from the frontend reaches these methods. - * - * The routes in the auth backend API are tied to these methods like below - * - * `/auth/[provider]/start -> start` - * `/auth/[provider]/handler/frame -> frameHandler` - * `/auth/[provider]/refresh -> refresh` - * `/auth/[provider]/logout -> logout` - * * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export interface AuthProviderRouteHandlers { - /** - * Handles the start route of the API. This initiates a sign in request with an auth provider. - * - * Request - * - scopes for the auth request (Optional) - * Response - * - redirect to the auth provider for the user to sign in or consent. - * - sets a nonce cookie and also pass the nonce as 'state' query parameter in the redirect request - */ - start(req: express.Request, res: express.Response): Promise; - - /** - * Once the user signs in or consents in the OAuth screen, the auth provider redirects to the - * callbackURL which is handled by this method. - * - * Request - * - to contain a nonce cookie and a 'state' query parameter - * Response - * - postMessage to the window with a payload that contains accessToken, expiryInSeconds?, idToken? and scope. - * - sets a refresh token cookie if the auth provider supports refresh tokens - */ - frameHandler(req: express.Request, res: express.Response): Promise; - - /** - * (Optional) If the auth provider supports refresh tokens then this method handles - * requests to get a new access token. - * - * Request - * - to contain a refresh token cookie and scope (Optional) query parameter. - * Response - * - payload with accessToken, expiryInSeconds?, idToken?, scope and user profile information. - */ - refresh?(req: express.Request, res: express.Response): Promise; - - /** - * (Optional) Handles sign out requests - * - * Response - * - removes the refresh token cookie - */ - logout?(req: express.Request, res: express.Response): Promise; -} - -/** @public */ -export type AuthProviderFactory = (options: { - providerId: string; - globalConfig: AuthProviderConfig; - config: Config; - logger: Logger; - resolverContext: AuthResolverContext; -}) => AuthProviderRouteHandlers; - -/** @public */ -export type AuthResponse = { - providerInfo: ProviderInfo; - profile: ProfileInfo; - backstageIdentity?: BackstageIdentityResponse; -}; +export type AuthProviderConfig = _AuthProviderConfig; /** - * Used to display login information to user, i.e. sidebar popup. - * - * It is also temporarily used as the profile of the signed-in user's Backstage - * identity, but we want to replace that with data from identity and/org catalog - * service - * * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export type ProfileInfo = { - /** - * Email ID of the signed in user. - */ - email?: string; - /** - * Display name that can be presented to the signed in user. - */ - displayName?: string; - /** - * URL to an image that can be used as the display image or avatar of the - * signed in user. - */ - picture?: string; -}; +export type AuthProviderRouteHandlers = _AuthProviderRouteHandlers; /** - * Type of sign in information context. Includes the profile information and - * authentication result which contains auth related information. - * * @public + * @deprecated import from `@backstage/plugin-auth-node` instead */ -export type SignInInfo = { - /** - * The simple profile passed down for use in the frontend. - */ - profile: ProfileInfo; - - /** - * The authentication result that was received from the authentication - * provider. - */ - result: TAuthResult; -}; +export type AuthProviderFactory = _AuthProviderFactory; /** - * Describes the function which handles the result of a successful - * authentication. Must return a valid {@link @backstage/plugin-auth-node#BackstageSignInResult}. - * * @public + * @deprecated import `ClientAuthResponse` from `@backstage/plugin-auth-node` instead */ -export type SignInResolver = ( - info: SignInInfo, - context: AuthResolverContext, -) => Promise; +export type AuthResponse = _ClientAuthResponse; + +/** + * @public + * @deprecated import from `@backstage/plugin-auth-node` instead + */ +export type ProfileInfo = _ProfileInfo; + +/** + * @public + * @deprecated import from `@backstage/plugin-auth-node` instead + */ +export type SignInInfo = _SignInInfo; + +/** + * @public + * @deprecated import from `@backstage/plugin-auth-node` instead + */ +export type SignInResolver = _SignInResolver; /** * The return type of an authentication handler. Must contain valid profile * information. * * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead */ export type AuthHandlerResult = { profile: ProfileInfo }; @@ -293,13 +124,17 @@ export type AuthHandlerResult = { profile: ProfileInfo }; * group of users. * * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead */ export type AuthHandler = ( input: TAuthResult, context: AuthResolverContext, ) => Promise; -/** @public */ +/** + * @public + * @deprecated Use `createOAuthRouteHandlers` from `@backstage/plugin-auth-node` instead + */ export type StateEncoder = ( req: OAuthStartRequest, ) => Promise<{ encodedState: string }>; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index fa8dd95727..e3bfc1c3f2 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -17,7 +17,7 @@ import express from 'express'; import Router from 'express-promise-router'; import cookieParser from 'cookie-parser'; -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { defaultAuthProviderFactories, AuthProviderFactory, @@ -44,13 +44,14 @@ export type ProviderFactories = { [s: string]: AuthProviderFactory }; /** @public */ export interface RouterOptions { - logger: Logger; + logger: LoggerService; database: PluginDatabaseManager; config: Config; discovery: PluginEndpointDiscovery; tokenManager: TokenManager; tokenFactoryAlgorithm?: string; providerFactories?: ProviderFactories; + disableDefaultProviderFactories?: boolean; catalogApi?: CatalogApi; } @@ -65,7 +66,7 @@ export async function createRouter( database, tokenManager, tokenFactoryAlgorithm, - providerFactories, + providerFactories = {}, catalogApi, } = options; const router = Router(); @@ -85,7 +86,9 @@ export async function createRouter( keyStore, keyDurationSeconds, logger: logger.child({ component: 'token-factory' }), - algorithm: tokenFactoryAlgorithm, + algorithm: + tokenFactoryAlgorithm ?? + config.getOptionalString('auth.identityTokenAlgorithm'), }); const secret = config.getOptionalString('auth.session.secret'); @@ -113,23 +116,28 @@ export async function createRouter( router.use(express.urlencoded({ extended: false })); router.use(express.json()); - const allProviderFactories = { - ...defaultAuthProviderFactories, - ...providerFactories, - }; - const providersConfig = config.getConfig('auth.providers'); - const configuredProviders = providersConfig.keys(); + const allProviderFactories = options.disableDefaultProviderFactories + ? providerFactories + : { + ...defaultAuthProviderFactories, + ...providerFactories, + }; + + const providersConfig = config.getOptionalConfig('auth.providers'); const isOriginAllowed = createOriginFilter(config); for (const [providerId, providerFactory] of Object.entries( allProviderFactories, )) { - if (configuredProviders.includes(providerId)) { + if (providersConfig?.has(providerId)) { logger.info(`Configuring auth provider: ${providerId}`); try { const provider = providerFactory({ providerId, + appUrl, + baseUrl: authUrl, + isOriginAllowed, globalConfig: { baseUrl: authUrl, appUrl, diff --git a/plugins/auth-backend/src/service/standaloneServer.ts b/plugins/auth-backend/src/service/standaloneServer.ts index 16ebbc9345..abda3a341c 100644 --- a/plugins/auth-backend/src/service/standaloneServer.ts +++ b/plugins/auth-backend/src/service/standaloneServer.ts @@ -23,11 +23,11 @@ import { } from '@backstage/backend-common'; import { Server } from 'http'; import Knex from 'knex'; -import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { createRouter } from './router'; export interface ServerOptions { - logger: Logger; + logger: LoggerService; } export async function startStandaloneServer( diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index f19e99feab..f0b0c5d962 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,89 @@ # @backstage/plugin-auth-node +## 0.3.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.3.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.3.0-next.0 + +### Minor Changes + +- 8513cd7d00e3: Introduced a new system for building auth providers for `@backstage/plugin-auth-backend`, which both increases the amount of code re-use across providers, and also works better with the new backend system. + + Many existing types have been moved from `@backstage/plugin-auth-backend` in order to avoid a direct dependency on the plugin from modules. + + Auth provider integrations are now primarily implemented through a pattern of creating "authenticators", which are in turn specific to each kind of integrations. Initially there are two types: `createOAuthAuthenticator` and `createProxyAuthenticator`. These come paired with functions that let you create the corresponding route handlers, `createOAuthRouteHandlers` and `createProxyAuthRouteHandlers`, as well as provider factories, `createOAuthProviderFactory` and `createProxyAuthProviderFactory`. This new authenticator pattern allows the sign-in logic to be separated from the auth integration logic, allowing it to be completely re-used across all providers of the same kind. + + The new provider factories also implement a new declarative way to configure sign-in resolvers, rather than configuration through code. Sign-in resolvers can now be configured through the `resolvers` configuration key, where the first resolver that provides an identity will be used, for example: + + ```yaml + auth: + providers: + google: + development: + clientId: ... + clientSecret: ... + signIn: + resolvers: + - resolver: emailMatchingUserEntityAnnotation + - resolver: emailLocalPartMatchingUserEntityName + ``` + + These configurable resolvers are created with a new `createSignInResolverFactory` function, which creates a sign-in resolver factory, optionally with an options schema that will be used both when configuring the sign-in resolver through configuration and code. + + The internal helpers from `@backstage/plugin-auth-backend` that were used to implement auth providers using passport strategies have now also been made available as public API, through `PassportHelpers` and `PassportOAuthAuthenticatorHelper`. + +### Patch Changes + +- 18619f793c94: The `BackstageIdentityResponse` interface now has an optional `expiresInSeconds` field that can be used to signal session expiration. The `prepareBackstageIdentityResponse` utility will now also read the expiration from the provided token, and include it in the response. +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.2.17 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## 0.2.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + ## 0.2.17-next.1 ### Patch Changes diff --git a/plugins/auth-node/api-report.md b/plugins/auth-node/api-report.md index bc9e392929..a782847d7a 100644 --- a/plugins/auth-node/api-report.md +++ b/plugins/auth-node/api-report.md @@ -3,11 +3,104 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackstageIdentityResponse as BackstageIdentityResponse_2 } from '@backstage/plugin-auth-node'; +import { BackstageSignInResult as BackstageSignInResult_2 } from '@backstage/plugin-auth-node'; +import { Config } from '@backstage/config'; +import { Entity } from '@backstage/catalog-model'; +import { EntityFilterQuery } from '@backstage/catalog-client'; +import express from 'express'; +import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import { JsonObject } from '@backstage/types'; +import { JsonValue } from '@backstage/types'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { Profile } from 'passport'; import { Request as Request_2 } from 'express'; +import { Response as Response_2 } from 'express'; +import { Strategy } from 'passport'; +import { ZodSchema } from 'zod'; +import { ZodTypeDef } from 'zod'; + +// @public @deprecated (undocumented) +export type AuthProviderConfig = { + baseUrl: string; + appUrl: string; + isOriginAllowed: (origin: string) => boolean; + cookieConfigurer?: CookieConfigurer; +}; + +// @public (undocumented) +export type AuthProviderFactory = (options: { + providerId: string; + globalConfig: AuthProviderConfig; + config: Config; + logger: LoggerService; + resolverContext: AuthResolverContext; + baseUrl: string; + appUrl: string; + isOriginAllowed: (origin: string) => boolean; + cookieConfigurer?: CookieConfigurer; +}) => AuthProviderRouteHandlers; + +// @public (undocumented) +export interface AuthProviderRegistrationOptions { + // (undocumented) + factory: AuthProviderFactory; + // (undocumented) + providerId: string; +} + +// @public +export interface AuthProviderRouteHandlers { + frameHandler(req: Request_2, res: Response_2): Promise; + logout?(req: Request_2, res: Response_2): Promise; + refresh?(req: Request_2, res: Response_2): Promise; + start(req: Request_2, res: Response_2): Promise; +} + +// @public (undocumented) +export interface AuthProvidersExtensionPoint { + // (undocumented) + registerProvider(options: AuthProviderRegistrationOptions): void; +} + +// @public (undocumented) +export const authProvidersExtensionPoint: ExtensionPoint; + +// @public +export type AuthResolverCatalogUserQuery = + | { + entityRef: + | string + | { + kind?: string; + namespace?: string; + name: string; + }; + } + | { + annotations: Record; + } + | { + filter: EntityFilterQuery; + }; + +// @public +export type AuthResolverContext = { + issueToken(params: TokenParams): Promise<{ + token: string; + }>; + findCatalogUser(query: AuthResolverCatalogUserQuery): Promise<{ + entity: Entity; + }>; + signInWithCatalogUser( + query: AuthResolverCatalogUserQuery, + ): Promise; +}; // @public export interface BackstageIdentityResponse extends BackstageSignInResult { + expiresInSeconds?: number; identity: BackstageUserIdentity; } @@ -23,6 +116,99 @@ export type BackstageUserIdentity = { ownershipEntityRefs: string[]; }; +// @public (undocumented) +export type ClientAuthResponse = { + providerInfo: TProviderInfo; + profile: ProfileInfo; + backstageIdentity?: BackstageIdentityResponse; +}; + +// @public +export namespace commonSignInResolvers { + const emailMatchingUserEntityProfileEmail: SignInResolverFactory< + unknown, + unknown + >; + const emailLocalPartMatchingUserEntityName: SignInResolverFactory< + unknown, + unknown + >; +} + +// @public +export type CookieConfigurer = (ctx: { + providerId: string; + baseUrl: string; + callbackUrl: string; + appOrigin: string; +}) => { + domain: string; + path: string; + secure: boolean; + sameSite?: 'none' | 'lax' | 'strict'; +}; + +// @public (undocumented) +export function createOAuthAuthenticator( + authenticator: OAuthAuthenticator, +): OAuthAuthenticator; + +// @public (undocumented) +export function createOAuthProviderFactory(options: { + authenticator: OAuthAuthenticator; + stateTransform?: OAuthStateTransform; + profileTransform?: ProfileTransform>; + signInResolver?: SignInResolver>; + signInResolverFactories?: { + [name in string]: SignInResolverFactory< + OAuthAuthenticatorResult, + unknown + >; + }; +}): AuthProviderFactory; + +// @public (undocumented) +export function createOAuthRouteHandlers( + options: OAuthRouteHandlersOptions, +): AuthProviderRouteHandlers; + +// @public (undocumented) +export function createProxyAuthenticator( + authenticator: ProxyAuthenticator, +): ProxyAuthenticator; + +// @public (undocumented) +export function createProxyAuthProviderFactory(options: { + authenticator: ProxyAuthenticator; + profileTransform?: ProfileTransform; + signInResolver?: SignInResolver; + signInResolverFactories?: Record< + string, + SignInResolverFactory + >; +}): AuthProviderFactory; + +// @public (undocumented) +export function createProxyAuthRouteHandlers( + options: ProxyAuthRouteHandlersOptions, +): AuthProviderRouteHandlers; + +// @public (undocumented) +export function createSignInResolverFactory< + TAuthResult, + TOptionsOutput, + TOptionsInput, +>( + options: SignInResolverFactoryOptions< + TAuthResult, + TOptionsOutput, + TOptionsInput + >, +): SignInResolverFactory; + +// @public (undocumented) +export function decodeOAuthState(encodedState: string): OAuthState; + // @public export class DefaultIdentityClient implements IdentityApi { // @deprecated @@ -34,6 +220,9 @@ export class DefaultIdentityClient implements IdentityApi { ): Promise; } +// @public (undocumented) +export function encodeOAuthState(state: OAuthState): string; + // @public export function getBearerTokenFromAuthorizationHeader( authorizationHeader: unknown, @@ -65,4 +254,393 @@ export type IdentityClientOptions = { issuer?: string; algorithms?: string[]; }; + +// @public (undocumented) +export interface OAuthAuthenticator { + // (undocumented) + authenticate( + input: OAuthAuthenticatorAuthenticateInput, + ctx: TContext, + ): Promise>; + // (undocumented) + defaultProfileTransform: ProfileTransform>; + // (undocumented) + initialize(ctx: { callbackUrl: string; config: Config }): TContext; + // (undocumented) + logout?(input: OAuthAuthenticatorLogoutInput, ctx: TContext): Promise; + // (undocumented) + refresh( + input: OAuthAuthenticatorRefreshInput, + ctx: TContext, + ): Promise>; + // (undocumented) + shouldPersistScopes?: boolean; + // (undocumented) + start( + input: OAuthAuthenticatorStartInput, + ctx: TContext, + ): Promise<{ + url: string; + status?: number; + }>; +} + +// @public (undocumented) +export interface OAuthAuthenticatorAuthenticateInput { + // (undocumented) + req: Request_2; +} + +// @public (undocumented) +export interface OAuthAuthenticatorLogoutInput { + // (undocumented) + accessToken?: string; + // (undocumented) + refreshToken?: string; + // (undocumented) + req: Request_2; +} + +// @public (undocumented) +export interface OAuthAuthenticatorRefreshInput { + // (undocumented) + refreshToken: string; + // (undocumented) + req: Request_2; + // (undocumented) + scope: string; +} + +// @public (undocumented) +export interface OAuthAuthenticatorResult { + // (undocumented) + fullProfile: TProfile; + // (undocumented) + session: OAuthSession; +} + +// @public (undocumented) +export interface OAuthAuthenticatorStartInput { + // (undocumented) + req: Request_2; + // (undocumented) + scope: string; + // (undocumented) + state: string; +} + +// @public (undocumented) +export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers { + constructor(handlers: Map); + // (undocumented) + frameHandler(req: express.Request, res: express.Response): Promise; + // (undocumented) + logout(req: express.Request, res: express.Response): Promise; + // (undocumented) + static mapConfig( + config: Config, + factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers, + ): OAuthEnvironmentHandler; + // (undocumented) + refresh(req: express.Request, res: express.Response): Promise; + // (undocumented) + start(req: express.Request, res: express.Response): Promise; +} + +// @public (undocumented) +export interface OAuthRouteHandlersOptions { + // (undocumented) + appUrl: string; + // (undocumented) + authenticator: OAuthAuthenticator; + // (undocumented) + baseUrl: string; + // (undocumented) + config: Config; + // (undocumented) + cookieConfigurer?: CookieConfigurer; + // (undocumented) + isOriginAllowed: (origin: string) => boolean; + // (undocumented) + profileTransform?: ProfileTransform>; + // (undocumented) + providerId: string; + // (undocumented) + resolverContext: AuthResolverContext; + // (undocumented) + signInResolver?: SignInResolver>; + // (undocumented) + stateTransform?: OAuthStateTransform; +} + +// @public (undocumented) +export interface OAuthSession { + // (undocumented) + accessToken: string; + // (undocumented) + expiresInSeconds?: number; + // (undocumented) + idToken?: string; + // (undocumented) + refreshToken?: string; + // (undocumented) + refreshTokenExpiresInSeconds?: number; + // (undocumented) + scope: string; + // (undocumented) + tokenType: string; +} + +// @public +export type OAuthState = { + nonce: string; + env: string; + origin?: string; + scope?: string; + redirectUrl?: string; + flow?: string; +}; + +// @public (undocumented) +export type OAuthStateTransform = ( + state: OAuthState, + context: { + req: Request_2; + }, +) => Promise<{ + state: OAuthState; +}>; + +// @public (undocumented) +export type PassportDoneCallback = ( + err?: Error, + result?: TResult, + privateInfo?: TPrivateInfo, +) => void; + +// @public (undocumented) +export class PassportHelpers { + // (undocumented) + static executeFetchUserProfileStrategy( + providerStrategy: Strategy, + accessToken: string, + ): Promise; + // (undocumented) + static executeFrameHandlerStrategy( + req: Request_2, + providerStrategy: Strategy, + options?: Record, + ): Promise<{ + result: TResult; + privateInfo: TPrivateInfo; + }>; + // (undocumented) + static executeRedirectStrategy( + req: Request_2, + providerStrategy: Strategy, + options: Record, + ): Promise<{ + url: string; + status?: number; + }>; + // (undocumented) + static executeRefreshTokenStrategy( + providerStrategy: Strategy, + refreshToken: string, + scope: string, + ): Promise<{ + accessToken: string; + refreshToken?: string; + params: any; + }>; + // (undocumented) + static transformProfile: ( + profile: PassportProfile, + idToken?: string, + ) => ProfileInfo; +} + +// @public (undocumented) +export class PassportOAuthAuthenticatorHelper { + // (undocumented) + authenticate( + input: OAuthAuthenticatorAuthenticateInput, + ): Promise>; + // (undocumented) + static defaultProfileTransform: ProfileTransform< + OAuthAuthenticatorResult + >; + // (undocumented) + fetchProfile(accessToken: string): Promise; + // (undocumented) + static from(strategy: Strategy): PassportOAuthAuthenticatorHelper; + // (undocumented) + refresh( + input: OAuthAuthenticatorRefreshInput, + ): Promise>; + // (undocumented) + start( + input: OAuthAuthenticatorStartInput, + options: Record, + ): Promise<{ + url: string; + status?: number; + }>; +} + +// @public (undocumented) +export type PassportOAuthDoneCallback = PassportDoneCallback< + PassportOAuthResult, + PassportOAuthPrivateInfo +>; + +// @public (undocumented) +export type PassportOAuthPrivateInfo = { + refreshToken?: string; +}; + +// @public (undocumented) +export type PassportOAuthResult = { + fullProfile: PassportProfile; + params: { + id_token?: string; + scope: string; + token_type?: string; + expires_in: number; + }; + accessToken: string; +}; + +// @public (undocumented) +export type PassportProfile = Profile & { + avatarUrl?: string; +}; + +// @public +export function prepareBackstageIdentityResponse( + result: BackstageSignInResult_2, +): BackstageIdentityResponse_2; + +// @public +export type ProfileInfo = { + email?: string; + displayName?: string; + picture?: string; +}; + +// @public +export type ProfileTransform = ( + result: TResult, + context: AuthResolverContext, +) => Promise<{ + profile: ProfileInfo; +}>; + +// @public (undocumented) +export interface ProxyAuthenticator { + // (undocumented) + authenticate( + options: { + req: Request_2; + }, + ctx: TContext, + ): Promise<{ + result: TResult; + }>; + // (undocumented) + defaultProfileTransform: ProfileTransform; + // (undocumented) + initialize(ctx: { config: Config }): Promise; +} + +// @public (undocumented) +export interface ProxyAuthRouteHandlersOptions { + // (undocumented) + authenticator: ProxyAuthenticator; + // (undocumented) + config: Config; + // (undocumented) + profileTransform?: ProfileTransform; + // (undocumented) + resolverContext: AuthResolverContext; + // (undocumented) + signInResolver: SignInResolver; +} + +// @public (undocumented) +export function readDeclarativeSignInResolver( + options: ReadDeclarativeSignInResolverOptions, +): SignInResolver | undefined; + +// @public (undocumented) +export interface ReadDeclarativeSignInResolverOptions { + // (undocumented) + config: Config; + // (undocumented) + signInResolverFactories: { + [name in string]: SignInResolverFactory; + }; +} + +// @public (undocumented) +export function sendWebMessageResponse( + res: Response_2, + appOrigin: string, + response: WebMessageResponse, +): void; + +// @public +export type SignInInfo = { + profile: ProfileInfo; + result: TAuthResult; +}; + +// @public +export type SignInResolver = ( + info: SignInInfo, + context: AuthResolverContext, +) => Promise; + +// @public (undocumented) +export interface SignInResolverFactory { + // (undocumented) + ( + ...options: undefined extends TOptions + ? [options?: TOptions] + : [options: TOptions] + ): SignInResolver; + // (undocumented) + optionsJsonSchema?: JsonObject; +} + +// @public (undocumented) +export interface SignInResolverFactoryOptions< + TAuthResult, + TOptionsOutput, + TOptionsInput, +> { + // (undocumented) + create(options: TOptionsOutput): SignInResolver; + // (undocumented) + optionsSchema?: ZodSchema; +} + +// @public +export type TokenParams = { + claims: { + sub: string; + ent?: string[]; + } & Record; +}; + +// @public +export type WebMessageResponse = + | { + type: 'authorization_response'; + response: ClientAuthResponse; + } + | { + type: 'authorization_response'; + error: Error; + }; ``` diff --git a/plugins/auth-node/catalog-info.yaml b/plugins/auth-node/catalog-info.yaml new file mode 100644 index 0000000000..4b23642810 --- /dev/null +++ b/plugins/auth-node/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth-node + title: '@backstage/plugin-auth-node' +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index ab98ce1e7f..728b4a8ea8 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.2.17-next.1", + "version": "0.3.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,19 +29,31 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/catalog-client": "workspace:^", + "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/types": "workspace:^", "@types/express": "*", + "@types/passport": "^1.0.3", "express": "^4.17.1", "jose": "^4.6.0", + "lodash": "^4.17.21", "node-fetch": "^2.6.7", - "winston": "^3.2.1" + "passport": "^0.6.0", + "winston": "^3.2.1", + "zod": "^3.21.4", + "zod-to-json-schema": "^3.21.4" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "cookie-parser": "^1.4.6", + "express-promise-router": "^4.1.1", "lodash": "^4.17.21", "msw": "^1.0.0", + "supertest": "^6.1.3", "uuid": "^8.0.0" }, "files": [ diff --git a/plugins/auth-node/src/extensions/AuthProvidersExtensionPoint.ts b/plugins/auth-node/src/extensions/AuthProvidersExtensionPoint.ts new file mode 100644 index 0000000000..b67d5be017 --- /dev/null +++ b/plugins/auth-node/src/extensions/AuthProvidersExtensionPoint.ts @@ -0,0 +1,35 @@ +/* + * 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 { createExtensionPoint } from '@backstage/backend-plugin-api'; +import { AuthProviderFactory } from '../types'; + +/** @public */ +export interface AuthProviderRegistrationOptions { + providerId: string; + factory: AuthProviderFactory; +} + +/** @public */ +export interface AuthProvidersExtensionPoint { + registerProvider(options: AuthProviderRegistrationOptions): void; +} + +/** @public */ +export const authProvidersExtensionPoint = + createExtensionPoint({ + id: 'auth.providers', + }); diff --git a/plugins/auth-node/src/extensions/index.ts b/plugins/auth-node/src/extensions/index.ts new file mode 100644 index 0000000000..43e1751685 --- /dev/null +++ b/plugins/auth-node/src/extensions/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { + authProvidersExtensionPoint, + type AuthProviderRegistrationOptions, + type AuthProvidersExtensionPoint, +} from './AuthProvidersExtensionPoint'; diff --git a/plugins/auth-node/src/flow/__testUtils__/parseWebMessageResponse.ts b/plugins/auth-node/src/flow/__testUtils__/parseWebMessageResponse.ts new file mode 100644 index 0000000000..eda8469ac6 --- /dev/null +++ b/plugins/auth-node/src/flow/__testUtils__/parseWebMessageResponse.ts @@ -0,0 +1,28 @@ +/* + * 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 { WebMessageResponse } from '../sendWebMessageResponse'; + +export function parseWebMessageResponse(text: string): { + response: WebMessageResponse; + origin: string; +} { + const [response, origin] = text.matchAll(/decodeURIComponent\('(.+?)'\)/g); + return { + response: JSON.parse(decodeURIComponent(response[1])), + origin: decodeURIComponent(origin[1]), + }; +} diff --git a/plugins/auth-node/src/flow/index.ts b/plugins/auth-node/src/flow/index.ts new file mode 100644 index 0000000000..51d91e5d58 --- /dev/null +++ b/plugins/auth-node/src/flow/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { + sendWebMessageResponse, + type WebMessageResponse, +} from './sendWebMessageResponse'; diff --git a/plugins/auth-node/src/flow/sendWebMessageResponse.test.ts b/plugins/auth-node/src/flow/sendWebMessageResponse.test.ts new file mode 100644 index 0000000000..cc192638f2 --- /dev/null +++ b/plugins/auth-node/src/flow/sendWebMessageResponse.test.ts @@ -0,0 +1,194 @@ +/* + * 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 express from 'express'; +import { + safelyEncodeURIComponent, + sendWebMessageResponse, + type WebMessageResponse, +} from './sendWebMessageResponse'; +import { parseWebMessageResponse } from './__testUtils__/parseWebMessageResponse'; + +describe('oauth helpers', () => { + describe('safelyEncodeURIComponent', () => { + it('encodes all occurrences of single quotes', () => { + expect(safelyEncodeURIComponent("a'ö'b")).toBe('a%27%C3%B6%27b'); + }); + }); + + describe('sendWebMessageResponse', () => { + const appOrigin = 'http://localhost:3000'; + it('should post a message back with payload success', () => { + const mockResponse = { + end: jest.fn().mockReturnThis(), + setHeader: jest.fn().mockReturnThis(), + } as unknown as express.Response; + + const data: WebMessageResponse = { + type: 'authorization_response', + response: { + providerInfo: { + accessToken: 'ACCESS_TOKEN', + idToken: 'ID_TOKEN', + expiresInSeconds: 10, + scope: 'email', + }, + profile: { + email: 'foo@bar.com', + }, + backstageIdentity: { + token: 'a.b.c', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'a', + }, + }, + }, + }; + const encoded = safelyEncodeURIComponent(JSON.stringify(data)); + + sendWebMessageResponse(mockResponse, appOrigin, data); + expect(mockResponse.setHeader).toHaveBeenCalledTimes(3); + expect(mockResponse.end).toHaveBeenCalledTimes(1); + expect(mockResponse.end).toHaveBeenCalledWith( + expect.stringContaining(encoded), + ); + }); + + it('should post a message back with payload error', () => { + const mockResponse = { + end: jest.fn().mockReturnThis(), + setHeader: jest.fn().mockReturnThis(), + } as unknown as express.Response; + + const data: WebMessageResponse = { + type: 'authorization_response', + error: new Error('Unknown error occurred'), + }; + const encoded = safelyEncodeURIComponent( + JSON.stringify({ + type: 'authorization_response', + error: { name: 'Error', message: 'Unknown error occurred' }, + }), + ); + + sendWebMessageResponse(mockResponse, appOrigin, data); + expect(mockResponse.setHeader).toHaveBeenCalledTimes(3); + expect(mockResponse.end).toHaveBeenCalledTimes(1); + expect(mockResponse.end).toHaveBeenCalledWith( + expect.stringContaining(encoded), + ); + }); + + it('should call postMessage twice but only one of them with target *', () => { + let responseBody = ''; + + const mockResponse = { + end: jest.fn(body => { + responseBody = body; + return this; + }), + setHeader: jest.fn().mockReturnThis(), + } as unknown as express.Response; + + const data: WebMessageResponse = { + type: 'authorization_response', + response: { + providerInfo: { + accessToken: 'ACCESS_TOKEN', + idToken: 'ID_TOKEN', + expiresInSeconds: 10, + scope: 'email', + }, + profile: { + email: 'foo@bar.com', + }, + backstageIdentity: { + token: 'a.b.c', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'a', + }, + }, + }, + }; + sendWebMessageResponse(mockResponse, appOrigin, data); + expect(parseWebMessageResponse(responseBody).response).toEqual({ + type: 'authorization_response', + response: { + providerInfo: expect.any(Object), + profile: { + email: 'foo@bar.com', + }, + backstageIdentity: expect.any(Object), + }, + }); + + const errData: WebMessageResponse = { + type: 'authorization_response', + error: new Error('Unknown error occurred'), + }; + sendWebMessageResponse(mockResponse, appOrigin, errData); + expect(parseWebMessageResponse(responseBody).response).toEqual({ + type: 'authorization_response', + error: { + name: 'Error', + message: 'Unknown error occurred', + }, + }); + }); + + it('handles single quotes and unicode chars safely', () => { + const mockResponse = { + end: jest.fn().mockReturnThis(), + setHeader: jest.fn().mockReturnThis(), + } as unknown as express.Response; + + const data: WebMessageResponse = { + type: 'authorization_response', + response: { + providerInfo: { + accessToken: 'ACCESS_TOKEN', + idToken: 'ID_TOKEN', + expiresInSeconds: 10, + scope: 'email', + }, + profile: { + email: 'foo@bar.com', + displayName: "Adam l'Hôpital", + }, + backstageIdentity: { + token: 'a.b.c', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'a', + }, + }, + }, + }; + + sendWebMessageResponse(mockResponse, appOrigin, data); + expect(mockResponse.setHeader).toHaveBeenCalledTimes(3); + expect(mockResponse.end).toHaveBeenCalledTimes(1); + expect(mockResponse.end).toHaveBeenCalledWith( + expect.stringContaining('Adam%20l%27H%C3%B4pital'), + ); + }); + }); +}); diff --git a/plugins/auth-node/src/flow/sendWebMessageResponse.ts b/plugins/auth-node/src/flow/sendWebMessageResponse.ts new file mode 100644 index 0000000000..4c7a85854d --- /dev/null +++ b/plugins/auth-node/src/flow/sendWebMessageResponse.ts @@ -0,0 +1,93 @@ +/* + * 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 { Response } from 'express'; +import crypto from 'crypto'; +import { ClientAuthResponse } from '../types'; +import { serializeError } from '@backstage/errors'; + +/** + * Payload sent as a post message after the auth request is complete. + * If successful then has a valid payload with Auth information else contains an error. + * + * @public + */ +export type WebMessageResponse = + | { + type: 'authorization_response'; + response: ClientAuthResponse; + } + | { + type: 'authorization_response'; + error: Error; + }; + +/** @internal */ +export function safelyEncodeURIComponent(value: string): string { + // Note the g at the end of the regex; all occurrences of single quotes must + // be replaced, which encodeURIComponent does not do itself by default + return encodeURIComponent(value).replace(/'/g, '%27'); +} + +/** @public */ +export function sendWebMessageResponse( + res: Response, + appOrigin: string, + response: WebMessageResponse, +): void { + const jsonData = JSON.stringify(response, (_, value) => { + if (value instanceof Error) { + return serializeError(value); + } + return value; + }); + const base64Data = safelyEncodeURIComponent(jsonData); + const base64Origin = safelyEncodeURIComponent(appOrigin); + + // NOTE: It is absolutely imperative that we use the safe encoder above, to + // be sure that the js code below does not allow the injection of malicious + // data. + + // TODO: Make target app origin configurable globally + + // + // postMessage fails silently if the targetOrigin is disallowed. + // So 2 postMessages are sent from the popup to the parent window. + // First, the origin being used to post the actual authorization response is + // shared with the parent window with a postMessage with targetOrigin '*'. + // Second, the actual authorization response is sent with the app origin + // as the targetOrigin. + // If the first message was received but the actual auth response was + // never received, the event listener can conclude that targetOrigin + // was disallowed, indicating potential misconfiguration. + // + const script = ` + var authResponse = decodeURIComponent('${base64Data}'); + var origin = decodeURIComponent('${base64Origin}'); + var originInfo = {'type': 'config_info', 'targetOrigin': origin}; + (window.opener || window.parent).postMessage(originInfo, '*'); + (window.opener || window.parent).postMessage(JSON.parse(authResponse), origin); + setTimeout(() => { + window.close(); + }, 100); // same as the interval of the core-app-api lib/loginPopup.ts (to address race conditions) + `; + const hash = crypto.createHash('sha256').update(script).digest('base64'); + + res.setHeader('Content-Type', 'text/html'); + res.setHeader('X-Frame-Options', 'sameorigin'); + res.setHeader('Content-Security-Policy', `script-src 'sha256-${hash}'`); + res.end(``); +} diff --git a/plugins/auth-node/src/DefaultIdentityClient.test.ts b/plugins/auth-node/src/identity/DefaultIdentityClient.test.ts similarity index 99% rename from plugins/auth-node/src/DefaultIdentityClient.test.ts rename to plugins/auth-node/src/identity/DefaultIdentityClient.test.ts index 13d9c5a17b..b9bf3cabd2 100644 --- a/plugins/auth-node/src/DefaultIdentityClient.test.ts +++ b/plugins/auth-node/src/identity/DefaultIdentityClient.test.ts @@ -26,7 +26,7 @@ import { setupServer } from 'msw/node'; import { v4 as uuid } from 'uuid'; import { DefaultIdentityClient } from './DefaultIdentityClient'; -import { IdentityApiGetIdentityRequest } from './types'; +import { IdentityApiGetIdentityRequest } from './IdentityApi'; interface AnyJWK extends Record { use: 'sig'; diff --git a/plugins/auth-node/src/DefaultIdentityClient.ts b/plugins/auth-node/src/identity/DefaultIdentityClient.ts similarity index 97% rename from plugins/auth-node/src/DefaultIdentityClient.ts rename to plugins/auth-node/src/identity/DefaultIdentityClient.ts index 8af8bf09e1..1095609dec 100644 --- a/plugins/auth-node/src/DefaultIdentityClient.ts +++ b/plugins/auth-node/src/identity/DefaultIdentityClient.ts @@ -25,12 +25,9 @@ import { jwtVerify, } from 'jose'; import { GetKeyFunction } from 'jose/dist/types/types'; -import { - BackstageIdentityResponse, - IdentityApiGetIdentityRequest, -} from './types'; import { getBearerTokenFromAuthorizationHeader } from './getBearerTokenFromAuthorizationHeader'; -import { IdentityApi } from './IdentityApi'; +import { IdentityApi, IdentityApiGetIdentityRequest } from './IdentityApi'; +import { BackstageIdentityResponse } from '../types'; const CLOCK_MARGIN_S = 10; diff --git a/plugins/auth-node/src/IdentityApi.ts b/plugins/auth-node/src/identity/IdentityApi.ts similarity index 80% rename from plugins/auth-node/src/IdentityApi.ts rename to plugins/auth-node/src/identity/IdentityApi.ts index 86171fa3ab..8322b9f827 100644 --- a/plugins/auth-node/src/IdentityApi.ts +++ b/plugins/auth-node/src/identity/IdentityApi.ts @@ -13,10 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - BackstageIdentityResponse, - IdentityApiGetIdentityRequest, -} from './types'; + +import { Request } from 'express'; +import { BackstageIdentityResponse } from '../types'; + +/** + * Options to request the identity from a Backstage backend request + * + * @public + */ +export type IdentityApiGetIdentityRequest = { + request: Request; +}; /** * An identity client api to authenticate Backstage diff --git a/plugins/auth-node/src/IdentityClient.test.ts b/plugins/auth-node/src/identity/IdentityClient.test.ts similarity index 100% rename from plugins/auth-node/src/IdentityClient.test.ts rename to plugins/auth-node/src/identity/IdentityClient.test.ts diff --git a/plugins/auth-node/src/IdentityClient.ts b/plugins/auth-node/src/identity/IdentityClient.ts similarity index 97% rename from plugins/auth-node/src/IdentityClient.ts rename to plugins/auth-node/src/identity/IdentityClient.ts index 8252a61f81..fd6580dbb2 100644 --- a/plugins/auth-node/src/IdentityClient.ts +++ b/plugins/auth-node/src/identity/IdentityClient.ts @@ -18,7 +18,7 @@ import { DefaultIdentityClient, IdentityClientOptions, } from './DefaultIdentityClient'; -import { BackstageIdentityResponse } from './types'; +import { BackstageIdentityResponse } from '../types'; /** * An identity client to interact with auth-backend and authenticate Backstage diff --git a/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.test.ts b/plugins/auth-node/src/identity/getBearerTokenFromAuthorizationHeader.test.ts similarity index 100% rename from plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.test.ts rename to plugins/auth-node/src/identity/getBearerTokenFromAuthorizationHeader.test.ts diff --git a/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.ts b/plugins/auth-node/src/identity/getBearerTokenFromAuthorizationHeader.ts similarity index 100% rename from plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.ts rename to plugins/auth-node/src/identity/getBearerTokenFromAuthorizationHeader.ts diff --git a/plugins/auth-node/src/identity/index.ts b/plugins/auth-node/src/identity/index.ts new file mode 100644 index 0000000000..24e2dcb691 --- /dev/null +++ b/plugins/auth-node/src/identity/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { prepareBackstageIdentityResponse } from './prepareBackstageIdentityResponse'; +export { getBearerTokenFromAuthorizationHeader } from './getBearerTokenFromAuthorizationHeader'; +export { + DefaultIdentityClient, + type IdentityClientOptions, +} from './DefaultIdentityClient'; +export { IdentityClient } from './IdentityClient'; +export type { IdentityApi, IdentityApiGetIdentityRequest } from './IdentityApi'; diff --git a/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.test.ts b/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.test.ts similarity index 53% rename from plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.test.ts rename to plugins/auth-node/src/identity/prepareBackstageIdentityResponse.test.ts index dac06f964e..d37224a11d 100644 --- a/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.test.ts +++ b/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.test.ts @@ -23,7 +23,37 @@ function mkToken(payload: unknown) { } describe('prepareBackstageIdentityResponse', () => { + afterEach(jest.resetAllMocks); + it('parses a complete token to determine the identity', () => { + jest.spyOn(Date, 'now').mockReturnValue(5000); + + const token = mkToken({ sub: 'k:ns/n', ent: ['k:ns/o'], exp: 1005 }); + expect( + prepareBackstageIdentityResponse({ + token, + }), + ).toEqual({ + token, + expiresInSeconds: 1000, + identity: { + type: 'user', + userEntityRef: 'k:ns/n', + ownershipEntityRefs: ['k:ns/o'], + }, + }); + }); + + it('should reject tokens without subject', () => { + const token = mkToken({}); + expect(() => + prepareBackstageIdentityResponse({ + token, + }), + ).toThrow('Identity response must return a token with subject claim'); + }); + + it('should treat expiration as optional', () => { const token = mkToken({ sub: 'k:ns/n', ent: ['k:ns/o'] }); expect( prepareBackstageIdentityResponse({ @@ -38,4 +68,15 @@ describe('prepareBackstageIdentityResponse', () => { }, }); }); + + it('should reject tokens with negative expiration', () => { + jest.spyOn(Date, 'now').mockReturnValue(5000); + + const token = mkToken({ sub: 'k:ns/n', ent: ['k:ns/o'], exp: 1 }); + expect(() => + prepareBackstageIdentityResponse({ + token, + }), + ).toThrow('Identity response must not return an expired token'); + }); }); diff --git a/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.ts b/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.ts new file mode 100644 index 0000000000..33e9bafd0d --- /dev/null +++ b/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.ts @@ -0,0 +1,66 @@ +/* + * 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 { InputError } from '@backstage/errors'; +import { + BackstageIdentityResponse, + BackstageSignInResult, +} from '@backstage/plugin-auth-node'; + +function parseJwtPayload(token: string) { + const [_header, payload, _signature] = token.split('.'); + return JSON.parse(Buffer.from(payload, 'base64').toString()); +} + +/** + * Parses a Backstage-issued token and decorates the + * {@link @backstage/plugin-auth-node#BackstageIdentityResponse} with identity information sourced from the + * token. + * + * @public + */ +export function prepareBackstageIdentityResponse( + result: BackstageSignInResult, +): BackstageIdentityResponse { + if (!result.token) { + throw new InputError(`Identity response must return a token`); + } + + const { sub, ent = [], exp: expStr } = parseJwtPayload(result.token); + if (!sub) { + throw new InputError( + `Identity response must return a token with subject claim`, + ); + } + + const expAt = Number(expStr); + + // Default to 1 hour if no expiration is set, in particular to make testing simpler + const exp = expAt ? Math.round(expAt - Date.now() / 1000) : undefined; + if (exp && exp < 0) { + throw new InputError(`Identity response must not return an expired token`); + } + + return { + ...result, + expiresInSeconds: exp, + identity: { + type: 'user', + userEntityRef: sub, + ownershipEntityRefs: ent, + }, + }; +} diff --git a/plugins/auth-node/src/index.ts b/plugins/auth-node/src/index.ts index a70f667adf..e7b0b628b8 100644 --- a/plugins/auth-node/src/index.ts +++ b/plugins/auth-node/src/index.ts @@ -20,14 +20,27 @@ * @packageDocumentation */ -export { getBearerTokenFromAuthorizationHeader } from './getBearerTokenFromAuthorizationHeader'; -export { DefaultIdentityClient } from './DefaultIdentityClient'; -export { IdentityClient } from './IdentityClient'; -export type { IdentityApi } from './IdentityApi'; -export type { IdentityClientOptions } from './DefaultIdentityClient'; +export * from './extensions'; +export * from './flow'; +export * from './identity'; +export * from './oauth'; +export * from './passport'; +export * from './proxy'; +export * from './sign-in'; export type { + AuthProviderConfig, + AuthProviderRouteHandlers, + AuthProviderFactory, + AuthResolverCatalogUserQuery, + AuthResolverContext, BackstageIdentityResponse, BackstageSignInResult, BackstageUserIdentity, - IdentityApiGetIdentityRequest, + ClientAuthResponse, + CookieConfigurer, + ProfileInfo, + ProfileTransform, + SignInInfo, + SignInResolver, + TokenParams, } from './types'; diff --git a/plugins/auth-node/src/oauth/OAuthCookieManager.ts b/plugins/auth-node/src/oauth/OAuthCookieManager.ts new file mode 100644 index 0000000000..5fe2304610 --- /dev/null +++ b/plugins/auth-node/src/oauth/OAuthCookieManager.ts @@ -0,0 +1,127 @@ +/* + * 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 { Request, Response } from 'express'; +import { CookieConfigurer } from '../types'; + +const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; +const TEN_MINUTES_MS = 600 * 1000; + +const defaultCookieConfigurer: CookieConfigurer = ({ + callbackUrl, + providerId, + appOrigin, +}) => { + const { hostname: domain, pathname, protocol } = new URL(callbackUrl); + const secure = protocol === 'https:'; + + // For situations where the auth-backend is running on a + // different domain than the app, we set the SameSite attribute + // to 'none' to allow third-party access to the cookie, but + // only if it's in a secure context (https). + let sameSite: ReturnType['sameSite'] = 'lax'; + if (new URL(appOrigin).hostname !== domain && secure) { + sameSite = 'none'; + } + + // If the provider supports callbackUrls, the pathname will + // contain the complete path to the frame handler so we need + // to slice off the trailing part of the path. + const path = pathname.endsWith(`${providerId}/handler/frame`) + ? pathname.slice(0, -'/handler/frame'.length) + : `${pathname}/${providerId}`; + + return { domain, path, secure, sameSite }; +}; + +/** @internal */ +export class OAuthCookieManager { + private readonly cookieConfigurer: CookieConfigurer; + private readonly nonceCookie: string; + private readonly refreshTokenCookie: string; + private readonly grantedScopeCookie: string; + + constructor( + private readonly options: { + providerId: string; + defaultAppOrigin: string; + baseUrl: string; + callbackUrl: string; + cookieConfigurer?: CookieConfigurer; + }, + ) { + this.cookieConfigurer = options.cookieConfigurer ?? defaultCookieConfigurer; + + this.nonceCookie = `${options.providerId}-nonce`; + this.refreshTokenCookie = `${options.providerId}-refresh-token`; + this.grantedScopeCookie = `${options.providerId}-granted-scope`; + } + + private getConfig(origin?: string, pathSuffix: string = '') { + const cookieConfig = this.cookieConfigurer({ + providerId: this.options.providerId, + baseUrl: this.options.baseUrl, + callbackUrl: this.options.callbackUrl, + appOrigin: origin ?? this.options.defaultAppOrigin, + }); + return { + httpOnly: true, + sameSite: 'lax' as const, + ...cookieConfig, + path: cookieConfig.path + pathSuffix, + }; + } + + setNonce(res: Response, nonce: string, origin?: string) { + res.cookie(this.nonceCookie, nonce, { + maxAge: TEN_MINUTES_MS, + ...this.getConfig(origin, '/handler'), + }); + } + + setRefreshToken(res: Response, refreshToken: string, origin?: string) { + res.cookie(this.refreshTokenCookie, refreshToken, { + maxAge: THOUSAND_DAYS_MS, + ...this.getConfig(origin), + }); + } + + removeRefreshToken(res: Response, origin?: string) { + res.cookie(this.refreshTokenCookie, '', { + maxAge: 0, + ...this.getConfig(origin), + }); + } + + setGrantedScopes(res: Response, scope: string, origin?: string) { + res.cookie(this.grantedScopeCookie, scope, { + maxAge: THOUSAND_DAYS_MS, + ...this.getConfig(origin), + }); + } + + getNonce(req: Request) { + return req.cookies[this.nonceCookie]; + } + + getRefreshToken(req: Request) { + return req.cookies[this.refreshTokenCookie]; + } + + getGrantedScopes(req: Request) { + return req.cookies[this.grantedScopeCookie]; + } +} diff --git a/plugins/auth-node/src/oauth/OAuthEnvironmentHandler.ts b/plugins/auth-node/src/oauth/OAuthEnvironmentHandler.ts new file mode 100644 index 0000000000..3a5ee00f8b --- /dev/null +++ b/plugins/auth-node/src/oauth/OAuthEnvironmentHandler.ts @@ -0,0 +1,97 @@ +/* + * 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 express from 'express'; +import { Config } from '@backstage/config'; +import { InputError, NotFoundError } from '@backstage/errors'; +import { AuthProviderRouteHandlers } from '../types'; +import { decodeOAuthState } from './state'; + +/** @public */ +export class OAuthEnvironmentHandler implements AuthProviderRouteHandlers { + static mapConfig( + config: Config, + factoryFunc: (envConfig: Config) => AuthProviderRouteHandlers, + ) { + const envs = config.keys(); + const handlers = new Map(); + + for (const env of envs) { + const envConfig = config.getConfig(env); + const handler = factoryFunc(envConfig); + handlers.set(env, handler); + } + + return new OAuthEnvironmentHandler(handlers); + } + + constructor( + private readonly handlers: Map, + ) {} + + async start(req: express.Request, res: express.Response): Promise { + const provider = this.getProviderForEnv(req); + await provider.start(req, res); + } + + async frameHandler( + req: express.Request, + res: express.Response, + ): Promise { + const provider = this.getProviderForEnv(req); + await provider.frameHandler(req, res); + } + + async refresh(req: express.Request, res: express.Response): Promise { + const provider = this.getProviderForEnv(req); + await provider.refresh?.(req, res); + } + + async logout(req: express.Request, res: express.Response): Promise { + const provider = this.getProviderForEnv(req); + await provider.logout?.(req, res); + } + + private getEnvFromRequest(req: express.Request): string | undefined { + const reqEnv = req.query.env?.toString(); + if (reqEnv) { + return reqEnv; + } + const stateParams = req.query.state?.toString(); + if (!stateParams) { + return undefined; + } + const { env } = decodeOAuthState(stateParams); + return env; + } + + private getProviderForEnv(req: express.Request): AuthProviderRouteHandlers { + const env: string | undefined = this.getEnvFromRequest(req); + + if (!env) { + throw new InputError(`Must specify 'env' query to select environment`); + } + + const handler = this.handlers.get(env); + if (!handler) { + throw new NotFoundError( + `No configuration available for the '${env}' environment of this provider.`, + ); + } + + return handler; + } +} diff --git a/plugins/auth-node/src/oauth/PassportOAuthAuthenticatorHelper.ts b/plugins/auth-node/src/oauth/PassportOAuthAuthenticatorHelper.ts new file mode 100644 index 0000000000..420a661da5 --- /dev/null +++ b/plugins/auth-node/src/oauth/PassportOAuthAuthenticatorHelper.ts @@ -0,0 +1,137 @@ +/* + * 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 { Strategy } from 'passport'; +import { + PassportDoneCallback, + PassportHelpers, + PassportProfile, +} from '../passport'; +import { ProfileTransform } from '../types'; +import { + OAuthAuthenticatorAuthenticateInput, + OAuthAuthenticatorRefreshInput, + OAuthAuthenticatorResult, + OAuthAuthenticatorStartInput, +} from './types'; + +/** @public */ +export type PassportOAuthResult = { + fullProfile: PassportProfile; + params: { + id_token?: string; + scope: string; + token_type?: string; + expires_in: number; + }; + accessToken: string; +}; + +/** @public */ +export type PassportOAuthPrivateInfo = { + refreshToken?: string; +}; + +/** @public */ +export type PassportOAuthDoneCallback = PassportDoneCallback< + PassportOAuthResult, + PassportOAuthPrivateInfo +>; + +/** @public */ +export class PassportOAuthAuthenticatorHelper { + static defaultProfileTransform: ProfileTransform< + OAuthAuthenticatorResult + > = async input => ({ + profile: PassportHelpers.transformProfile( + input.fullProfile, + input.session.idToken, + ), + }); + + static from(strategy: Strategy) { + return new PassportOAuthAuthenticatorHelper(strategy); + } + + readonly #strategy: Strategy; + + private constructor(strategy: Strategy) { + this.#strategy = strategy; + } + + async start( + input: OAuthAuthenticatorStartInput, + options: Record, + ): Promise<{ url: string; status?: number }> { + return PassportHelpers.executeRedirectStrategy(input.req, this.#strategy, { + scope: input.scope, + state: input.state, + ...options, + }); + } + + async authenticate( + input: OAuthAuthenticatorAuthenticateInput, + ): Promise> { + const { result, privateInfo } = + await PassportHelpers.executeFrameHandlerStrategy< + PassportOAuthResult, + PassportOAuthPrivateInfo + >(input.req, this.#strategy); + + return { + fullProfile: result.fullProfile as PassportProfile, + session: { + accessToken: result.accessToken, + tokenType: result.params.token_type ?? 'bearer', + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + idToken: result.params.id_token, + refreshToken: privateInfo.refreshToken, + }, + }; + } + + async refresh( + input: OAuthAuthenticatorRefreshInput, + ): Promise> { + const result = await PassportHelpers.executeRefreshTokenStrategy( + this.#strategy, + input.refreshToken, + input.scope, + ); + const fullProfile = await this.fetchProfile(result.accessToken); + return { + fullProfile, + session: { + accessToken: result.accessToken, + tokenType: result.params.token_type ?? 'bearer', + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + idToken: result.params.id_token, + refreshToken: result.refreshToken, + }, + }; + } + + async fetchProfile(accessToken: string): Promise { + const profile = await PassportHelpers.executeFetchUserProfileStrategy( + this.#strategy, + accessToken, + ); + return profile; + } +} diff --git a/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts b/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts new file mode 100644 index 0000000000..87fde6a1a2 --- /dev/null +++ b/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts @@ -0,0 +1,66 @@ +/* + * 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 { readDeclarativeSignInResolver } from '../sign-in'; +import { + AuthProviderFactory, + ProfileTransform, + SignInResolver, +} from '../types'; +import { OAuthEnvironmentHandler } from './OAuthEnvironmentHandler'; +import { createOAuthRouteHandlers } from './createOAuthRouteHandlers'; +import { OAuthStateTransform } from './state'; +import { OAuthAuthenticator, OAuthAuthenticatorResult } from './types'; +import { SignInResolverFactory } from '../sign-in/createSignInResolverFactory'; + +/** @public */ +export function createOAuthProviderFactory(options: { + authenticator: OAuthAuthenticator; + stateTransform?: OAuthStateTransform; + profileTransform?: ProfileTransform>; + signInResolver?: SignInResolver>; + signInResolverFactories?: { + [name in string]: SignInResolverFactory< + OAuthAuthenticatorResult, + unknown + >; + }; +}): AuthProviderFactory { + return ctx => { + return OAuthEnvironmentHandler.mapConfig(ctx.config, envConfig => { + const signInResolver = + options.signInResolver ?? + readDeclarativeSignInResolver({ + config: envConfig, + signInResolverFactories: options.signInResolverFactories ?? {}, + }); + + return createOAuthRouteHandlers({ + authenticator: options.authenticator, + appUrl: ctx.appUrl, + baseUrl: ctx.baseUrl, + config: envConfig, + isOriginAllowed: ctx.isOriginAllowed, + cookieConfigurer: ctx.cookieConfigurer, + providerId: ctx.providerId, + resolverContext: ctx.resolverContext, + stateTransform: options.stateTransform, + profileTransform: options.profileTransform, + signInResolver, + }); + }); + }; +} diff --git a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts new file mode 100644 index 0000000000..2573d95ea1 --- /dev/null +++ b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts @@ -0,0 +1,742 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import express from 'express'; +import request, { SuperAgentTest } from 'supertest'; +import cookieParser from 'cookie-parser'; +import PromiseRouter from 'express-promise-router'; +import { AuthProviderRouteHandlers, AuthResolverContext } from '../types'; +import { createOAuthRouteHandlers } from './createOAuthRouteHandlers'; +import { OAuthAuthenticator } from './types'; +import { errorHandler } from '@backstage/backend-common'; +import { encodeOAuthState, OAuthState } from './state'; +import { PassportProfile } from '../passport'; +import { parseWebMessageResponse } from '../flow/__testUtils__/parseWebMessageResponse'; + +const mockAuthenticator: jest.Mocked> = { + initialize: jest.fn(_r => ({ ctx: 'authenticator' })), + start: jest.fn(), + authenticate: jest.fn(), + refresh: jest.fn(), + logout: jest.fn(), + defaultProfileTransform: jest.fn(async (_r, _c) => ({ profile: {} })), +}; + +const mockBackstageToken = `a.${btoa( + JSON.stringify({ sub: 'user:default/mock', ent: [] }), +)}.c`; + +const mockSession = { + accessToken: 'access-token', + expiresInSeconds: 3, + scope: 'my-scope', + tokenType: 'bear', + idToken: 'id-token', + refreshToken: 'refresh-token', +}; + +const baseConfig = { + authenticator: mockAuthenticator, + appUrl: 'http://127.0.0.1', + baseUrl: 'http://127.0.0.1:7007', + isOriginAllowed: () => true, + providerId: 'my-provider', + config: new ConfigReader({}), + resolverContext: { ctx: 'resolver' } as unknown as AuthResolverContext, +}; + +function wrapInApp(handlers: AuthProviderRouteHandlers) { + const app = express(); + + const router = PromiseRouter(); + + router.use(cookieParser()); + app.use('/my-provider', router); + app.use(errorHandler()); + + router.get('/start', handlers.start.bind(handlers)); + router.get('/handler/frame', handlers.frameHandler.bind(handlers)); + router.post('/handler/frame', handlers.frameHandler.bind(handlers)); + if (handlers.logout) { + router.post('/logout', handlers.logout.bind(handlers)); + } + if (handlers.refresh) { + router.get('/refresh', handlers.refresh.bind(handlers)); + router.post('/refresh', handlers.refresh.bind(handlers)); + } + + return app; +} + +function getNonceCookie(test: SuperAgentTest) { + return test.jar.getCookie('my-provider-nonce', { + domain: '127.0.0.1', + path: '/my-provider/handler', + script: false, + secure: false, + }); +} + +function getRefreshTokenCookie(test: SuperAgentTest) { + return test.jar.getCookie('my-provider-refresh-token', { + domain: '127.0.0.1', + path: '/my-provider', + script: false, + secure: false, + }); +} + +function getGrantedScopesCookie(test: SuperAgentTest) { + return test.jar.getCookie('my-provider-granted-scope', { + domain: '127.0.0.1', + path: '/my-provider', + script: false, + secure: false, + }); +} + +describe('createOAuthRouteHandlers', () => { + afterEach(() => jest.clearAllMocks()); + + it('should be created', () => { + const handlers = createOAuthRouteHandlers(baseConfig); + expect(handlers).toEqual({ + start: expect.any(Function), + frameHandler: expect.any(Function), + refresh: expect.any(Function), + logout: expect.any(Function), + }); + }); + + describe('start', () => { + it('should require an env query', async () => { + const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); + const res = await request(app).get('/my-provider/start'); + + expect(res.status).toBe(400); + expect(res.body).toMatchObject({ + error: { + name: 'InputError', + message: 'No env provided in request query parameters', + }, + }); + }); + + it('should start', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + mockAuthenticator.start.mockResolvedValue({ + url: 'https://example.com/redirect', + }); + + const res = await agent.get( + '/my-provider/start?env=development&scope=my-scope', + ); + + const { value: nonce } = getNonceCookie(agent); + + expect(res.text).toBe(''); + expect(res.status).toBe(302); + expect(res.get('Location')).toBe('https://example.com/redirect'); + expect(res.get('Content-Length')).toBe('0'); + + expect(mockAuthenticator.start).toHaveBeenCalledWith( + { + req: expect.anything(), + scope: 'my-scope', + state: encodeOAuthState({ + nonce: decodeURIComponent(nonce), + env: 'development', + }), + }, + { ctx: 'authenticator' }, + ); + }); + + it('should start with additional parameters, transform state, and persist scopes', async () => { + const agent = request.agent( + wrapInApp( + createOAuthRouteHandlers({ + ...baseConfig, + authenticator: { + ...mockAuthenticator, + shouldPersistScopes: true, + }, + stateTransform: async state => ({ + state: { ...state, nonce: '123' }, + }), + }), + ), + ); + + mockAuthenticator.start.mockResolvedValue({ + url: 'https://example.com/redirect', + }); + + const res = await agent.get('/my-provider/start').query({ + env: 'development', + scope: 'my-scope', + origin: 'https://remotehost', + redirectUrl: 'https://remotehost/redirect', + flow: 'redirect', + }); + + expect(res.text).toBe(''); + expect(res.status).toBe(302); + expect(res.get('Location')).toBe('https://example.com/redirect'); + expect(res.get('Content-Length')).toBe('0'); + + expect(mockAuthenticator.start).toHaveBeenCalledWith( + { + req: expect.anything(), + scope: 'my-scope', + state: encodeOAuthState({ + nonce: '123', + env: 'development', + origin: 'https://remotehost', + redirectUrl: 'https://remotehost/redirect', + flow: 'redirect', + scope: 'my-scope', + }), + }, + { ctx: 'authenticator' }, + ); + }); + }); + + describe('frameHandler', () => { + it('should authenticate', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + agent.jar.setCookie( + 'my-provider-nonce=123', + '127.0.0.1', + '/my-provider/handler', + ); + + mockAuthenticator.authenticate.mockResolvedValue({ + fullProfile: { id: 'id' } as PassportProfile, + session: mockSession, + }); + + const res = await agent.get('/my-provider/handler/frame').query({ + state: encodeOAuthState({ + env: 'development', + nonce: '123', + } as OAuthState), + }); + + expect(mockAuthenticator.authenticate).toHaveBeenCalledWith( + { req: expect.anything() }, + { ctx: 'authenticator' }, + ); + + expect(res.status).toBe(200); + expect(parseWebMessageResponse(res.text).response).toEqual({ + type: 'authorization_response', + response: { + profile: {}, + providerInfo: { + accessToken: 'access-token', + expiresInSeconds: 3, + idToken: 'id-token', + scope: 'my-scope', + }, + }, + }); + + expect(getRefreshTokenCookie(agent).value).toBe('refresh-token'); + expect(getGrantedScopesCookie(agent)).toBeUndefined(); + }); + + it('should authenticate with sign-in, profile transform, and persisted scopes', async () => { + const agent = request.agent( + wrapInApp( + createOAuthRouteHandlers({ + ...baseConfig, + authenticator: { + ...mockAuthenticator, + shouldPersistScopes: true, + }, + profileTransform: async () => ({ profile: { email: 'em@i.l' } }), + signInResolver: async () => ({ token: mockBackstageToken }), + }), + ), + ); + + agent.jar.setCookie( + 'my-provider-nonce=123', + '127.0.0.1', + '/my-provider/handler', + ); + + mockAuthenticator.authenticate.mockResolvedValue({ + fullProfile: { id: 'id' } as PassportProfile, + session: mockSession, + }); + + const res = await agent.get('/my-provider/handler/frame').query({ + state: encodeOAuthState({ + env: 'development', + nonce: '123', + scope: 'my-scope', + } as OAuthState), + }); + + expect(res.status).toBe(200); + expect(parseWebMessageResponse(res.text).response).toEqual({ + type: 'authorization_response', + response: { + profile: { email: 'em@i.l' }, + providerInfo: { + accessToken: 'access-token', + expiresInSeconds: 3, + idToken: 'id-token', + scope: 'my-scope', + }, + backstageIdentity: { + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'user:default/mock', + }, + token: mockBackstageToken, + }, + }, + }); + + expect(getRefreshTokenCookie(agent).value).toBe('refresh-token'); + expect(getGrantedScopesCookie(agent).value).toBe('my-scope'); + }); + + it('should redirect with persisted scope', async () => { + const agent = request.agent( + wrapInApp( + createOAuthRouteHandlers({ + ...baseConfig, + authenticator: { + ...mockAuthenticator, + shouldPersistScopes: true, + }, + profileTransform: async () => ({ profile: { email: 'em@i.l' } }), + signInResolver: async () => ({ token: mockBackstageToken }), + }), + ), + ); + + agent.jar.setCookie( + 'my-provider-nonce=123', + '127.0.0.1', + '/my-provider/handler', + ); + + mockAuthenticator.authenticate.mockResolvedValue({ + fullProfile: { id: 'id' } as PassportProfile, + session: mockSession, + }); + + const res = await agent.get('/my-provider/handler/frame').query({ + state: encodeOAuthState({ + env: 'development', + nonce: '123', + scope: 'my-scope', + flow: 'redirect', + redirectUrl: 'https://127.0.0.1:3000/redirect', + } as OAuthState), + }); + + expect(res.status).toBe(302); + expect(res.get('Location')).toBe('https://127.0.0.1:3000/redirect'); + + expect(getRefreshTokenCookie(agent).value).toBe('refresh-token'); + expect(getGrantedScopesCookie(agent).value).toBe('my-scope'); + }); + + it('should require a valid origin', async () => { + const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); + const res = await request(app) + .get('/my-provider/handler/frame') + .query({ + state: encodeOAuthState({ + env: 'development', + nonce: '123', + origin: 'invalid-origin', + }), + }); + + expect(res.status).toBe(200); + expect(parseWebMessageResponse(res.text).response).toEqual({ + type: 'authorization_response', + error: { + name: 'NotAllowedError', + message: 'App origin is invalid, failed to parse', + }, + }); + }); + + it('should reject origins that are not allowed', async () => { + const app = wrapInApp( + createOAuthRouteHandlers({ + ...baseConfig, + isOriginAllowed: () => false, + }), + ); + const res = await request(app) + .get('/my-provider/handler/frame') + .query({ + state: encodeOAuthState({ + env: 'development', + nonce: '123', + origin: 'http://localhost:3000', + }), + }); + + expect(res.status).toBe(200); + expect(parseWebMessageResponse(res.text).response).toEqual({ + type: 'authorization_response', + error: { + name: 'NotAllowedError', + message: "Origin 'http://localhost:3000' is not allowed", + }, + }); + }); + + it('should reject missing state env', async () => { + const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); + const res = await request(app) + .get('/my-provider/handler/frame') + .query({ + state: encodeOAuthState({ + nonce: '123', + } as OAuthState), + }); + + expect(res.status).toBe(200); + expect(parseWebMessageResponse(res.text).response).toEqual({ + type: 'authorization_response', + error: { + name: 'NotAllowedError', + message: 'OAuth state is invalid, missing env', + }, + }); + }); + + it('should reject missing cookie nonce', async () => { + const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); + const res = await request(app) + .get('/my-provider/handler/frame') + .query({ + state: encodeOAuthState({ + env: 'development', + nonce: '123', + }), + }); + + expect(res.status).toBe(200); + expect(parseWebMessageResponse(res.text).response).toEqual({ + type: 'authorization_response', + error: { + name: 'NotAllowedError', + message: 'Auth response is missing cookie nonce', + }, + }); + }); + + it('should reject missing state nonce', async () => { + const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); + const res = await request(app) + .get('/my-provider/handler/frame') + .query({ + state: encodeOAuthState({ + env: 'development', + } as OAuthState), + }); + + expect(res.status).toBe(200); + expect(parseWebMessageResponse(res.text).response).toEqual({ + type: 'authorization_response', + error: { + name: 'NotAllowedError', + message: 'OAuth state is invalid, missing nonce', + }, + }); + }); + + it('should reject mismatched nonce', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + agent.jar.setCookie( + 'my-provider-nonce=456', + '127.0.0.1', + '/my-provider/handler', + ); + + const res = await agent.get('/my-provider/handler/frame').query({ + state: encodeOAuthState({ + env: 'development', + nonce: '123', + } as OAuthState), + }); + + expect(res.status).toBe(200); + expect(parseWebMessageResponse(res.text).response).toEqual({ + type: 'authorization_response', + error: { + name: 'NotAllowedError', + message: 'Invalid nonce', + }, + }); + }); + }); + + describe('refresh', () => { + it('should refresh', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + agent.jar.setCookie( + 'my-provider-refresh-token=refresh-token', + '127.0.0.1', + '/my-provider', + ); + + mockAuthenticator.refresh.mockImplementation(async ({ scope }) => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { ...mockSession, scope }, + })); + + const res = await agent + .post('/my-provider/refresh') + .set('X-Requested-With', 'XMLHttpRequest') + .query({ scope: 'my-scope' }); + + expect(mockAuthenticator.refresh).toHaveBeenCalledWith( + { + req: expect.anything(), + refreshToken: 'refresh-token', + scope: 'my-scope', + }, + { ctx: 'authenticator' }, + ); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + profile: {}, + providerInfo: { + accessToken: 'access-token', + expiresInSeconds: 3, + idToken: 'id-token', + scope: 'my-scope', + }, + }); + }); + + it('should refresh with sign-in, profile transform, and persisted scopes', async () => { + const agent = request.agent( + wrapInApp( + createOAuthRouteHandlers({ + ...baseConfig, + authenticator: { + ...mockAuthenticator, + shouldPersistScopes: true, + }, + profileTransform: async () => ({ profile: { email: 'em@i.l' } }), + signInResolver: async () => ({ token: mockBackstageToken }), + }), + ), + ); + + agent.jar.setCookie( + 'my-provider-refresh-token=refresh-token', + '127.0.0.1', + '/my-provider', + ); + agent.jar.setCookie( + 'my-provider-granted-scope=persisted-scope', + '127.0.0.1', + '/my-provider', + ); + + mockAuthenticator.refresh.mockImplementation(async ({ scope }) => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { ...mockSession, scope, refreshToken: 'new-refresh-token' }, + })); + + const res = await agent + .post('/my-provider/refresh') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(mockAuthenticator.refresh).toHaveBeenCalledWith( + { + req: expect.anything(), + refreshToken: 'refresh-token', + scope: 'persisted-scope', + }, + { ctx: 'authenticator' }, + ); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + profile: { email: 'em@i.l' }, + providerInfo: { + accessToken: 'access-token', + expiresInSeconds: 3, + idToken: 'id-token', + scope: 'persisted-scope', + }, + backstageIdentity: { + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'user:default/mock', + }, + token: mockBackstageToken, + }, + }); + expect(getRefreshTokenCookie(agent).value).toBe('new-refresh-token'); + }); + + it('should forward errors', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + agent.jar.setCookie( + 'my-provider-refresh-token=refresh-token', + '127.0.0.1', + '/my-provider', + ); + + mockAuthenticator.refresh.mockRejectedValue(new Error('NOPE')); + + const res = await agent + .post('/my-provider/refresh') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ + error: { + name: 'AuthenticationError', + message: 'Refresh failed; caused by Error: NOPE', + }, + }); + }); + + it('should require refresh cookie', async () => { + const res = await request(wrapInApp(createOAuthRouteHandlers(baseConfig))) + .post('/my-provider/refresh') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ + error: { + name: 'AuthenticationError', + message: + 'Refresh failed; caused by InputError: Missing session cookie', + }, + }); + }); + + it('should reject requests without CSRF header', async () => { + const res = await request( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ).post('/my-provider/refresh'); + + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ + error: { + name: 'AuthenticationError', + message: 'Invalid X-Requested-With header', + }, + }); + }); + + it('should reject requests with invalid CSRF header', async () => { + const res = await request(wrapInApp(createOAuthRouteHandlers(baseConfig))) + .post('/my-provider/refresh') + .set('X-Requested-With', 'invalid'); + + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ + error: { + name: 'AuthenticationError', + message: 'Invalid X-Requested-With header', + }, + }); + }); + }); + + describe('logout', () => { + it('should log out', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + agent.jar.setCookie( + 'my-provider-refresh-token=my-refresh-token', + '127.0.0.1', + '/my-provider', + ); + + expect(getRefreshTokenCookie(agent).value).toBe('my-refresh-token'); + + const res = await agent + .post('/my-provider/logout') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(res.status).toBe(200); + expect(res.body).toEqual({}); + + expect(getRefreshTokenCookie(agent)).toBeUndefined(); + }); + + it('should reject requests without CSRF header', async () => { + const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); + + const res = await request(app).post('/my-provider/logout'); + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ + error: { + name: 'AuthenticationError', + message: 'Invalid X-Requested-With header', + }, + }); + }); + + it('should reject requests with invalid CSRF header', async () => { + const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); + + const res = await request(app) + .post('/my-provider/logout') + .set('X-Requested-With', 'wrong-value'); + expect(res.status).toBe(401); + expect(res.body).toMatchObject({ + error: { + name: 'AuthenticationError', + message: 'Invalid X-Requested-With header', + }, + }); + }); + }); +}); diff --git a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts new file mode 100644 index 0000000000..011b3fc0f0 --- /dev/null +++ b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts @@ -0,0 +1,343 @@ +/* + * 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 express from 'express'; +import crypto from 'crypto'; +import { URL } from 'url'; +import { + AuthenticationError, + InputError, + isError, + NotAllowedError, +} from '@backstage/errors'; +import { + encodeOAuthState, + decodeOAuthState, + OAuthStateTransform, + OAuthState, +} from './state'; +import { sendWebMessageResponse } from '../flow'; +import { prepareBackstageIdentityResponse } from '../identity'; +import { OAuthCookieManager } from './OAuthCookieManager'; +import { + AuthProviderRouteHandlers, + AuthResolverContext, + ClientAuthResponse, + CookieConfigurer, + ProfileTransform, + SignInResolver, +} from '../types'; +import { OAuthAuthenticator, OAuthAuthenticatorResult } from './types'; +import { Config } from '@backstage/config'; + +/** @public */ +export interface OAuthRouteHandlersOptions { + authenticator: OAuthAuthenticator; + appUrl: string; + baseUrl: string; + isOriginAllowed: (origin: string) => boolean; + providerId: string; + config: Config; + resolverContext: AuthResolverContext; + stateTransform?: OAuthStateTransform; + profileTransform?: ProfileTransform>; + cookieConfigurer?: CookieConfigurer; + signInResolver?: SignInResolver>; +} + +/** @internal */ +type ClientOAuthResponse = ClientAuthResponse<{ + /** + * An access token issued for the signed in user. + */ + accessToken: string; + /** + * (Optional) Id token issued for the signed in user. + */ + idToken?: string; + /** + * Expiry of the access token in seconds. + */ + expiresInSeconds?: number; + /** + * Scopes granted for the access token. + */ + scope: string; +}>; + +/** @public */ +export function createOAuthRouteHandlers( + options: OAuthRouteHandlersOptions, +): AuthProviderRouteHandlers { + const { + authenticator, + config, + baseUrl, + appUrl, + providerId, + isOriginAllowed, + cookieConfigurer, + resolverContext, + signInResolver, + } = options; + + const defaultAppOrigin = new URL(appUrl).origin; + const callbackUrl = + config.getOptionalString('callbackUrl') ?? + `${baseUrl}/${providerId}/handler/frame`; + + const stateTransform = options.stateTransform ?? (state => ({ state })); + const profileTransform = + options.profileTransform ?? authenticator.defaultProfileTransform; + const authenticatorCtx = authenticator.initialize({ config, callbackUrl }); + const cookieManager = new OAuthCookieManager({ + baseUrl, + callbackUrl, + defaultAppOrigin, + providerId, + cookieConfigurer, + }); + + return { + async start( + this: never, + req: express.Request, + res: express.Response, + ): Promise { + // retrieve scopes from request + const scope = req.query.scope?.toString() ?? ''; + const env = req.query.env?.toString(); + const origin = req.query.origin?.toString(); + const redirectUrl = req.query.redirectUrl?.toString(); + const flow = req.query.flow?.toString(); + + if (!env) { + throw new InputError('No env provided in request query parameters'); + } + + const nonce = crypto.randomBytes(16).toString('base64'); + // set a nonce cookie before redirecting to oauth provider + cookieManager.setNonce(res, nonce, origin); + + const state: OAuthState = { nonce, env, origin, redirectUrl, flow }; + + // If scopes are persisted then we pass them through the state so that we + // can set the cookie on successful auth + if (authenticator.shouldPersistScopes) { + state.scope = scope; + } + + const { state: transformedState } = await stateTransform(state, { req }); + const encodedState = encodeOAuthState(transformedState); + + const { url, status } = await options.authenticator.start( + { req, scope, state: encodedState }, + authenticatorCtx, + ); + + res.statusCode = status || 302; + res.setHeader('Location', url); + res.setHeader('Content-Length', '0'); + res.end(); + }, + + async frameHandler( + this: never, + req: express.Request, + res: express.Response, + ): Promise { + let appOrigin = defaultAppOrigin; + + try { + const state = decodeOAuthState(req.query.state?.toString() ?? ''); + + if (state.origin) { + try { + appOrigin = new URL(state.origin).origin; + } catch { + throw new NotAllowedError('App origin is invalid, failed to parse'); + } + if (!isOriginAllowed(appOrigin)) { + throw new NotAllowedError(`Origin '${appOrigin}' is not allowed`); + } + } + + // The same nonce is passed through cookie and state, and they must match + const cookieNonce = cookieManager.getNonce(req); + const stateNonce = state.nonce; + if (!cookieNonce) { + throw new NotAllowedError('Auth response is missing cookie nonce'); + } + if (cookieNonce !== stateNonce) { + throw new NotAllowedError('Invalid nonce'); + } + + const result = await authenticator.authenticate( + { req }, + authenticatorCtx, + ); + const { profile } = await profileTransform(result, resolverContext); + + const response: ClientOAuthResponse = { + profile, + providerInfo: { + idToken: result.session.idToken, + accessToken: result.session.accessToken, + scope: result.session.scope, + expiresInSeconds: result.session.expiresInSeconds, + }, + }; + + if (signInResolver) { + const identity = await signInResolver( + { profile, result }, + resolverContext, + ); + response.backstageIdentity = + prepareBackstageIdentityResponse(identity); + } + + // Store the scope that we have been granted for this session. This is useful if + // the provider does not return granted scopes on refresh or if they are normalized. + if (authenticator.shouldPersistScopes && state.scope) { + cookieManager.setGrantedScopes(res, state.scope, appOrigin); + result.session.scope = state.scope; + } + + if (result.session.refreshToken) { + // set new refresh token + cookieManager.setRefreshToken( + res, + result.session.refreshToken, + appOrigin, + ); + } + + // When using the redirect flow we rely on refresh token we just + // acquired to get a new session once we're back in the app. + if (state.flow === 'redirect') { + if (!state.redirectUrl) { + throw new InputError( + 'No redirectUrl provided in request query parameters', + ); + } + res.redirect(state.redirectUrl); + return; + } + + // post message back to popup if successful + sendWebMessageResponse(res, appOrigin, { + type: 'authorization_response', + response, + }); + } catch (error) { + const { name, message } = isError(error) + ? error + : new Error('Encountered invalid error'); // Being a bit safe and not forwarding the bad value + // post error message back to popup if failure + sendWebMessageResponse(res, appOrigin, { + type: 'authorization_response', + error: { name, message }, + }); + } + }, + + async logout( + this: never, + req: express.Request, + res: express.Response, + ): Promise { + // We use this as a lightweight CSRF protection + if (req.header('X-Requested-With') !== 'XMLHttpRequest') { + throw new AuthenticationError('Invalid X-Requested-With header'); + } + + if (authenticator.logout) { + const refreshToken = cookieManager.getRefreshToken(req); + await authenticator.logout({ req, refreshToken }, authenticatorCtx); + } + + // remove refresh token cookie if it is set + cookieManager.removeRefreshToken(res, req.get('origin')); + + res.status(200).end(); + }, + + async refresh( + this: never, + req: express.Request, + res: express.Response, + ): Promise { + // We use this as a lightweight CSRF protection + if (req.header('X-Requested-With') !== 'XMLHttpRequest') { + throw new AuthenticationError('Invalid X-Requested-With header'); + } + + try { + const refreshToken = cookieManager.getRefreshToken(req); + + // throw error if refresh token is missing in the request + if (!refreshToken) { + throw new InputError('Missing session cookie'); + } + + let scope = req.query.scope?.toString() ?? ''; + if (authenticator.shouldPersistScopes) { + scope = cookieManager.getGrantedScopes(req); + } + + const result = await authenticator.refresh( + { req, scope, refreshToken }, + authenticatorCtx, + ); + + const { profile } = await profileTransform(result, resolverContext); + + const newRefreshToken = result.session.refreshToken; + if (newRefreshToken && newRefreshToken !== refreshToken) { + cookieManager.setRefreshToken( + res, + newRefreshToken, + req.get('origin'), + ); + } + + const response: ClientOAuthResponse = { + profile, + providerInfo: { + idToken: result.session.idToken, + accessToken: result.session.accessToken, + scope: result.session.scope, + expiresInSeconds: result.session.expiresInSeconds, + }, + }; + + if (signInResolver) { + const identity = await signInResolver( + { profile, result }, + resolverContext, + ); + response.backstageIdentity = + prepareBackstageIdentityResponse(identity); + } + + res.status(200).json(response); + } catch (error) { + throw new AuthenticationError('Refresh failed', error); + } + }, + }; +} diff --git a/plugins/auth-node/src/oauth/index.ts b/plugins/auth-node/src/oauth/index.ts new file mode 100644 index 0000000000..25213dd2f0 --- /dev/null +++ b/plugins/auth-node/src/oauth/index.ts @@ -0,0 +1,44 @@ +/* + * 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 { + createOAuthRouteHandlers, + type OAuthRouteHandlersOptions, +} from './createOAuthRouteHandlers'; +export { + PassportOAuthAuthenticatorHelper, + type PassportOAuthDoneCallback, + type PassportOAuthPrivateInfo, + type PassportOAuthResult, +} from './PassportOAuthAuthenticatorHelper'; +export { OAuthEnvironmentHandler } from './OAuthEnvironmentHandler'; +export { createOAuthProviderFactory } from './createOAuthProviderFactory'; +export { + encodeOAuthState, + decodeOAuthState, + type OAuthState, + type OAuthStateTransform, +} from './state'; +export { + createOAuthAuthenticator, + type OAuthAuthenticator, + type OAuthAuthenticatorAuthenticateInput, + type OAuthAuthenticatorLogoutInput, + type OAuthAuthenticatorRefreshInput, + type OAuthAuthenticatorResult, + type OAuthAuthenticatorStartInput, + type OAuthSession, +} from './types'; diff --git a/plugins/auth-node/src/oauth/state.ts b/plugins/auth-node/src/oauth/state.ts new file mode 100644 index 0000000000..fc747d08a5 --- /dev/null +++ b/plugins/auth-node/src/oauth/state.ts @@ -0,0 +1,62 @@ +/* + * 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 pickBy from 'lodash/pickBy'; +import { Request } from 'express'; +import { NotAllowedError } from '@backstage/errors'; + +/** + * A type for the serialized value in the `state` parameter of the OAuth authorization flow + * @public + */ +export type OAuthState = { + nonce: string; + env: string; + origin?: string; + scope?: string; + redirectUrl?: string; + flow?: string; +}; + +/** @public */ +export type OAuthStateTransform = ( + state: OAuthState, + context: { req: Request }, +) => Promise<{ state: OAuthState }>; + +/** @public */ +export function encodeOAuthState(state: OAuthState): string { + const stateString = new URLSearchParams( + pickBy(state, value => value !== undefined), + ).toString(); + + return Buffer.from(stateString, 'utf-8').toString('hex'); +} + +/** @public */ +export function decodeOAuthState(encodedState: string): OAuthState { + const state = Object.fromEntries( + new URLSearchParams(Buffer.from(encodedState, 'hex').toString('utf-8')), + ); + if (!state.env || state.env?.length === 0) { + throw new NotAllowedError('OAuth state is invalid, missing env'); + } + if (!state.nonce || state.nonce?.length === 0) { + throw new NotAllowedError('OAuth state is invalid, missing nonce'); + } + + return state as OAuthState; +} diff --git a/plugins/auth-node/src/oauth/types.ts b/plugins/auth-node/src/oauth/types.ts new file mode 100644 index 0000000000..4bd7bd3f3f --- /dev/null +++ b/plugins/auth-node/src/oauth/types.ts @@ -0,0 +1,89 @@ +/* + * 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 { Config } from '@backstage/config'; +import { Request } from 'express'; +import { ProfileTransform } from '../types'; + +/** @public */ +export interface OAuthSession { + accessToken: string; + tokenType: string; + idToken?: string; + scope: string; + expiresInSeconds?: number; + refreshToken?: string; + refreshTokenExpiresInSeconds?: number; +} + +/** @public */ +export interface OAuthAuthenticatorStartInput { + scope: string; + state: string; + req: Request; +} + +/** @public */ +export interface OAuthAuthenticatorAuthenticateInput { + req: Request; +} + +/** @public */ +export interface OAuthAuthenticatorRefreshInput { + scope: string; + refreshToken: string; + req: Request; +} + +/** @public */ +export interface OAuthAuthenticatorLogoutInput { + accessToken?: string; + refreshToken?: string; + req: Request; +} + +/** @public */ +export interface OAuthAuthenticatorResult { + fullProfile: TProfile; + session: OAuthSession; +} + +/** @public */ +export interface OAuthAuthenticator { + defaultProfileTransform: ProfileTransform>; + shouldPersistScopes?: boolean; + initialize(ctx: { callbackUrl: string; config: Config }): TContext; + start( + input: OAuthAuthenticatorStartInput, + ctx: TContext, + ): Promise<{ url: string; status?: number }>; + authenticate( + input: OAuthAuthenticatorAuthenticateInput, + ctx: TContext, + ): Promise>; + refresh( + input: OAuthAuthenticatorRefreshInput, + ctx: TContext, + ): Promise>; + logout?(input: OAuthAuthenticatorLogoutInput, ctx: TContext): Promise; +} + +/** @public */ +export function createOAuthAuthenticator( + authenticator: OAuthAuthenticator, +): OAuthAuthenticator { + return authenticator; +} diff --git a/plugins/auth-node/src/passport/PassportHelpers.ts b/plugins/auth-node/src/passport/PassportHelpers.ts new file mode 100644 index 0000000000..6c13523811 --- /dev/null +++ b/plugins/auth-node/src/passport/PassportHelpers.ts @@ -0,0 +1,249 @@ +/* + * 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 { Request } from 'express'; +import { Strategy } from 'passport'; +import { PassportProfile } from './types'; +import { ProfileInfo } from '../types'; + +// Re-declared here to avoid direct dependency on passport-oauth2 +/** @internal */ +interface InternalOAuthError extends Error { + oauthError?: { + data?: string; + }; +} + +/** @internal */ +function decodeJwtPayload(token: string): Record { + const payloadStr = token.split('.')[1]; + if (!payloadStr) { + throw new Error('Invalid JWT token'); + } + + let payload: unknown; + try { + payload = JSON.parse( + Buffer.from( + payloadStr.replace(/-/g, '+').replace(/_/g, '/'), + 'base64', + ).toString('utf8'), + ); + } catch (e) { + throw new Error('Invalid JWT token'); + } + if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { + throw new Error('Invalid JWT token'); + } + return payload as Record; +} + +/** @public */ +export class PassportHelpers { + private constructor() {} + + static transformProfile = ( + profile: PassportProfile, + idToken?: string, + ): ProfileInfo => { + let email: string | undefined = undefined; + if (profile.emails && profile.emails.length > 0) { + const [firstEmail] = profile.emails; + email = firstEmail.value; + } + + let picture: string | undefined = undefined; + if (profile.avatarUrl) { + picture = profile.avatarUrl; + } else if (profile.photos && profile.photos.length > 0) { + const [firstPhoto] = profile.photos; + picture = firstPhoto.value; + } + + let displayName: string | undefined = + profile.displayName ?? profile.username ?? profile.id; + + if ((!email || !picture || !displayName) && idToken) { + try { + const decoded: Record = decodeJwtPayload(idToken); + if (!email && decoded.email) { + email = decoded.email; + } + if (!picture && decoded.picture) { + picture = decoded.picture; + } + if (!displayName && decoded.name) { + displayName = decoded.name; + } + } catch (e) { + throw new Error(`Failed to parse id token and get profile info, ${e}`); + } + } + + return { + email, + picture, + displayName, + }; + }; + + static async executeRedirectStrategy( + req: Request, + providerStrategy: Strategy, + options: Record, + ): Promise<{ + /** + * URL to redirect to + */ + url: string; + /** + * Status code to use for the redirect + */ + status?: number; + }> { + return new Promise(resolve => { + const strategy = Object.create(providerStrategy); + strategy.redirect = (url: string, status?: number) => { + resolve({ url, status: status ?? undefined }); + }; + + strategy.authenticate(req, { ...options }); + }); + } + + static async executeFrameHandlerStrategy( + req: Request, + providerStrategy: Strategy, + options?: Record, + ): Promise<{ result: TResult; privateInfo: TPrivateInfo }> { + return new Promise((resolve, reject) => { + const strategy = Object.create(providerStrategy); + strategy.success = (result: any, privateInfo: any) => { + resolve({ result, privateInfo }); + }; + strategy.fail = ( + info: { type: 'success' | 'error'; message?: string }, + // _status: number, + ) => { + reject(new Error(`Authentication rejected, ${info.message ?? ''}`)); + }; + strategy.error = (error: InternalOAuthError) => { + let message = `Authentication failed, ${error.message}`; + + if (error.oauthError?.data) { + try { + const errorData = JSON.parse(error.oauthError.data); + + if (errorData.message) { + message += ` - ${errorData.message}`; + } + } catch (parseError) { + message += ` - ${error.oauthError}`; + } + } + + reject(new Error(message)); + }; + strategy.redirect = () => { + reject(new Error('Unexpected redirect')); + }; + strategy.authenticate(req, { ...(options ?? {}) }); + }); + } + + static async executeRefreshTokenStrategy( + providerStrategy: Strategy, + refreshToken: string, + scope: string, + ): Promise<{ + /** + * An access token issued for the signed in user. + */ + accessToken: string; + /** + * Optionally, the server can issue a new Refresh Token for the user + */ + refreshToken?: string; + params: any; + }> { + return new Promise((resolve, reject) => { + const anyStrategy = providerStrategy as any; + const OAuth2 = anyStrategy._oauth2.constructor; + const oauth2 = new OAuth2( + anyStrategy._oauth2._clientId, + anyStrategy._oauth2._clientSecret, + anyStrategy._oauth2._baseSite, + anyStrategy._oauth2._authorizeUrl, + anyStrategy._refreshURL || anyStrategy._oauth2._accessTokenUrl, + anyStrategy._oauth2._customHeaders, + ); + + oauth2.getOAuthAccessToken( + refreshToken, + { + scope, + grant_type: 'refresh_token', + }, + ( + err: Error | null, + accessToken: string, + newRefreshToken: string, + params: any, + ) => { + if (err) { + reject( + new Error(`Failed to refresh access token ${err.toString()}`), + ); + } + if (!accessToken) { + reject( + new Error( + `Failed to refresh access token, no access token received`, + ), + ); + } + + resolve({ + accessToken, + refreshToken: newRefreshToken, + params, + }); + }, + ); + }); + } + + static async executeFetchUserProfileStrategy( + providerStrategy: Strategy, + accessToken: string, + ): Promise { + return new Promise((resolve, reject) => { + const anyStrategy = providerStrategy as unknown as { + userProfile(accessToken: string, callback: Function): void; + }; + anyStrategy.userProfile( + accessToken, + (error: Error, rawProfile: PassportProfile) => { + if (error) { + reject(error); + } else { + resolve(rawProfile); + } + }, + ); + }); + } +} diff --git a/plugins/auth-node/src/passport/index.ts b/plugins/auth-node/src/passport/index.ts new file mode 100644 index 0000000000..f70c0af921 --- /dev/null +++ b/plugins/auth-node/src/passport/index.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 { PassportHelpers } from './PassportHelpers'; +export type { PassportDoneCallback, PassportProfile } from './types'; diff --git a/plugins/auth-node/src/passport/types.ts b/plugins/auth-node/src/passport/types.ts new file mode 100644 index 0000000000..4a3f41b6b2 --- /dev/null +++ b/plugins/auth-node/src/passport/types.ts @@ -0,0 +1,29 @@ +/* + * 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 { Profile } from 'passport'; + +/** @public */ +export type PassportProfile = Profile & { + avatarUrl?: string; +}; + +/** @public */ +export type PassportDoneCallback = ( + err?: Error, + result?: TResult, + privateInfo?: TPrivateInfo, +) => void; diff --git a/plugins/auth-node/src/proxy/createProxyAuthProviderFactory.ts b/plugins/auth-node/src/proxy/createProxyAuthProviderFactory.ts new file mode 100644 index 0000000000..8626c40b33 --- /dev/null +++ b/plugins/auth-node/src/proxy/createProxyAuthProviderFactory.ts @@ -0,0 +1,61 @@ +/* + * 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 { + readDeclarativeSignInResolver, + SignInResolverFactory, +} from '../sign-in'; +import { + AuthProviderFactory, + ProfileTransform, + SignInResolver, +} from '../types'; +import { createProxyAuthRouteHandlers } from './createProxyRouteHandlers'; +import { ProxyAuthenticator } from './types'; + +/** @public */ +export function createProxyAuthProviderFactory(options: { + authenticator: ProxyAuthenticator; + profileTransform?: ProfileTransform; + signInResolver?: SignInResolver; + signInResolverFactories?: Record< + string, + SignInResolverFactory + >; +}): AuthProviderFactory { + return ctx => { + const signInResolver = + options.signInResolver ?? + readDeclarativeSignInResolver({ + config: ctx.config, + signInResolverFactories: options.signInResolverFactories ?? {}, + }); + + if (!signInResolver) { + throw new Error( + `No sign-in resolver configured for proxy auth provider '${ctx.providerId}'`, + ); + } + + return createProxyAuthRouteHandlers({ + signInResolver, + config: ctx.config, + authenticator: options.authenticator, + resolverContext: ctx.resolverContext, + profileTransform: options.profileTransform, + }); + }; +} diff --git a/plugins/auth-node/src/proxy/createProxyRouteHandlers.ts b/plugins/auth-node/src/proxy/createProxyRouteHandlers.ts new file mode 100644 index 0000000000..521fe39288 --- /dev/null +++ b/plugins/auth-node/src/proxy/createProxyRouteHandlers.ts @@ -0,0 +1,80 @@ +/* + * 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 { Request, Response } from 'express'; +import { Config } from '@backstage/config'; +import { + AuthProviderRouteHandlers, + AuthResolverContext, + ClientAuthResponse, + ProfileTransform, + SignInResolver, +} from '../types'; +import { ProxyAuthenticator } from './types'; +import { prepareBackstageIdentityResponse } from '../identity'; +import { NotImplementedError } from '@backstage/errors'; + +/** @public */ +export interface ProxyAuthRouteHandlersOptions { + authenticator: ProxyAuthenticator; + config: Config; + resolverContext: AuthResolverContext; + signInResolver: SignInResolver; + profileTransform?: ProfileTransform; +} + +/** @public */ +export function createProxyAuthRouteHandlers( + options: ProxyAuthRouteHandlersOptions, +): AuthProviderRouteHandlers { + const { authenticator, config, resolverContext, signInResolver } = options; + + const profileTransform = + options.profileTransform ?? authenticator.defaultProfileTransform; + const authenticatorCtx = authenticator.initialize({ config }); + + return { + async start(): Promise { + throw new NotImplementedError('Not implemented'); + }, + + async frameHandler(): Promise { + throw new NotImplementedError('Not implemented'); + }, + + async refresh(this: never, req: Request, res: Response): Promise { + const { result } = await authenticator.authenticate( + { req }, + authenticatorCtx, + ); + + const { profile } = await profileTransform(result, resolverContext); + + const identity = await signInResolver( + { profile, result }, + resolverContext, + ); + + const response: ClientAuthResponse<{}> = { + profile, + providerInfo: {}, + backstageIdentity: prepareBackstageIdentityResponse(identity), + }; + + res.status(200).json(response); + }, + }; +} diff --git a/plugins/auth-node/src/proxy/index.ts b/plugins/auth-node/src/proxy/index.ts new file mode 100644 index 0000000000..907f8829e5 --- /dev/null +++ b/plugins/auth-node/src/proxy/index.ts @@ -0,0 +1,22 @@ +/* + * 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 { createProxyAuthenticator, type ProxyAuthenticator } from './types'; +export { createProxyAuthProviderFactory } from './createProxyAuthProviderFactory'; +export { + createProxyAuthRouteHandlers, + type ProxyAuthRouteHandlersOptions, +} from './createProxyRouteHandlers'; diff --git a/plugins/auth-node/src/proxy/types.ts b/plugins/auth-node/src/proxy/types.ts new file mode 100644 index 0000000000..969b022abb --- /dev/null +++ b/plugins/auth-node/src/proxy/types.ts @@ -0,0 +1,36 @@ +/* + * 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 { Config } from '@backstage/config'; +import { Request } from 'express'; +import { ProfileTransform } from '../types'; + +/** @public */ +export interface ProxyAuthenticator { + defaultProfileTransform: ProfileTransform; + initialize(ctx: { config: Config }): Promise; + authenticate( + options: { req: Request }, + ctx: TContext, + ): Promise<{ result: TResult }>; +} + +/** @public */ +export function createProxyAuthenticator( + authenticator: ProxyAuthenticator, +): ProxyAuthenticator { + return authenticator; +} diff --git a/plugins/auth-node/src/sign-in/commonSignInResolvers.ts b/plugins/auth-node/src/sign-in/commonSignInResolvers.ts new file mode 100644 index 0000000000..5b5da07c73 --- /dev/null +++ b/plugins/auth-node/src/sign-in/commonSignInResolvers.ts @@ -0,0 +1,73 @@ +/* + * 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 { createSignInResolverFactory } from './createSignInResolverFactory'; + +/** + * A collection of common sign-in resolvers that work with any auth provider. + * + * @public + */ +export namespace commonSignInResolvers { + /** + * A common sign-in resolver that looks up the user using their email address + * as email of the entity. + */ + export const emailMatchingUserEntityProfileEmail = + createSignInResolverFactory({ + create() { + return async (info, ctx) => { + 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, + }, + }); + }; + }, + }); + + /** + * A common sign-in resolver that looks up the user using the local part of + * their email address as the entity name. + */ + export const emailLocalPartMatchingUserEntityName = + createSignInResolverFactory({ + create() { + return async (info, ctx) => { + const { profile } = info; + + if (!profile.email) { + throw new Error( + 'Login failed, user profile does not contain an email', + ); + } + const [localPart] = profile.email.split('@'); + + return ctx.signInWithCatalogUser({ + entityRef: { name: localPart }, + }); + }; + }, + }); +} diff --git a/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts b/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts new file mode 100644 index 0000000000..0632b704c3 --- /dev/null +++ b/plugins/auth-node/src/sign-in/createSignInResolverFactory.ts @@ -0,0 +1,75 @@ +/* + * 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 { ZodSchema, ZodTypeDef } from 'zod'; +import { SignInResolver } from '../types'; +import zodToJsonSchema from 'zod-to-json-schema'; +import { JsonObject } from '@backstage/types'; +import { InputError } from '@backstage/errors'; + +/** @public */ +export interface SignInResolverFactory { + ( + ...options: undefined extends TOptions + ? [options?: TOptions] + : [options: TOptions] + ): SignInResolver; + optionsJsonSchema?: JsonObject; +} + +/** @public */ +export interface SignInResolverFactoryOptions< + TAuthResult, + TOptionsOutput, + TOptionsInput, +> { + optionsSchema?: ZodSchema; + create(options: TOptionsOutput): SignInResolver; +} + +/** @public */ +export function createSignInResolverFactory< + TAuthResult, + TOptionsOutput, + TOptionsInput, +>( + options: SignInResolverFactoryOptions< + TAuthResult, + TOptionsOutput, + TOptionsInput + >, +): SignInResolverFactory { + const { optionsSchema } = options; + if (!optionsSchema) { + return (resolverOptions?: TOptionsInput) => { + if (resolverOptions) { + throw new InputError('sign-in resolver does not accept options'); + } + return options.create(undefined as TOptionsOutput); + }; + } + const factory = ( + ...[resolverOptions]: undefined extends TOptionsInput + ? [options?: TOptionsInput] + : [options: TOptionsInput] + ) => { + const parsedOptions = optionsSchema.parse(resolverOptions); + return options.create(parsedOptions); + }; + + factory.optionsJsonSchema = zodToJsonSchema(optionsSchema) as JsonObject; + return factory; +} diff --git a/plugins/auth-node/src/sign-in/index.ts b/plugins/auth-node/src/sign-in/index.ts new file mode 100644 index 0000000000..736393a762 --- /dev/null +++ b/plugins/auth-node/src/sign-in/index.ts @@ -0,0 +1,26 @@ +/* + * 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 { + createSignInResolverFactory, + type SignInResolverFactory, + type SignInResolverFactoryOptions, +} from './createSignInResolverFactory'; +export { + readDeclarativeSignInResolver, + type ReadDeclarativeSignInResolverOptions, +} from './readDeclarativeSignInResolver'; +export { commonSignInResolvers } from './commonSignInResolvers'; diff --git a/plugins/auth-node/src/sign-in/readDeclarativeSignInResolver.ts b/plugins/auth-node/src/sign-in/readDeclarativeSignInResolver.ts new file mode 100644 index 0000000000..82b4918326 --- /dev/null +++ b/plugins/auth-node/src/sign-in/readDeclarativeSignInResolver.ts @@ -0,0 +1,71 @@ +/* + * 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 { Config } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; +import { SignInResolver } from '../types'; +import { SignInResolverFactory } from './createSignInResolverFactory'; + +/** @public */ +export interface ReadDeclarativeSignInResolverOptions { + config: Config; + signInResolverFactories: { + [name in string]: SignInResolverFactory; + }; +} + +/** @public */ +export function readDeclarativeSignInResolver( + options: ReadDeclarativeSignInResolverOptions, +): SignInResolver | undefined { + const resolvers = + options.config + .getOptionalConfigArray('signIn.resolvers') + ?.map(resolverConfig => { + const resolverName = resolverConfig.getString('resolver'); + if (!Object.hasOwn(options.signInResolverFactories, resolverName)) { + throw new Error( + `Sign-in resolver '${resolverName}' is not available`, + ); + } + const resolver = options.signInResolverFactories[resolverName]; + const { resolver: _ignored, ...resolverOptions } = + resolverConfig.get(); + + return resolver( + Object.keys(resolverOptions).length > 0 ? resolverOptions : undefined, + ); + }) ?? []; + + if (resolvers.length === 0) { + return undefined; + } + + return async (profile, context) => { + for (const resolver of resolvers) { + try { + return await resolver(profile, context); + } catch (error) { + if (error?.name === 'NotFoundError') { + continue; + } + throw error; + } + } + + throw new Error('Failed to sign-in, unable to resolve user identity'); + }; +} diff --git a/plugins/auth-node/src/types.ts b/plugins/auth-node/src/types.ts index 9aec98a372..d4cb0ac48e 100644 --- a/plugins/auth-node/src/types.ts +++ b/plugins/auth-node/src/types.ts @@ -14,7 +14,12 @@ * limitations under the License. */ -import { Request } from 'express'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { EntityFilterQuery } from '@backstage/catalog-client'; +import { Entity } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; +import { Request, Response } from 'express'; /** * A representation of a successful Backstage sign-in. @@ -31,15 +36,6 @@ export interface BackstageSignInResult { token: string; } -/** - * Options to request the identity from a Backstage backend request - * - * @public - */ -export type IdentityApiGetIdentityRequest = { - request: Request; -}; - /** * Response object containing the {@link BackstageUserIdentity} and the token * from the authentication provider. @@ -47,6 +43,11 @@ export type IdentityApiGetIdentityRequest = { * @public */ export interface BackstageIdentityResponse extends BackstageSignInResult { + /** + * The number of seconds until the token expires. If not set, it can be assumed that the token does not expire. + */ + expiresInSeconds?: number; + /** * A plaintext description of the identity that is encapsulated within the token. */ @@ -76,3 +77,301 @@ export type BackstageUserIdentity = { */ ownershipEntityRefs: string[]; }; + +/** + * A query for a single user in the catalog. + * + * If `entityRef` is used, the default kind is `'User'`. + * + * If `annotations` are used, all annotations must be present and + * match the provided value exactly. Only entities of kind `'User'` will be considered. + * + * If `filter` are used they are passed on as they are to the `CatalogApi`. + * + * Regardless of the query method, the query must match exactly one entity + * in the catalog, or an error will be thrown. + * + * @public + */ +export type AuthResolverCatalogUserQuery = + | { + entityRef: + | string + | { + kind?: string; + namespace?: string; + name: string; + }; + } + | { + annotations: Record; + } + | { + filter: EntityFilterQuery; + }; + +/** + * Parameters used to issue new Backstage Tokens + * + * @public + */ +export type TokenParams = { + /** + * The claims that will be embedded within the token. At a minimum, this should include + * the subject claim, `sub`. It is common to also list entity ownership relations in the + * `ent` list. Additional claims may also be added at the developer's discretion except + * for the following list, which will be overwritten by the TokenIssuer: `iss`, `aud`, + * `iat`, and `exp`. The Backstage team also maintains the right add new claims in the future + * without listing the change as a "breaking change". + */ + claims: { + /** The token subject, i.e. User ID */ + sub: string; + /** A list of entity references that the user claims ownership through */ + ent?: string[]; + } & Record; +}; + +/** + * The context that is used for auth processing. + * + * @public + */ +export type AuthResolverContext = { + /** + * Issues a Backstage token using the provided parameters. + */ + issueToken(params: TokenParams): Promise<{ token: string }>; + + /** + * Finds a single user in the catalog using the provided query. + * + * See {@link AuthResolverCatalogUserQuery} for details. + */ + findCatalogUser( + query: AuthResolverCatalogUserQuery, + ): Promise<{ entity: Entity }>; + + /** + * Finds a single user in the catalog using the provided query, and then + * issues an identity for that user using default ownership resolution. + * + * See {@link AuthResolverCatalogUserQuery} for details. + */ + signInWithCatalogUser( + query: AuthResolverCatalogUserQuery, + ): Promise; +}; + +/** + * Any Auth provider needs to implement this interface which handles the routes in the + * auth backend. Any auth API requests from the frontend reaches these methods. + * + * The routes in the auth backend API are tied to these methods like below + * + * `/auth/[provider]/start -> start` + * `/auth/[provider]/handler/frame -> frameHandler` + * `/auth/[provider]/refresh -> refresh` + * `/auth/[provider]/logout -> logout` + * + * @public + */ +export interface AuthProviderRouteHandlers { + /** + * Handles the start route of the API. This initiates a sign in request with an auth provider. + * + * Request + * - scopes for the auth request (Optional) + * Response + * - redirect to the auth provider for the user to sign in or consent. + * - sets a nonce cookie and also pass the nonce as 'state' query parameter in the redirect request + */ + start(req: Request, res: Response): Promise; + + /** + * Once the user signs in or consents in the OAuth screen, the auth provider redirects to the + * callbackURL which is handled by this method. + * + * Request + * - to contain a nonce cookie and a 'state' query parameter + * Response + * - postMessage to the window with a payload that contains accessToken, expiryInSeconds?, idToken? and scope. + * - sets a refresh token cookie if the auth provider supports refresh tokens + */ + frameHandler(req: Request, res: Response): Promise; + + /** + * (Optional) If the auth provider supports refresh tokens then this method handles + * requests to get a new access token. + * + * Other types of providers may also use this method to implement its own logic to create new sessions + * upon request. For example, this can be used to create a new session for a provider that handles requests + * from an authenticating proxy. + * + * Request + * - to contain a refresh token cookie and scope (Optional) query parameter. + * Response + * - payload with accessToken, expiryInSeconds?, idToken?, scope and user profile information. + */ + refresh?(req: Request, res: Response): Promise; + + /** + * (Optional) Handles sign out requests + * + * Response + * - removes the refresh token cookie + */ + logout?(req: Request, res: Response): Promise; +} + +/** + * @public + * @deprecated Use top-level properties passed to `AuthProviderFactory` instead + */ +export type AuthProviderConfig = { + /** + * The protocol://domain[:port] where the app is hosted. This is used to construct the + * callbackURL to redirect to once the user signs in to the auth provider. + */ + baseUrl: string; + + /** + * The base URL of the app as provided by app.baseUrl + */ + appUrl: string; + + /** + * A function that is called to check whether an origin is allowed to receive the authentication result. + */ + isOriginAllowed: (origin: string) => boolean; + + /** + * The function used to resolve cookie configuration based on the auth provider options. + */ + cookieConfigurer?: CookieConfigurer; +}; + +/** @public */ +export type AuthProviderFactory = (options: { + providerId: string; + /** @deprecated Use top-level properties instead */ + globalConfig: AuthProviderConfig; + config: Config; + logger: LoggerService; + resolverContext: AuthResolverContext; + /** + * The protocol://domain[:port] where the app is hosted. This is used to construct the + * callbackURL to redirect to once the user signs in to the auth provider. + */ + baseUrl: string; + + /** + * The base URL of the app as provided by app.baseUrl + */ + appUrl: string; + + /** + * A function that is called to check whether an origin is allowed to receive the authentication result. + */ + isOriginAllowed: (origin: string) => boolean; + + /** + * The function used to resolve cookie configuration based on the auth provider options. + */ + cookieConfigurer?: CookieConfigurer; +}) => AuthProviderRouteHandlers; + +/** @public */ +export type ClientAuthResponse = { + providerInfo: TProviderInfo; + profile: ProfileInfo; + backstageIdentity?: BackstageIdentityResponse; +}; + +/** + * Type of sign in information context. Includes the profile information and + * authentication result which contains auth related information. + * + * @public + */ +export type SignInInfo = { + /** + * The simple profile passed down for use in the frontend. + */ + profile: ProfileInfo; + + /** + * The authentication result that was received from the authentication + * provider. + */ + result: TAuthResult; +}; + +/** + * Describes the function which handles the result of a successful + * authentication. Must return a valid {@link @backstage/plugin-auth-node#BackstageSignInResult}. + * + * @public + */ +export type SignInResolver = ( + info: SignInInfo, + context: AuthResolverContext, +) => Promise; + +/** + * Describes the function that transforms the result of a successful + * authentication into a {@link ProfileInfo} object. + * + * This function may optionally throw an error in order to reject authentication. + * + * @public + */ +export type ProfileTransform = ( + result: TResult, + context: AuthResolverContext, +) => Promise<{ profile: ProfileInfo }>; + +/** + * Used to display login information to user, i.e. sidebar popup. + * + * It is also temporarily used as the profile of the signed-in user's Backstage + * identity, but we want to replace that with data from identity and/org catalog + * service + * + * @public + */ +export type ProfileInfo = { + /** + * Email ID of the signed in user. + */ + email?: string; + /** + * Display name that can be presented to the signed in user. + */ + displayName?: string; + /** + * URL to an image that can be used as the display image or avatar of the + * signed in user. + */ + picture?: string; +}; + +/** + * The callback used to resolve the cookie configuration for auth providers that use cookies. + * @public + */ +export type CookieConfigurer = (ctx: { + /** ID of the auth provider that this configuration applies to */ + providerId: string; + /** The externally reachable base URL of the auth-backend plugin */ + baseUrl: string; + /** The configured callback URL of the auth provider */ + callbackUrl: string; + /** The origin URL of the app */ + appOrigin: string; +}) => { + domain: string; + path: string; + secure: boolean; + sameSite?: 'none' | 'lax' | 'strict'; +}; diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index c961856b7b..8f6aa6c4cc 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,55 @@ # @backstage/plugin-azure-devops-backend +## 0.3.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.3.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.3.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.3.27 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/config@1.0.8 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.3.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + ## 0.3.27-next.1 ### Patch Changes diff --git a/plugins/azure-devops-backend/api-report.md b/plugins/azure-devops-backend/api-report.md index a2a43cd6af..8c916a1495 100644 --- a/plugins/azure-devops-backend/api-report.md +++ b/plugins/azure-devops-backend/api-report.md @@ -96,7 +96,8 @@ export class AzureDevOpsApi { } // @public -export const azureDevOpsPlugin: () => BackendFeature; +const azureDevOpsPlugin: () => BackendFeature; +export default azureDevOpsPlugin; // @public (undocumented) export function createRouter(options: RouterOptions): Promise; diff --git a/plugins/azure-devops-backend/catalog-info.yaml b/plugins/azure-devops-backend/catalog-info.yaml new file mode 100644 index 0000000000..4f45937842 --- /dev/null +++ b/plugins/azure-devops-backend/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-azure-devops-backend + title: '@backstage/plugin-azure-devops-backend' +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: awanlin diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index 857b1643b3..9be538bff4 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-backend", - "version": "0.3.27-next.1", + "version": "0.3.30-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/azure-devops-backend/src/index.ts b/plugins/azure-devops-backend/src/index.ts index c0b1276acf..3c802e7144 100644 --- a/plugins/azure-devops-backend/src/index.ts +++ b/plugins/azure-devops-backend/src/index.ts @@ -22,4 +22,4 @@ export { AzureDevOpsApi } from './api'; export * from './service/router'; -export { azureDevOpsPlugin } from './plugin'; +export { azureDevOpsPlugin as default } from './plugin'; diff --git a/plugins/azure-devops-common/catalog-info.yaml b/plugins/azure-devops-common/catalog-info.yaml new file mode 100644 index 0000000000..939f6ec031 --- /dev/null +++ b/plugins/azure-devops-common/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-azure-devops-common + title: '@backstage/plugin-azure-devops-common' +spec: + lifecycle: experimental + type: backstage-common-library + owner: awanlin diff --git a/plugins/azure-devops-common/package.json b/plugins/azure-devops-common/package.json index cf0b33275f..d99c411f79 100644 --- a/plugins/azure-devops-common/package.json +++ b/plugins/azure-devops-common/package.json @@ -22,6 +22,7 @@ "keywords": [ "backstage" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md index 1b405ad870..1a053d40cb 100644 --- a/plugins/azure-devops/CHANGELOG.md +++ b/plugins/azure-devops/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-azure-devops +## 0.3.6-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.3.5-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-devops-common@0.3.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.3.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.3.3-next.0 ### Patch Changes diff --git a/plugins/azure-devops/api-report.md b/plugins/azure-devops/api-report.md index d083d00dba..2c916ec33b 100644 --- a/plugins/azure-devops/api-report.md +++ b/plugins/azure-devops/api-report.md @@ -14,8 +14,10 @@ import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { GitTag } from '@backstage/plugin-azure-devops-common'; import { IdentityApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { PullRequest } from '@backstage/plugin-azure-devops-common'; import { PullRequestOptions } from '@backstage/plugin-azure-devops-common'; +import { default as React_2 } from 'react'; import { Readme } from '@backstage/plugin-azure-devops-common'; import { ReadmeConfig } from '@backstage/plugin-azure-devops-common'; import { RepoBuild } from '@backstage/plugin-azure-devops-common'; @@ -163,14 +165,16 @@ export class AzureDevOpsClient implements AzureDevOpsApi { export const azureDevOpsPlugin: BackstagePlugin<{}, {}, {}>; // @public (undocumented) -export const AzurePullRequestsIcon: (props: SvgIconProps) => JSX.Element; +export const AzurePullRequestsIcon: ( + props: SvgIconProps, +) => React_2.JSX.Element; // @public (undocumented) export const AzurePullRequestsPage: (props: { projectName?: string | undefined; pollingInterval?: number | undefined; defaultColumnConfigs?: PullRequestColumnConfig[] | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export type BaseFilter = { @@ -223,22 +227,22 @@ export type CreatedByUserFilter = BaseFilter & ); // @public (undocumented) -export const EntityAzureGitTagsContent: () => JSX.Element; +export const EntityAzureGitTagsContent: () => JSX_2.Element; // @public (undocumented) export const EntityAzurePipelinesContent: (props: { defaultLimit?: number | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityAzurePullRequestsContent: (props: { defaultLimit?: number | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityAzureReadmeCard: (props: { maxHeight?: number | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export type Filter = diff --git a/plugins/azure-devops/catalog-info.yaml b/plugins/azure-devops/catalog-info.yaml new file mode 100644 index 0000000000..7f2b9be648 --- /dev/null +++ b/plugins/azure-devops/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-azure-devops + title: '@backstage/plugin-azure-devops' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: awanlin diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index daaedae825..c405779a37 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops", - "version": "0.3.3-next.0", + "version": "0.3.6-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,6 +18,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/azure-devops" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -38,6 +39,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "humanize-duration": "^3.27.0", "luxon": "^3.0.0", "react-use": "^17.2.4" @@ -56,8 +58,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/azure-sites-backend/CHANGELOG.md b/plugins/azure-sites-backend/CHANGELOG.md index 4a025ba54e..759e66ef23 100644 --- a/plugins/azure-sites-backend/CHANGELOG.md +++ b/plugins/azure-sites-backend/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/plugin-azure-sites-backend +## 0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config@1.0.8 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + ## 0.1.10-next.1 ### Patch Changes diff --git a/plugins/azure-sites-backend/catalog-info.yaml b/plugins/azure-sites-backend/catalog-info.yaml new file mode 100644 index 0000000000..07ed594da8 --- /dev/null +++ b/plugins/azure-sites-backend/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-azure-sites-backend + title: '@backstage/plugin-azure-sites-backend' +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: maintainers diff --git a/plugins/azure-sites-backend/package.json b/plugins/azure-sites-backend/package.json index 93cdffe8a5..08c381729b 100644 --- a/plugins/azure-sites-backend/package.json +++ b/plugins/azure-sites-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-sites-backend", - "version": "0.1.10-next.1", + "version": "0.1.13-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/azure-sites-common/catalog-info.yaml b/plugins/azure-sites-common/catalog-info.yaml new file mode 100644 index 0000000000..74d7be8715 --- /dev/null +++ b/plugins/azure-sites-common/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-azure-sites-common + title: '@backstage/plugin-azure-sites-common' + description: Common functionalities for the azure plugin +spec: + lifecycle: experimental + type: backstage-common-library + owner: maintainers diff --git a/plugins/azure-sites-common/package.json b/plugins/azure-sites-common/package.json index c1bf28c65b..e264d5cb36 100644 --- a/plugins/azure-sites-common/package.json +++ b/plugins/azure-sites-common/package.json @@ -23,6 +23,7 @@ "keywords": [ "backstage" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/plugins/azure-sites/CHANGELOG.md b/plugins/azure-sites/CHANGELOG.md index 636c69c76a..d65ad0bc0c 100644 --- a/plugins/azure-sites/CHANGELOG.md +++ b/plugins/azure-sites/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/plugin-azure-sites +## 0.1.13-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.12-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-sites-common@0.1.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.1.10-next.0 ### Patch Changes diff --git a/plugins/azure-sites/api-report.md b/plugins/azure-sites/api-report.md index 38ca6a8fe2..bec89e17bc 100644 --- a/plugins/azure-sites/api-report.md +++ b/plugins/azure-sites/api-report.md @@ -13,6 +13,8 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { IdentityApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -40,7 +42,7 @@ export class AzureSitesApiBackendClient implements AzureSitesApi { } // @public (undocumented) -export const AzureSitesOverviewWidget: () => JSX.Element; +export const AzureSitesOverviewWidget: () => React_2.JSX.Element; // @public (undocumented) export const azureSitesPlugin: BackstagePlugin< @@ -52,7 +54,7 @@ export const azureSitesPlugin: BackstagePlugin< >; // @public (undocumented) -export const EntityAzureSitesOverviewWidget: () => JSX.Element; +export const EntityAzureSitesOverviewWidget: () => JSX_2.Element; // @public (undocumented) export const isAzureWebSiteNameAvailable: ( diff --git a/plugins/azure-sites/catalog-info.yaml b/plugins/azure-sites/catalog-info.yaml new file mode 100644 index 0000000000..0902759994 --- /dev/null +++ b/plugins/azure-sites/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-azure-sites + title: '@backstage/plugin-azure-sites' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/azure-sites/package.json b/plugins/azure-sites/package.json index 82b1bd1041..db391d1a6e 100644 --- a/plugins/azure-sites/package.json +++ b/plugins/azure-sites/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-sites", - "version": "0.1.10-next.0", + "version": "0.1.13-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,6 +22,7 @@ "backstage", "azure" ], + "sideEffects": false, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", @@ -41,6 +42,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "react-use": "^17.2.4" }, @@ -58,8 +60,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md index 4645a45090..7188e68e81 100644 --- a/plugins/badges-backend/CHANGELOG.md +++ b/plugins/badges-backend/CHANGELOG.md @@ -1,5 +1,68 @@ # @backstage/plugin-badges-backend +## 0.2.6-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + +## 0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## 0.2.3 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## 0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + ## 0.2.3-next.1 ### Patch Changes diff --git a/plugins/badges-backend/api-report.md b/plugins/badges-backend/api-report.md index 4594228543..ab5dbd9720 100644 --- a/plugins/badges-backend/api-report.md +++ b/plugins/badges-backend/api-report.md @@ -83,7 +83,8 @@ export type BadgeSpec = { }; // @public -export const badgesPlugin: () => BackendFeature; +const badgesPlugin: () => BackendFeature; +export default badgesPlugin; // @public export interface BadgesStore { diff --git a/plugins/badges-backend/catalog-info.yaml b/plugins/badges-backend/catalog-info.yaml new file mode 100644 index 0000000000..7d4e1e85a1 --- /dev/null +++ b/plugins/badges-backend/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-badges-backend + title: '@backstage/plugin-badges-backend' + description: A Backstage backend plugin that generates README badges for your entities +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: maintainers diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index f68854b9f6..263b7e2b60 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges-backend", "description": "A Backstage backend plugin that generates README badges for your entities", - "version": "0.2.3-next.1", + "version": "0.2.6-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -56,7 +56,6 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/cli": "workspace:^", - "@types/node": "*", "cross-fetch": "^3.1.5" }, "files": [ diff --git a/plugins/badges-backend/src/index.ts b/plugins/badges-backend/src/index.ts index 0c417a7ad6..b114704565 100644 --- a/plugins/badges-backend/src/index.ts +++ b/plugins/badges-backend/src/index.ts @@ -25,4 +25,4 @@ export * from './lib'; export * from './service/router'; export * from './types'; export * from './database/badgesStore'; -export { badgesPlugin } from './plugin'; +export { badgesPlugin as default } from './plugin'; diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md index 5d7527fba4..6f9a3d5424 100644 --- a/plugins/badges/CHANGELOG.md +++ b/plugins/badges/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/plugin-badges +## 0.2.48-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.2.48-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.2.47-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.2.45 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.2.45-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.2.45-next.0 ### Patch Changes diff --git a/plugins/badges/api-report.md b/plugins/badges/api-report.md index b051bbb452..e73a887316 100644 --- a/plugins/badges/api-report.md +++ b/plugins/badges/api-report.md @@ -6,6 +6,7 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; // @public (undocumented) export const badgesPlugin: BackstagePlugin<{}, {}, {}>; @@ -14,5 +15,5 @@ export const badgesPlugin: BackstagePlugin<{}, {}, {}>; export const EntityBadgesDialog: (props: { open: boolean; onClose?: (() => any) | undefined; -}) => JSX.Element; +}) => JSX_2.Element; ``` diff --git a/plugins/badges/catalog-info.yaml b/plugins/badges/catalog-info.yaml new file mode 100644 index 0000000000..1e96e514dc --- /dev/null +++ b/plugins/badges/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-badges + title: '@backstage/plugin-badges' + description: A Backstage plugin that generates README badges for your entities +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 79add2944c..0957145fa8 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges", "description": "A Backstage plugin that generates README badges for your entities", - "version": "0.2.45-next.0", + "version": "0.2.48-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,6 +19,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/badges" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -38,6 +39,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -50,8 +52,7 @@ "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", "@backstage/test-utils": "workspace:^", - "@testing-library/jest-dom": "^5.16.5", - "@types/node": "^16.11.26" + "@testing-library/jest-dom": "^5.16.5" }, "files": [ "dist" diff --git a/plugins/bazaar-backend/CHANGELOG.md b/plugins/bazaar-backend/CHANGELOG.md index f505eb9b81..defdc9de0e 100644 --- a/plugins/bazaar-backend/CHANGELOG.md +++ b/plugins/bazaar-backend/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-bazaar-backend +## 0.2.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/errors@1.2.1 + +## 0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/errors@1.2.1 + +## 0.2.13-next.0 + +### Patch Changes + +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## 0.2.11 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## 0.2.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + ## 0.2.11-next.1 ### Patch Changes diff --git a/plugins/bazaar-backend/api-report.md b/plugins/bazaar-backend/api-report.md index 6281530851..d9243fd3af 100644 --- a/plugins/bazaar-backend/api-report.md +++ b/plugins/bazaar-backend/api-report.md @@ -11,7 +11,8 @@ import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; // @alpha -export const bazaarPlugin: () => BackendFeature; +const bazaarPlugin: () => BackendFeature; +export default bazaarPlugin; // @public (undocumented) export function createRouter(options: RouterOptions): Promise; diff --git a/plugins/bazaar-backend/catalog-info.yaml b/plugins/bazaar-backend/catalog-info.yaml new file mode 100644 index 0000000000..ca9a705b52 --- /dev/null +++ b/plugins/bazaar-backend/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-bazaar-backend + title: '@backstage/plugin-bazaar-backend' +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: maintainers diff --git a/plugins/bazaar-backend/migrations/20211014144054_init.js b/plugins/bazaar-backend/migrations/20211014144054_init.js index 503812225d..480771acb4 100644 --- a/plugins/bazaar-backend/migrations/20211014144054_init.js +++ b/plugins/bazaar-backend/migrations/20211014144054_init.js @@ -23,7 +23,7 @@ exports.up = async function up(knex) { await knex.schema.createTable('metadata', table => { table.comment('The table of Bazaar metadata'); table - .text('entity_ref') + .string('entity_ref') .notNullable() .unique() .comment('The ref of the entity'); @@ -49,7 +49,7 @@ exports.up = async function up(knex) { await knex.schema.createTable('members', table => { table.comment('The table of Bazaar members'); table - .text('entity_ref') + .string('entity_ref') .notNullable() .references('metadata.entity_ref') .onDelete('CASCADE') diff --git a/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js b/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js index 0953d5c2b1..ae2618fbef 100644 --- a/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js +++ b/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js @@ -89,9 +89,11 @@ exports.up = async function up(knex) { await knex.schema.alterTable('members', table => { table .integer('item_id') + .unsigned() .references('metadata.id') .onDelete('CASCADE') .comment('Id of the associated item'); + table.dropForeign('entity_ref'); table.dropColumn('entity_ref'); }); } diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index 6532c6c291..ecc7f409d2 100644 --- a/plugins/bazaar-backend/package.json +++ b/plugins/bazaar-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar-backend", - "version": "0.2.11-next.1", + "version": "0.2.14-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/bazaar-backend/src/index.ts b/plugins/bazaar-backend/src/index.ts index dd0cdbb750..d6f3df2d04 100644 --- a/plugins/bazaar-backend/src/index.ts +++ b/plugins/bazaar-backend/src/index.ts @@ -15,4 +15,4 @@ */ export * from './service/router'; -export { bazaarPlugin } from './plugin'; +export { bazaarPlugin as default } from './plugin'; diff --git a/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts b/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts index 0c78dbdd87..4e46581240 100644 --- a/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts +++ b/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts @@ -36,7 +36,7 @@ jest.setTimeout(60_000); describe('DatabaseHandler', () => { const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3', 'MYSQL_8'], }); function createDatabaseManager( diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md index b9e8e4001e..ae4847fe30 100644 --- a/plugins/bazaar/CHANGELOG.md +++ b/plugins/bazaar/CHANGELOG.md @@ -1,5 +1,76 @@ # @backstage/plugin-bazaar +## 0.2.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.13.0-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/cli@0.22.13-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.2.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/plugin-catalog@1.12.5-next.1 + - @backstage/cli@0.22.13-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.2.15-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/cli@0.22.12-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.2.12 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/cli@0.22.10 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog@1.12.1 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.2.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/plugin-catalog@1.12.1-next.2 + - @backstage/cli@0.22.10-next.1 + ## 0.2.12-next.1 ### Patch Changes diff --git a/plugins/bazaar/api-report.md b/plugins/bazaar/api-report.md index 11ce735bf5..282983b378 100644 --- a/plugins/bazaar/api-report.md +++ b/plugins/bazaar/api-report.md @@ -8,12 +8,14 @@ import { ApiHolder } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) export const BazaarOverviewCard: ( props: BazaarOverviewCardProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type BazaarOverviewCardProps = { @@ -24,7 +26,7 @@ export type BazaarOverviewCardProps = { }; // @public (undocumented) -export const BazaarPage: (props: BazaarPageProps) => JSX.Element; +export const BazaarPage: (props: BazaarPageProps) => JSX_2.Element; // @public (undocumented) export type BazaarPageProps = { @@ -44,7 +46,7 @@ export const bazaarPlugin: BackstagePlugin< >; // @public (undocumented) -export const EntityBazaarInfoCard: () => JSX.Element | null; +export const EntityBazaarInfoCard: () => React_2.JSX.Element | null; // @public (undocumented) export const isBazaarAvailable: ( @@ -55,7 +57,7 @@ export const isBazaarAvailable: ( ) => Promise; // @public (undocumented) -export const SortView: (props: SortViewProps) => JSX.Element; +export const SortView: (props: SortViewProps) => React_2.JSX.Element; // @public (undocumented) export type SortViewProps = { diff --git a/plugins/bazaar/catalog-info.yaml b/plugins/bazaar/catalog-info.yaml new file mode 100644 index 0000000000..4fb2562731 --- /dev/null +++ b/plugins/bazaar/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-bazaar + title: '@backstage/plugin-bazaar' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index d0f26d68f0..17aade25be 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar", - "version": "0.2.12-next.1", + "version": "0.2.16-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,6 +18,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/bazaar" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -43,6 +44,7 @@ "@material-ui/lab": "4.0.0-alpha.61", "@material-ui/pickers": "^3.3.10", "@testing-library/jest-dom": "^5.10.1", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "material-ui-search-bar": "^1.0.0", "react-hook-form": "^7.13.0", @@ -56,8 +58,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/dev-utils": "workspace:^", - "@testing-library/jest-dom": "^5.10.1", - "@types/react": "^16.13.1 || ^17.0.0" + "@testing-library/jest-dom": "^5.10.1" }, "files": [ "dist" diff --git a/plugins/bitbucket-cloud-common/CHANGELOG.md b/plugins/bitbucket-cloud-common/CHANGELOG.md index 1afd2eb74e..d21ff40a7b 100644 --- a/plugins/bitbucket-cloud-common/CHANGELOG.md +++ b/plugins/bitbucket-cloud-common/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-bitbucket-cloud-common +## 0.2.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.2 + +## 0.2.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.1 + +## 0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + +## 0.2.9 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/integration@1.6.0 + ## 0.2.9-next.0 ### Patch Changes diff --git a/plugins/bitbucket-cloud-common/catalog-info.yaml b/plugins/bitbucket-cloud-common/catalog-info.yaml new file mode 100644 index 0000000000..1a931c06cb --- /dev/null +++ b/plugins/bitbucket-cloud-common/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-bitbucket-cloud-common + title: '@backstage/plugin-bitbucket-cloud-common' + description: Common functionalities for bitbucket-cloud plugins +spec: + lifecycle: experimental + type: backstage-common-library + owner: pjungermann diff --git a/plugins/bitbucket-cloud-common/package.json b/plugins/bitbucket-cloud-common/package.json index 7d1ef19b6b..1367eeca4b 100644 --- a/plugins/bitbucket-cloud-common/package.json +++ b/plugins/bitbucket-cloud-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-bitbucket-cloud-common", "description": "Common functionalities for bitbucket-cloud plugins", - "version": "0.2.9-next.0", + "version": "0.2.12-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,6 +20,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/bitbucket-cloud-common" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md index 51a286ff26..47fe1d0703 100644 --- a/plugins/bitrise/CHANGELOG.md +++ b/plugins/bitrise/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-bitrise +## 0.1.51-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## 0.1.51-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## 0.1.50-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.1.48 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## 0.1.48-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.1.48-next.1 ### Patch Changes diff --git a/plugins/bitrise/api-report.md b/plugins/bitrise/api-report.md index 01d10c61cf..7f2e44f04f 100644 --- a/plugins/bitrise/api-report.md +++ b/plugins/bitrise/api-report.md @@ -7,6 +7,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; // @public (undocumented) export const BITRISE_APP_ANNOTATION = 'bitrise.io/app'; @@ -15,7 +16,7 @@ export const BITRISE_APP_ANNOTATION = 'bitrise.io/app'; export const bitrisePlugin: BackstagePlugin<{}, {}, {}>; // @public (undocumented) -export const EntityBitriseContent: () => JSX.Element; +export const EntityBitriseContent: () => JSX_2.Element; // @public (undocumented) export const isBitriseAvailable: (entity: Entity) => boolean; diff --git a/plugins/bitrise/catalog-info.yaml b/plugins/bitrise/catalog-info.yaml new file mode 100644 index 0000000000..19df0a6736 --- /dev/null +++ b/plugins/bitrise/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-bitrise + title: '@backstage/plugin-bitrise' + description: A Backstage plugin that integrates towards Bitrise +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: sda-se-reviewers diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 1ee1d7efbe..23352407d6 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-bitrise", "description": "A Backstage plugin that integrates towards Bitrise", - "version": "0.1.48-next.1", + "version": "0.1.51-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,6 +19,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/bitrise" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -37,6 +38,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "lodash": "^4.17.21", "luxon": "^3.0.0", "qs": "^6.9.6", @@ -57,8 +59,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "@types/recharts": "^1.8.15", "msw": "^1.0.0" }, diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index 09d7823993..4119519634 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,90 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/integration-aws-node@0.1.6-next.1 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-kubernetes-common@0.6.6-next.1 + +## 0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.6-next.0 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/integration-aws-node@0.1.6-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/integration-aws-node@0.1.5 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-kubernetes-common@0.6.5 + +## 0.2.3 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- b4222908b0c3: Added option to configure AWS `accountId` in `AwsS3EntityProvider` +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/integration-aws-node@0.1.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-kubernetes-common@0.6.5 + +## 0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + ## 0.2.3-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/alpha-api-report.md b/plugins/catalog-backend-module-aws/alpha-api-report.md index f78f6a802d..218acc7506 100644 --- a/plugins/catalog-backend-module-aws/alpha-api-report.md +++ b/plugins/catalog-backend-module-aws/alpha-api-report.md @@ -6,7 +6,8 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; // @alpha -export const catalogModuleAwsS3EntityProvider: () => BackendFeature; +const catalogModuleAwsS3EntityProvider: () => BackendFeature; +export default catalogModuleAwsS3EntityProvider; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog-backend-module-aws/catalog-info.yaml b/plugins/catalog-backend-module-aws/catalog-info.yaml new file mode 100644 index 0000000000..8bff4a9a37 --- /dev/null +++ b/plugins/catalog-backend-module-aws/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-aws + title: '@backstage/plugin-catalog-backend-module-aws' + description: A Backstage catalog backend module that helps integrate towards AWS +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: pjungermann diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index bd5377fb67..b627437a5f 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", "description": "A Backstage catalog backend module that helps integrate towards AWS", - "version": "0.2.3-next.1", + "version": "0.2.6-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-aws/src/alpha.ts b/plugins/catalog-backend-module-aws/src/alpha.ts index c277a0a680..01a9b25f7c 100644 --- a/plugins/catalog-backend-module-aws/src/alpha.ts +++ b/plugins/catalog-backend-module-aws/src/alpha.ts @@ -15,3 +15,4 @@ */ export * from './module'; +export { default } from './module'; 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 7599f8446f..ad1e94cad6 100644 --- a/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts +++ b/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts @@ -14,14 +14,8 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { coreServices } from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; -import { startTestBackend } from '@backstage/backend-test-utils'; -import { ConfigReader } from '@backstage/config'; +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'; @@ -38,14 +32,14 @@ describe('catalogModuleAwsS3EntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); - const config = new ConfigReader({ + const config = { catalog: { providers: { awsS3: { @@ -57,16 +51,15 @@ describe('catalogModuleAwsS3EntityProvider', () => { }, }, }, - }); + }; await startTestBackend({ extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], - services: [ - [coreServices.rootConfig, config], - [coreServices.logger, getVoidLogger()], - [coreServices.scheduler, scheduler], + features: [ + catalogModuleAwsS3EntityProvider(), + mockServices.rootConfig.factory({ data: config }), + scheduler.factory, ], - features: [catalogModuleAwsS3EntityProvider()], }); expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); diff --git a/plugins/catalog-backend-module-aws/src/module/index.ts b/plugins/catalog-backend-module-aws/src/module/index.ts index 5644c034ea..2734ddafcd 100644 --- a/plugins/catalog-backend-module-aws/src/module/index.ts +++ b/plugins/catalog-backend-module-aws/src/module/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { catalogModuleAwsS3EntityProvider } from './catalogModuleAwsS3EntityProvider'; +export { catalogModuleAwsS3EntityProvider as default } from './catalogModuleAwsS3EntityProvider'; diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md index bf2ce7bb76..169781835a 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.22-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + +## 0.1.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.1.21-next.0 + +### Patch Changes + +- 94f96508491d: Improve consistency of results from the `AzureDevOpsEntityProvider`. +- 5f1a92b9f19f: Use `DefaultAzureDevOpsCredentialsProvider` to retrieve credentials for Azure DevOps. +- 044b4f2fb1e3: Remove duplications from Azure search before committing the new locations to the catalog. +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## 0.1.19 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + +## 0.1.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + ## 0.1.19-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-azure/alpha-api-report.md b/plugins/catalog-backend-module-azure/alpha-api-report.md index 66551a2856..eeb5a3c621 100644 --- a/plugins/catalog-backend-module-azure/alpha-api-report.md +++ b/plugins/catalog-backend-module-azure/alpha-api-report.md @@ -6,7 +6,8 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; // @alpha -export const catalogModuleAzureDevOpsEntityProvider: () => BackendFeature; +const catalogModuleAzureDevOpsEntityProvider: () => BackendFeature; +export default catalogModuleAzureDevOpsEntityProvider; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog-backend-module-azure/catalog-info.yaml b/plugins/catalog-backend-module-azure/catalog-info.yaml new file mode 100644 index 0000000000..c1a74cac6f --- /dev/null +++ b/plugins/catalog-backend-module-azure/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-azure + title: '@backstage/plugin-catalog-backend-module-azure' + description: A Backstage catalog backend module that helps integrate towards Azure +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index fe9cb9a7cc..899f51dceb 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-azure", "description": "A Backstage catalog backend module that helps integrate towards Azure", - "version": "0.1.19-next.1", + "version": "0.1.22-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-azure/src/alpha.ts b/plugins/catalog-backend-module-azure/src/alpha.ts index c277a0a680..01a9b25f7c 100644 --- a/plugins/catalog-backend-module-azure/src/alpha.ts +++ b/plugins/catalog-backend-module-azure/src/alpha.ts @@ -15,3 +15,4 @@ */ export * from './module'; +export { default } from './module'; 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 f26208353a..493f0dda51 100644 --- a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts @@ -18,11 +18,40 @@ import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { codeSearch, CodeSearchResponse } from './azure'; +import { + DefaultAzureDevOpsCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; describe('azure', () => { const server = setupServer(); setupRequestMockHandlers(server); + const createFixture = (host: string, token: string) => { + const azureConfig = { + host: host, + credentials: [ + { + personalAccessToken: token, + }, + ], + }; + const scmIntegrations = ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + azure: [azureConfig], + }, + }), + ); + + return { + azureConfig: scmIntegrations.azure.byHost(host)?.config!, + credentialsProvider: + DefaultAzureDevOpsCredentialsProvider.fromIntegrations(scmIntegrations), + }; + }; + describe('codeSearch', () => { it('returns empty when nothing is found', async () => { const response: CodeSearchResponse = { count: 0, results: [] }; @@ -34,6 +63,12 @@ describe('azure', () => { expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); expect(req.body).toEqual({ searchText: 'path:/catalog-info.yaml repo:* proj:engineering', + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], $skip: 0, $top: 1000, }); @@ -42,9 +77,14 @@ describe('azure', () => { ), ); + const { credentialsProvider, azureConfig } = createFixture( + 'dev.azure.com', + 'ABC', + ); await expect( codeSearch( - { host: 'dev.azure.com', token: 'ABC' }, + credentialsProvider, + azureConfig, 'shopify', 'engineering', '', @@ -88,6 +128,12 @@ describe('azure', () => { expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); expect(req.body).toEqual({ searchText: 'path:/catalog-info.yaml repo:* proj:engineering', + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], $skip: 0, $top: 1000, }); @@ -96,9 +142,14 @@ describe('azure', () => { ), ); + const { credentialsProvider, azureConfig } = createFixture( + 'dev.azure.com', + 'ABC', + ); await expect( codeSearch( - { host: 'dev.azure.com', token: 'ABC' }, + credentialsProvider, + azureConfig, 'shopify', 'engineering', '', @@ -132,6 +183,12 @@ describe('azure', () => { expect(req.body).toEqual({ searchText: 'path:/catalog-info.yaml repo:backstage proj:engineering', + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], $skip: 0, $top: 1000, }); @@ -140,9 +197,15 @@ describe('azure', () => { ), ); + const { credentialsProvider, azureConfig } = createFixture( + 'dev.azure.com', + 'ABC', + ); + await expect( codeSearch( - { host: 'dev.azure.com', token: 'ABC' }, + credentialsProvider, + azureConfig, 'shopify', 'engineering', 'backstage', @@ -175,6 +238,12 @@ describe('azure', () => { expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); expect(req.body).toEqual({ searchText: 'path:/catalog-info.yaml repo:* proj:engineering', + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], $skip: 0, $top: 1000, }); @@ -183,9 +252,15 @@ describe('azure', () => { ), ); + const { credentialsProvider, azureConfig } = createFixture( + 'azuredevops.mycompany.com', + 'ABC', + ); + await expect( codeSearch( - { host: 'azuredevops.mycompany.com', token: 'ABC' }, + credentialsProvider, + azureConfig, 'shopify', 'engineering', '', @@ -236,9 +311,15 @@ describe('azure', () => { ), ); + const { credentialsProvider, azureConfig } = createFixture( + 'dev.azure.com', + 'ABC', + ); + await expect( codeSearch( - { host: 'dev.azure.com', token: 'ABC' }, + credentialsProvider, + azureConfig, 'shopify', 'engineering', 'backstage', diff --git a/plugins/catalog-backend-module-azure/src/lib/azure.ts b/plugins/catalog-backend-module-azure/src/lib/azure.ts index 916d70a9fa..c8ff8dd739 100644 --- a/plugins/catalog-backend-module-azure/src/lib/azure.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.ts @@ -16,8 +16,8 @@ import fetch from 'node-fetch'; import { + AzureDevOpsCredentialsProvider, AzureIntegrationConfig, - getAzureRequestOptions, } from '@backstage/integration'; export interface CodeSearchResponse { @@ -42,6 +42,7 @@ const PAGE_SIZE = 1000; // codeSearch returns all files that matches the given search path. export async function codeSearch( + credentialsProvider: AzureDevOpsCredentialsProvider, azureConfig: AzureIntegrationConfig, org: string, project: string, @@ -57,13 +58,24 @@ export async function codeSearch( let hasMorePages = true; do { + const credentials = await credentialsProvider.getCredentials({ + url: `https://${azureConfig.host}/${org}`, + }); + const response = await fetch(searchUrl, { - ...(await getAzureRequestOptions(azureConfig, { + headers: { + ...credentials?.headers, 'Content-Type': 'application/json', - })), + }, method: 'POST', body: JSON.stringify({ searchText: `path:${path} repo:${repo || '*'} proj:${project || '*'}`, + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], $skip: items.length, $top: PAGE_SIZE, }), 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 e3140f4583..a20921fa3f 100644 --- a/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts +++ b/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts @@ -14,14 +14,8 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { coreServices } from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; -import { startTestBackend } from '@backstage/backend-test-utils'; -import { ConfigReader } from '@backstage/config'; +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'; @@ -38,14 +32,14 @@ describe('catalogModuleAzureDevOpsEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); - const config = new ConfigReader({ + const config = { catalog: { providers: { azureDevOps: { @@ -60,16 +54,16 @@ describe('catalogModuleAzureDevOpsEntityProvider', () => { }, }, }, - }); + }; await startTestBackend({ extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], - services: [ - [coreServices.rootConfig, config], - [coreServices.logger, getVoidLogger()], - [coreServices.scheduler, scheduler], + features: [ + catalogModuleAzureDevOpsEntityProvider(), + mockServices.rootConfig.factory({ data: config }), + mockServices.logger.factory(), + scheduler.factory, ], - features: [catalogModuleAzureDevOpsEntityProvider()], }); expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); diff --git a/plugins/catalog-backend-module-azure/src/module/index.ts b/plugins/catalog-backend-module-azure/src/module/index.ts index cfa8c78548..329ad604e2 100644 --- a/plugins/catalog-backend-module-azure/src/module/index.ts +++ b/plugins/catalog-backend-module-azure/src/module/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { catalogModuleAzureDevOpsEntityProvider } from './catalogModuleAzureDevOpsEntityProvider'; +export { catalogModuleAzureDevOpsEntityProvider as default } from './catalogModuleAzureDevOpsEntityProvider'; diff --git a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts index 4a8e1d4537..36dd920621 100644 --- a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts @@ -158,6 +158,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { await processor.readLocation(location, false, emitter); expect(mockCodeSearch).toHaveBeenCalledWith( + expect.anything(), { host: 'dev.azure.com' }, 'shopify', 'engineering', @@ -207,6 +208,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { await processor.readLocation(location, false, emitter); expect(mockCodeSearch).toHaveBeenCalledWith( + expect.anything(), { host: 'dev.azure.com' }, 'shopify', 'engineering', @@ -248,6 +250,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { await processor.readLocation(location, false, emitter); expect(mockCodeSearch).toHaveBeenCalledWith( + expect.anything(), { host: 'dev.azure.com' }, 'shopify', 'engineering', @@ -277,6 +280,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { await processor.readLocation(location, false, emitter); expect(mockCodeSearch).toHaveBeenCalledWith( + expect.anything(), { host: 'dev.azure.com' }, 'shopify', 'engineering', diff --git a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts index bad6067dc3..d9638e6cc7 100644 --- a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts @@ -16,6 +16,8 @@ import { Config } from '@backstage/config'; import { + AzureDevOpsCredentialsProvider, + DefaultAzureDevOpsCredentialsProvider, ScmIntegrationRegistry, ScmIntegrations, } from '@backstage/integration'; @@ -46,6 +48,7 @@ import { codeSearch } from '../lib'; */ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { private readonly integrations: ScmIntegrationRegistry; + private readonly credentialsProvider: AzureDevOpsCredentialsProvider; private readonly logger: Logger; static fromConfig(config: Config, options: { logger: Logger }) { @@ -63,6 +66,10 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { }) { this.integrations = options.integrations; this.logger = options.logger; + this.credentialsProvider = + DefaultAzureDevOpsCredentialsProvider.fromIntegrations( + options.integrations, + ); } getProcessorName(): string { @@ -93,6 +100,7 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { ); const files = await codeSearch( + this.credentialsProvider, azureConfig, org, project, diff --git a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts index 5abd23ce00..92398779c7 100644 --- a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts +++ b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts @@ -16,7 +16,12 @@ import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks'; import { Config } from '@backstage/config'; -import { AzureIntegration, ScmIntegrations } from '@backstage/integration'; +import { + AzureDevOpsCredentialsProvider, + AzureIntegration, + DefaultAzureDevOpsCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; import { EntityProvider, EntityProviderConnection, @@ -50,6 +55,9 @@ export class AzureDevOpsEntityProvider implements EntityProvider { }, ): AzureDevOpsEntityProvider[] { const providerConfigs = readAzureDevOpsConfigs(configRoot); + const scmIntegrations = ScmIntegrations.fromConfig(configRoot); + const credentialsProvider = + DefaultAzureDevOpsCredentialsProvider.fromIntegrations(scmIntegrations); if (!options.schedule && !options.scheduler) { throw new Error('Either schedule or scheduler must be provided.'); @@ -79,6 +87,7 @@ export class AzureDevOpsEntityProvider implements EntityProvider { return new AzureDevOpsEntityProvider( providerConfig, integration, + credentialsProvider, options.logger, taskRunner, ); @@ -88,6 +97,7 @@ export class AzureDevOpsEntityProvider implements EntityProvider { private constructor( private readonly config: AzureDevOpsConfig, private readonly integration: AzureIntegration, + private readonly credentialsProvider: AzureDevOpsCredentialsProvider, logger: Logger, taskRunner: TaskRunner, ) { @@ -139,6 +149,7 @@ export class AzureDevOpsEntityProvider implements EntityProvider { logger.info('Discovering Azure DevOps catalog files'); const files = await codeSearch( + this.credentialsProvider, this.integration.config, this.config.organization, this.config.project, @@ -148,7 +159,10 @@ export class AzureDevOpsEntityProvider implements EntityProvider { logger.info(`Discovered ${files.length} catalog files`); - const locations = files.map(key => this.createLocationSpec(key)); + const targets = files.map(key => this.createObjectUrl(key)); + const locations = Array.from(new Set(targets)).map(key => + this.createLocationSpec(key), + ); await this.connection.applyMutation({ type: 'full', @@ -165,9 +179,7 @@ export class AzureDevOpsEntityProvider implements EntityProvider { ); } - private createLocationSpec(file: CodeSearchResultItem): LocationSpec { - const target = this.createObjectUrl(file); - + private createLocationSpec(target: string): LocationSpec { return { type: 'url', target: target, diff --git a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md index 593e1fd2a1..b3b5960077 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,86 @@ # @backstage/plugin-catalog-backend-module-bitbucket-cloud +## 0.1.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/plugin-bitbucket-cloud-common@0.2.12-next.2 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-events-node@0.2.12-next.2 + +## 0.1.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.12-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-events-node@0.2.12-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + +## 0.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-bitbucket-cloud-common@0.2.11-next.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + +## 0.1.15 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.9 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/plugin-events-node@0.2.9 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-common@1.0.15 + +## 0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-events-node@0.2.9-next.2 + ## 0.1.15-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-cloud/alpha-api-report.md b/plugins/catalog-backend-module-bitbucket-cloud/alpha-api-report.md index a5d0ffc665..69b2b46e4e 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/alpha-api-report.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/alpha-api-report.md @@ -6,7 +6,8 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; // @alpha (undocumented) -export const catalogModuleBitbucketCloudEntityProvider: () => BackendFeature; +const catalogModuleBitbucketCloudEntityProvider: () => BackendFeature; +export default catalogModuleBitbucketCloudEntityProvider; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog-backend-module-bitbucket-cloud/catalog-info.yaml b/plugins/catalog-backend-module-bitbucket-cloud/catalog-info.yaml new file mode 100644 index 0000000000..99fc6661db --- /dev/null +++ b/plugins/catalog-backend-module-bitbucket-cloud/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-bitbucket-cloud + title: '@backstage/plugin-catalog-backend-module-bitbucket-cloud' + description: >- + A Backstage catalog backend module that helps integrate towards Bitbucket + Cloud +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: pjungermann diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index 30875be251..ac1dd99dfd 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-cloud", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket Cloud", - "version": "0.1.15-next.1", + "version": "0.1.18-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/alpha.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/alpha.ts index c277a0a680..01a9b25f7c 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/alpha.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/alpha.ts @@ -15,3 +15,4 @@ */ export * from './module'; +export { default } from './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 b46304d4e3..c771a40acc 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 @@ -14,11 +14,7 @@ * limitations under the License. */ -import { coreServices } from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { startTestBackend, mockServices } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; @@ -43,19 +39,20 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); await startTestBackend({ extensionPoints: [ [catalogProcessingExtensionPoint, catalogExtensionPointImpl], [eventsExtensionPoint, eventsExtensionPointImpl], ], - services: [ + features: [ + catalogModuleBitbucketCloudEntityProvider(), mockServices.rootConfig.factory({ data: { catalog: { @@ -71,9 +68,8 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => { }, }, }), - [coreServices.scheduler, scheduler], + scheduler.factory, ], - features: [catalogModuleBitbucketCloudEntityProvider()], }); expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/module/index.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/module/index.ts index f9654971be..22a777ea49 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/module/index.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/module/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { catalogModuleBitbucketCloudEntityProvider } from './catalogModuleBitbucketCloudEntityProvider'; +export { catalogModuleBitbucketCloudEntityProvider as default } from './catalogModuleBitbucketCloudEntityProvider'; diff --git a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md index 52920ff815..b2556975c1 100644 --- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,73 @@ # @backstage/plugin-catalog-backend-module-bitbucket-server +## 0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + +## 0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/errors@1.2.1 + +## 0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## 0.1.13 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## 0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + ## 0.1.13-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-server/alpha-api-report.md b/plugins/catalog-backend-module-bitbucket-server/alpha-api-report.md index 2cc53a3c16..5e5b4f68cd 100644 --- a/plugins/catalog-backend-module-bitbucket-server/alpha-api-report.md +++ b/plugins/catalog-backend-module-bitbucket-server/alpha-api-report.md @@ -6,7 +6,8 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; // @alpha (undocumented) -export const catalogModuleBitbucketServerEntityProvider: () => BackendFeature; +const catalogModuleBitbucketServerEntityProvider: () => BackendFeature; +export default catalogModuleBitbucketServerEntityProvider; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog-backend-module-bitbucket-server/catalog-info.yaml b/plugins/catalog-backend-module-bitbucket-server/catalog-info.yaml new file mode 100644 index 0000000000..47c49530a0 --- /dev/null +++ b/plugins/catalog-backend-module-bitbucket-server/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-bitbucket-server + title: '@backstage/plugin-catalog-backend-module-bitbucket-server' +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index ac28eecc36..5f5e23dff6 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.13-next.1", + "version": "0.1.16-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-bitbucket-server/src/alpha.ts b/plugins/catalog-backend-module-bitbucket-server/src/alpha.ts index c277a0a680..01a9b25f7c 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/alpha.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/alpha.ts @@ -15,3 +15,4 @@ */ export * from './module'; +export { default } from './module'; 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 f072823052..ab92e24fd7 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 @@ -14,14 +14,8 @@ * limitations under the License. */ -import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { coreServices } from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; -import { startTestBackend } from '@backstage/backend-test-utils'; +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'; @@ -38,14 +32,14 @@ describe('catalogModuleBitbucketServerEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); - const config = new ConfigReader({ + const config = { catalog: { providers: { bitbucketServer: { @@ -64,16 +58,16 @@ describe('catalogModuleBitbucketServerEntityProvider', () => { }, ], }, - }); + }; await startTestBackend({ extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], - services: [ - [coreServices.rootConfig, config], - [coreServices.logger, getVoidLogger()], - [coreServices.scheduler, scheduler], + features: [ + catalogModuleBitbucketServerEntityProvider(), + mockServices.rootConfig.factory({ data: config }), + mockServices.logger.factory(), + scheduler.factory, ], - features: [catalogModuleBitbucketServerEntityProvider()], }); expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); diff --git a/plugins/catalog-backend-module-bitbucket-server/src/module/index.ts b/plugins/catalog-backend-module-bitbucket-server/src/module/index.ts index 160f099ae0..033556f566 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/module/index.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/module/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { catalogModuleBitbucketServerEntityProvider } from './catalogModuleBitbucketServerEntityProvider'; +export { catalogModuleBitbucketServerEntityProvider as default } from './catalogModuleBitbucketServerEntityProvider'; diff --git a/plugins/catalog-backend-module-bitbucket/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket/CHANGELOG.md index 5b1e75e86d..ad6f296fbf 100644 --- a/plugins/catalog-backend-module-bitbucket/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket/CHANGELOG.md @@ -1,5 +1,69 @@ # @backstage/plugin-catalog-backend-module-bitbucket +## 0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.12-next.2 + +## 0.2.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.12-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.11-next.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## 0.2.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.9 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/integration@1.6.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.2.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + ## 0.2.15-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket/catalog-info.yaml b/plugins/catalog-backend-module-bitbucket/catalog-info.yaml new file mode 100644 index 0000000000..a384181766 --- /dev/null +++ b/plugins/catalog-backend-module-bitbucket/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-bitbucket + title: '@backstage/plugin-catalog-backend-module-bitbucket' + description: A Backstage catalog backend module that helps integrate towards Bitbucket +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-bitbucket/package.json b/plugins/catalog-backend-module-bitbucket/package.json index 53d80eb89e..681db2dd42 100644 --- a/plugins/catalog-backend-module-bitbucket/package.json +++ b/plugins/catalog-backend-module-bitbucket/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket", - "version": "0.2.15-next.1", + "version": "0.2.18-next.2", "deprecated": true, "main": "src/index.ts", "types": "src/index.ts", diff --git a/plugins/catalog-backend-module-gcp/CHANGELOG.md b/plugins/catalog-backend-module-gcp/CHANGELOG.md index 7fd00d048d..3efe6cb7d5 100644 --- a/plugins/catalog-backend-module-gcp/CHANGELOG.md +++ b/plugins/catalog-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,68 @@ # @backstage/plugin-catalog-backend-module-gcp +## 0.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-kubernetes-common@0.6.6-next.1 + +## 0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.6-next.0 + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + +## 0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-kubernetes-common@0.6.5 + +## 0.1.0 + +### Minor Changes + +- 290eff6692aa: Added GCP catalog plugin with GKE provider + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/backend-tasks@0.5.5 + - @backstage/config@1.0.8 + - @backstage/plugin-kubernetes-common@0.6.5 + +## 0.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + ## 0.1.0-next.0 ### Minor Changes diff --git a/plugins/catalog-backend-module-gcp/api-report.md b/plugins/catalog-backend-module-gcp/api-report.md index 6f963ed51d..b20b9c8363 100644 --- a/plugins/catalog-backend-module-gcp/api-report.md +++ b/plugins/catalog-backend-module-gcp/api-report.md @@ -12,7 +12,8 @@ import { Logger } from 'winston'; import { SchedulerService } from '@backstage/backend-plugin-api'; // @public -export const catalogModuleGcpGkeEntityProvider: () => BackendFeature; +const catalogModuleGcpGkeEntityProvider: () => BackendFeature; +export default catalogModuleGcpGkeEntityProvider; // @public export class GkeEntityProvider implements EntityProvider { diff --git a/plugins/catalog-backend-module-gcp/catalog-info.yaml b/plugins/catalog-backend-module-gcp/catalog-info.yaml new file mode 100644 index 0000000000..2a8b83b3ee --- /dev/null +++ b/plugins/catalog-backend-module-gcp/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-gcp + title: '@backstage/plugin-catalog-backend-module-gcp' + description: A Backstage catalog backend module that helps integrate towards GCP +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-gcp/package.json b/plugins/catalog-backend-module-gcp/package.json index 7ddb7d7b97..067fc3cf9b 100644 --- a/plugins/catalog-backend-module-gcp/package.json +++ b/plugins/catalog-backend-module-gcp/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-gcp", "description": "A Backstage catalog backend module that helps integrate towards GCP", - "version": "0.1.0-next.0", + "version": "0.1.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-gcp/src/index.ts b/plugins/catalog-backend-module-gcp/src/index.ts index fb3984a04d..1666fde7e0 100644 --- a/plugins/catalog-backend-module-gcp/src/index.ts +++ b/plugins/catalog-backend-module-gcp/src/index.ts @@ -22,3 +22,4 @@ export * from './providers'; export * from './module'; +export { default } from './module'; diff --git a/plugins/catalog-backend-module-gcp/src/module/index.ts b/plugins/catalog-backend-module-gcp/src/module/index.ts index c7cd310c97..b51790cbb8 100644 --- a/plugins/catalog-backend-module-gcp/src/module/index.ts +++ b/plugins/catalog-backend-module-gcp/src/module/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { catalogModuleGcpGkeEntityProvider } from './catalogModuleGcpGkeEntityProvider'; +export { catalogModuleGcpGkeEntityProvider as default } from './catalogModuleGcpGkeEntityProvider'; diff --git a/plugins/catalog-backend-module-gerrit/CHANGELOG.md b/plugins/catalog-backend-module-gerrit/CHANGELOG.md index 3f7d7a8dd3..0bceb9560e 100644 --- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,73 @@ # @backstage/plugin-catalog-backend-module-gerrit +## 0.1.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + +## 0.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/errors@1.2.1 + +## 0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## 0.1.16 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## 0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + ## 0.1.16-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gerrit/alpha-api-report.md b/plugins/catalog-backend-module-gerrit/alpha-api-report.md index 5aee61f28a..dd6c61c5ab 100644 --- a/plugins/catalog-backend-module-gerrit/alpha-api-report.md +++ b/plugins/catalog-backend-module-gerrit/alpha-api-report.md @@ -6,7 +6,8 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; // @alpha (undocumented) -export const catalogModuleGerritEntityProvider: () => BackendFeature; +const catalogModuleGerritEntityProvider: () => BackendFeature; +export default catalogModuleGerritEntityProvider; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog-backend-module-gerrit/catalog-info.yaml b/plugins/catalog-backend-module-gerrit/catalog-info.yaml new file mode 100644 index 0000000000..9f75a840aa --- /dev/null +++ b/plugins/catalog-backend-module-gerrit/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-gerrit + title: '@backstage/plugin-catalog-backend-module-gerrit' +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index f9566da0e2..e37b8f8549 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.16-next.1", + "version": "0.1.19-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-gerrit/src/alpha.ts b/plugins/catalog-backend-module-gerrit/src/alpha.ts index c277a0a680..01a9b25f7c 100644 --- a/plugins/catalog-backend-module-gerrit/src/alpha.ts +++ b/plugins/catalog-backend-module-gerrit/src/alpha.ts @@ -15,3 +15,4 @@ */ export * from './module'; +export { default } from './module'; 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 464837124f..4afcd8e2d4 100644 --- a/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts @@ -14,14 +14,8 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { coreServices } from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; -import { startTestBackend } from '@backstage/backend-test-utils'; -import { ConfigReader } from '@backstage/config'; +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'; @@ -38,14 +32,14 @@ describe('catalogModuleGerritEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); - const config = new ConfigReader({ + const config = { catalog: { providers: { gerrit: { @@ -70,16 +64,16 @@ describe('catalogModuleGerritEntityProvider', () => { }, ], }, - }); + }; await startTestBackend({ extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], - services: [ - [coreServices.rootConfig, config], - [coreServices.logger, getVoidLogger()], - [coreServices.scheduler, scheduler], + features: [ + catalogModuleGerritEntityProvider(), + mockServices.rootConfig.factory({ data: config }), + mockServices.logger.factory(), + scheduler.factory, ], - features: [catalogModuleGerritEntityProvider()], }); expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); diff --git a/plugins/catalog-backend-module-gerrit/src/module/index.ts b/plugins/catalog-backend-module-gerrit/src/module/index.ts index 95760538e7..10dd6b4475 100644 --- a/plugins/catalog-backend-module-gerrit/src/module/index.ts +++ b/plugins/catalog-backend-module-gerrit/src/module/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { catalogModuleGerritEntityProvider } from './catalogModuleGerritEntityProvider'; +export { catalogModuleGerritEntityProvider as default } from './catalogModuleGerritEntityProvider'; diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md index af93dd5419..766f129b57 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,98 @@ # @backstage/plugin-catalog-backend-module-github +## 0.3.8-next.2 + +### Patch Changes + +- 96353bb7cb4a: Properly support custom `userTransformer` returning `undefined` in `GithubMultiOrgEntityProvider` +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-events-node@0.2.12-next.2 + +## 0.3.8-next.1 + +### Patch Changes + +- 3c44761b9191: Allow github user and team transforms to return any Entity +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-events-node@0.2.12-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + +## 0.3.3 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- 81c231c9c9ee: Fixed a bug where the visibility filter was case sensitive and casting was inconsistent. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-catalog-backend@1.12.0 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/plugin-events-node@0.2.9 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + +## 0.3.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.0-next.2 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-events-node@0.2.9-next.2 + ## 0.3.3-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-github/alpha-api-report.md b/plugins/catalog-backend-module-github/alpha-api-report.md index 0d4e16b4ea..3bf72c449b 100644 --- a/plugins/catalog-backend-module-github/alpha-api-report.md +++ b/plugins/catalog-backend-module-github/alpha-api-report.md @@ -4,9 +4,25 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; +import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import { TeamTransformer } from '@backstage/plugin-catalog-backend-module-github'; +import { UserTransformer } from '@backstage/plugin-catalog-backend-module-github'; // @alpha -export const catalogModuleGithubEntityProvider: () => BackendFeature; +const catalogModuleGithubEntityProvider: () => BackendFeature; +export default catalogModuleGithubEntityProvider; + +// @alpha +export const catalogModuleGithubOrgEntityProvider: () => BackendFeature; + +// @alpha +export interface GithubOrgEntityProviderTransformsExtensionPoint { + setTeamTransformer(transformer: TeamTransformer): void; + setUserTransformer(transformer: UserTransformer): void; +} + +// @alpha +export const githubOrgEntityProviderTransformsExtensionPoint: ExtensionPoint; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog-backend-module-github/api-report.md b/plugins/catalog-backend-module-github/api-report.md index 7b944b33a9..12ab6d35e7 100644 --- a/plugins/catalog-backend-module-github/api-report.md +++ b/plugins/catalog-backend-module-github/api-report.md @@ -16,7 +16,6 @@ import { EventSubscriber } from '@backstage/plugin-events-node'; import { GithubCredentialsProvider } from '@backstage/integration'; import { GithubIntegrationConfig } from '@backstage/integration'; import { graphql } from '@octokit/graphql'; -import { GroupEntity } from '@backstage/catalog-model'; import { LocationSpec } from '@backstage/plugin-catalog-node'; import { Logger } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -25,7 +24,6 @@ import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmLocationAnalyzer } from '@backstage/plugin-catalog-backend'; import { TaskRunner } from '@backstage/backend-tasks'; import { TokenManager } from '@backstage/backend-common'; -import { UserEntity } from '@backstage/catalog-model'; // @public export const defaultOrganizationTeamTransformer: TeamTransformer; @@ -303,7 +301,7 @@ export type GithubUser = { export type TeamTransformer = ( item: GithubTeam, ctx: TransformerContext, -) => Promise; +) => Promise; // @public export interface TransformerContext { @@ -319,5 +317,5 @@ export interface TransformerContext { export type UserTransformer = ( item: GithubUser, ctx: TransformerContext, -) => Promise; +) => Promise; ``` diff --git a/plugins/catalog-backend-module-github/catalog-info.yaml b/plugins/catalog-backend-module-github/catalog-info.yaml new file mode 100644 index 0000000000..2b1b9a9553 --- /dev/null +++ b/plugins/catalog-backend-module-github/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-github + title: '@backstage/plugin-catalog-backend-module-github' + description: A Backstage catalog backend module that helps integrate towards GitHub +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-github/config.d.ts b/plugins/catalog-backend-module-github/config.d.ts index 9755504ee2..ad4d2d6ec5 100644 --- a/plugins/catalog-backend-module-github/config.d.ts +++ b/plugins/catalog-backend-module-github/config.d.ts @@ -183,6 +183,69 @@ export interface Config { schedule?: TaskScheduleDefinitionConfig; }; }; + + /** + * Configuration for catalogModuleGithubOrgEntityProvider + */ + githubOrg?: + | { + /** + * A stable id for this provider. Entities from this provider will + * be associated with this ID, so you should take care not to change + * it over time since that may lead to orphaned entities and/or + * conflicts. + * + * @example "ghe" + */ + id: string; + + /** + * The target that this provider should consume. + * + * @example "https://mycompany.github.com" + */ + githubUrl: string; + + /** + * The list of the GitHub orgs to consume. By default will consume all accessible + * orgs on the given GitHub instance (support for GitHub App integration only). + */ + orgs?: string[]; + + /** + * The refresh schedule to use. + */ + schedule: TaskScheduleDefinitionConfig; + } + | Array<{ + /** + * A stable id for this provider. Entities from this provider will + * be associated with this ID, so you should take care not to change + * it over time since that may lead to orphaned entities and/or + * conflicts. + * + * @example "ghe" + */ + id: string; + + /** + * The target that this provider should consume. + * + * @example "https://mycompany.github.com" + */ + githubUrl: string; + + /** + * The list of the GitHub orgs to consume. By default will consume all accessible + * orgs on the given GitHub instance (support for GitHub App integration only). + */ + orgs?: string[]; + + /** + * The refresh schedule to use. + */ + schedule: TaskScheduleDefinitionConfig; + }>; }; }; } diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index b05630862c..cfab77b141 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-github", "description": "A Backstage catalog backend module that helps integrate towards GitHub", - "version": "0.3.3-next.1", + "version": "0.3.8-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-github/src/alpha.ts b/plugins/catalog-backend-module-github/src/alpha.ts index c277a0a680..01a9b25f7c 100644 --- a/plugins/catalog-backend-module-github/src/alpha.ts +++ b/plugins/catalog-backend-module-github/src/alpha.ts @@ -15,3 +15,4 @@ */ export * from './module'; +export { default } from './module'; diff --git a/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts index 7669fd5b47..71613d88d2 100644 --- a/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts +++ b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import { Entity, GroupEntity, UserEntity } from '@backstage/catalog-model'; import { graphql } from '@octokit/graphql'; import { ANNOTATION_GITHUB_TEAM_SLUG, @@ -34,24 +34,24 @@ export interface TransformerContext { } /** - * Transformer for GitHub users to UserEntity + * Transformer for GitHub users to an Entity * * @public */ export type UserTransformer = ( item: GithubUser, ctx: TransformerContext, -) => Promise; +) => Promise; /** - * Transformer for GitHub Team to GroupEntity + * Transformer for GitHub Team to an Entity * * @public */ export type TeamTransformer = ( item: GithubTeam, ctx: TransformerContext, -) => Promise; +) => Promise; /** * Default transformer for GitHub users to UserEntity 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 7a0a29e098..d83e8031cc 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.test.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.test.ts @@ -229,7 +229,7 @@ describe('github', () => { it('reads teams', async () => { const output = { - groups: [ + teams: [ expect.objectContaining({ metadata: expect.objectContaining({ name: 'team', @@ -329,7 +329,7 @@ describe('github', () => { it('reads teams', async () => { const output = { - groups: [ + teams: [ expect.objectContaining({ metadata: expect.objectContaining({ name: 'Team-custom', @@ -409,7 +409,7 @@ describe('github', () => { }; const output = { - groups: [ + teams: [ expect.objectContaining({ metadata: expect.objectContaining({ name: 'Team-custom', @@ -444,7 +444,7 @@ describe('github', () => { customTeamTransformer, ); - expect(teams.groups).toHaveLength(1); + expect(teams.teams).toHaveLength(1); expect(teams).toEqual(output); }); }); diff --git a/plugins/catalog-backend-module-github/src/lib/github.ts b/plugins/catalog-backend-module-github/src/lib/github.ts index 412b751c38..6b490f363a 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, GroupEntity, UserEntity } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { GithubCredentialType } from '@backstage/integration'; import { graphql } from '@octokit/graphql'; import { @@ -139,7 +139,7 @@ export async function getOrganizationUsers( org: string, tokenType: GithubCredentialType, userTransformer: UserTransformer = defaultUserTransformer, -): Promise<{ users: UserEntity[] }> { +): Promise<{ users: Entity[] }> { const query = ` query users($org: String!, $email: Boolean!, $cursor: String) { organization(login: $org) { @@ -188,7 +188,7 @@ export async function getOrganizationTeams( org: string, teamTransformer: TeamTransformer = defaultOrganizationTeamTransformer, ): Promise<{ - groups: GroupEntity[]; + teams: Entity[]; }> { const query = ` query teams($org: String!, $cursor: String) { @@ -222,7 +222,7 @@ export async function getOrganizationTeams( const materialisedTeams = async ( item: GithubTeamResponse, ctx: TransformerContext, - ): Promise => { + ): Promise => { const memberNames: GithubUser[] = []; if (!item.members.pageInfo.hasNextPage) { @@ -247,7 +247,7 @@ export async function getOrganizationTeams( return await teamTransformer(team, ctx); }; - const groups = await queryWithPaging( + const teams = await queryWithPaging( client, query, org, @@ -256,7 +256,7 @@ export async function getOrganizationTeams( { org }, ); - return { groups }; + return { teams }; } export async function getOrganizationTeamsFromUsers( @@ -265,7 +265,7 @@ export async function getOrganizationTeamsFromUsers( userLogins: string[], teamTransformer: TeamTransformer = defaultOrganizationTeamTransformer, ): Promise<{ - groups: GroupEntity[]; + teams: Entity[]; }> { const query = ` query teams($org: String!, $cursor: String, $userLogins: [String!] = "") { @@ -306,7 +306,7 @@ export async function getOrganizationTeamsFromUsers( const materialisedTeams = async ( item: GithubTeamResponse, ctx: TransformerContext, - ): Promise => { + ): Promise => { const memberNames: GithubUser[] = []; if (!item.members.pageInfo.hasNextPage) { @@ -331,7 +331,7 @@ export async function getOrganizationTeamsFromUsers( return await teamTransformer(team, ctx); }; - const groups = await queryWithPaging( + const teams = await queryWithPaging( client, query, org, @@ -340,7 +340,7 @@ export async function getOrganizationTeamsFromUsers( { org, userLogins }, ); - return { groups }; + return { teams }; } export async function getOrganizationsFromUser( @@ -377,7 +377,7 @@ export async function getOrganizationTeam( teamSlug: string, teamTransformer: TeamTransformer = defaultOrganizationTeamTransformer, ): Promise<{ - group: GroupEntity; + team: Entity; }> { const query = ` query teams($org: String!, $teamSlug: String!) { @@ -401,7 +401,7 @@ export async function getOrganizationTeam( const materialisedTeam = async ( item: GithubTeamResponse, ctx: TransformerContext, - ): Promise => { + ): Promise => { const memberNames: GithubUser[] = []; if (!item.members.pageInfo.hasNextPage) { @@ -432,17 +432,17 @@ export async function getOrganizationTeam( }); if (!response.organization?.team) - throw new Error(`Found no match for group ${teamSlug}`); + throw new Error(`Found no match for team ${teamSlug}`); - const group = await materialisedTeam(response.organization?.team, { + const team = await materialisedTeam(response.organization?.team, { query, client, org, }); - if (!group) throw new Error(`Can't transform for group ${teamSlug}`); + if (!team) throw new Error(`Can't transform for team ${teamSlug}`); - return { group }; + return { team }; } export async function getOrganizationRepositories( diff --git a/plugins/catalog-backend-module-github/src/lib/guards.ts b/plugins/catalog-backend-module-github/src/lib/guards.ts new file mode 100644 index 0000000000..f092d2365a --- /dev/null +++ b/plugins/catalog-backend-module-github/src/lib/guards.ts @@ -0,0 +1,32 @@ +/* + * 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 { + Entity, + GroupEntity, + UserEntity, + isGroupEntity, + isUserEntity, +} from '@backstage/catalog-model'; + +export function areGroupEntities( + entities: Entity[], +): entities is GroupEntity[] { + return entities.every(e => isGroupEntity(e)); +} + +export function areUserEntities(entities: Entity[]): entities is UserEntity[] { + return entities.every(e => isUserEntity(e)); +} diff --git a/plugins/catalog-backend-module-github/src/lib/util.ts b/plugins/catalog-backend-module-github/src/lib/util.ts index bb91e9dd2a..58ae8d6551 100644 --- a/plugins/catalog-backend-module-github/src/lib/util.ts +++ b/plugins/catalog-backend-module-github/src/lib/util.ts @@ -107,5 +107,10 @@ export function satisfiesVisibilityFilter( if (!visibilities.length) { return true; } - return visibilities.includes(visibility); + const lowerCaseVisibilities = visibilities.map(v => + v.toLocaleLowerCase('en-US'), + ); + const lowerCaseVisibility = visibility.toLocaleLowerCase('en-US'); + + return lowerCaseVisibilities.includes(lowerCaseVisibility); } diff --git a/plugins/catalog-backend-module-github/src/module/catalogModuleGithubEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/module/catalogModuleGithubEntityProvider.test.ts index ca82e58e26..cb38be1a5d 100644 --- a/plugins/catalog-backend-module-github/src/module/catalogModuleGithubEntityProvider.test.ts +++ b/plugins/catalog-backend-module-github/src/module/catalogModuleGithubEntityProvider.test.ts @@ -14,22 +14,16 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { coreServices } from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; -import { startTestBackend } from '@backstage/backend-test-utils'; -import { ConfigReader } from '@backstage/config'; +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 { catalogModuleGithubEntityProvider } from './catalogModuleGithubEntityProvider'; -import { GithubEntityProvider } from '../providers/GithubEntityProvider'; describe('catalogModuleGithubEntityProvider', () => { it('should register provider at the catalog extension point', async () => { - let addedProviders: Array | undefined; + let addedProviders: Array | undefined; let usedSchedule: TaskScheduleDefinition | undefined; const extensionPoint = { @@ -38,14 +32,14 @@ describe('catalogModuleGithubEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); - const config = new ConfigReader({ + const config = { catalog: { providers: { github: { @@ -57,16 +51,15 @@ describe('catalogModuleGithubEntityProvider', () => { }, }, }, - }); + }; await startTestBackend({ extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], - services: [ - [coreServices.rootConfig, config], - [coreServices.logger, getVoidLogger()], - [coreServices.scheduler, scheduler], + features: [ + catalogModuleGithubEntityProvider(), + mockServices.rootConfig.factory({ data: config }), + scheduler.factory, ], - features: [catalogModuleGithubEntityProvider()], }); expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); diff --git a/plugins/catalog-backend-module-github/src/module/catalogModuleGithubOrgEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/module/catalogModuleGithubOrgEntityProvider.test.ts new file mode 100644 index 0000000000..1b8b115a96 --- /dev/null +++ b/plugins/catalog-backend-module-github/src/module/catalogModuleGithubOrgEntityProvider.test.ts @@ -0,0 +1,77 @@ +/* + * 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 { 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 './catalogModuleGithubOrgEntityProvider'; + +describe('catalogModuleGithubOrgEntityProvider', () => { + it('should register provider at the catalog extension point', async () => { + let addedProviders: Array | undefined; + let usedSchedule: TaskScheduleDefinition | undefined; + + const extensionPoint = { + addEntityProvider: (...providers: any) => { + addedProviders = providers; + }, + }; + const runner = jest.fn(); + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { + usedSchedule = schedule; + return { run: runner }; + }, + }); + + const config = { + catalog: { + providers: { + githubOrg: [ + { + id: 'default', + githubUrl: 'https://github.com', + orgs: ['backstage'], + schedule: { + frequency: 'P1M', + timeout: 'PT3M', + }, + }, + ], + }, + }, + }; + + await startTestBackend({ + extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], + features: [ + catalogModuleGithubOrgEntityProvider(), + mockServices.rootConfig.factory({ data: config }), + scheduler.factory, + ], + }); + + expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); + expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M')); + expect(addedProviders?.length).toEqual(1); + expect(addedProviders![0].getProviderName()).toEqual( + 'GithubMultiOrgEntityProvider:default', + ); + expect(runner).not.toHaveBeenCalled(); + }); +}); diff --git a/plugins/catalog-backend-module-github/src/module/catalogModuleGithubOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/module/catalogModuleGithubOrgEntityProvider.ts new file mode 100644 index 0000000000..4838be54e1 --- /dev/null +++ b/plugins/catalog-backend-module-github/src/module/catalogModuleGithubOrgEntityProvider.ts @@ -0,0 +1,144 @@ +/* + * 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 { loggerToWinstonLogger } from '@backstage/backend-common'; +import { + coreServices, + createBackendModule, + createExtensionPoint, +} from '@backstage/backend-plugin-api'; +import { + readTaskScheduleDefinitionFromConfig, + TaskScheduleDefinition, +} from '@backstage/backend-tasks'; +import { Config } from '@backstage/config'; +import { + GithubMultiOrgEntityProvider, + TeamTransformer, + UserTransformer, +} from '@backstage/plugin-catalog-backend-module-github'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; + +/** + * Interface for {@link githubOrgEntityProviderTransformsExtensionPoint}. + * + * @alpha + */ +export interface GithubOrgEntityProviderTransformsExtensionPoint { + /** + * Set a custom transformer for transforming from GitHub users to catalog + * entities. + */ + setUserTransformer(transformer: UserTransformer): void; + + /** + * Set a custom transformer for transforming from GitHub teams to catalog + * entities. + */ + setTeamTransformer(transformer: TeamTransformer): void; +} + +/** + * Extension point for runtime configuration of {@link catalogModuleGithubOrgEntityProvider}. + * + * @alpha + */ +export const githubOrgEntityProviderTransformsExtensionPoint = + createExtensionPoint({ + id: 'catalog.githubOrgEntityProvider', + }); + +/** + * Registers the `GithubMultiOrgEntityProvider` with the catalog processing extension point. + * + * @alpha + */ +export const catalogModuleGithubOrgEntityProvider = createBackendModule({ + pluginId: 'catalog', + moduleId: 'githubOrgEntityProvider', + register(env) { + let userTransformer: UserTransformer | undefined; + let teamTransformer: TeamTransformer | undefined; + + env.registerExtensionPoint( + githubOrgEntityProviderTransformsExtensionPoint, + { + setUserTransformer(transformer) { + if (userTransformer) { + throw new Error('User transformer may only be set once'); + } + userTransformer = transformer; + }, + setTeamTransformer(transformer) { + if (teamTransformer) { + throw new Error('Team transformer may only be set once'); + } + teamTransformer = transformer; + }, + }, + ); + + env.registerInit({ + deps: { + catalog: catalogProcessingExtensionPoint, + config: coreServices.rootConfig, + logger: coreServices.logger, + scheduler: coreServices.scheduler, + }, + async init({ catalog, config, logger, scheduler }) { + for (const definition of readDefinitionsFromConfig(config)) { + catalog.addEntityProvider( + GithubMultiOrgEntityProvider.fromConfig(config, { + id: definition.id, + githubUrl: definition.githubUrl, + orgs: definition.orgs, + schedule: scheduler.createScheduledTaskRunner( + definition.schedule, + ), + logger: loggerToWinstonLogger(logger), + userTransformer, + teamTransformer, + }), + ); + } + }, + }); + }, +}); + +function readDefinitionsFromConfig(rootConfig: Config): Array<{ + id: string; + githubUrl: string; + orgs?: string[]; + schedule: TaskScheduleDefinition; +}> { + const baseKey = 'catalog.providers.githubOrg'; + const baseConfig = rootConfig.getOptional(baseKey); + if (!baseConfig) { + return []; + } + + const configs = Array.isArray(baseConfig) + ? rootConfig.getConfigArray(baseKey) + : [rootConfig.getConfig(baseKey)]; + + return configs.map(c => ({ + id: c.getString('id'), + githubUrl: c.getString('githubUrl'), + orgs: c.getOptionalStringArray('orgs'), + schedule: readTaskScheduleDefinitionFromConfig(c.getConfig('schedule')), + })); +} diff --git a/plugins/catalog-backend-module-github/src/module/index.ts b/plugins/catalog-backend-module-github/src/module/index.ts index 640a633093..7e8a214ca3 100644 --- a/plugins/catalog-backend-module-github/src/module/index.ts +++ b/plugins/catalog-backend-module-github/src/module/index.ts @@ -14,4 +14,9 @@ * limitations under the License. */ -export { catalogModuleGithubEntityProvider } from './catalogModuleGithubEntityProvider'; +export { catalogModuleGithubEntityProvider as default } from './catalogModuleGithubEntityProvider'; +export { + catalogModuleGithubOrgEntityProvider, + githubOrgEntityProviderTransformsExtensionPoint, + type GithubOrgEntityProviderTransformsExtensionPoint, +} from './catalogModuleGithubOrgEntityProvider'; diff --git a/plugins/catalog-backend-module-github/src/processors/GithubMultiOrgReaderProcessor.ts b/plugins/catalog-backend-module-github/src/processors/GithubMultiOrgReaderProcessor.ts index cf5e484fc2..8bfbd20e2e 100644 --- a/plugins/catalog-backend-module-github/src/processors/GithubMultiOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-github/src/processors/GithubMultiOrgReaderProcessor.ts @@ -16,9 +16,9 @@ import { DEFAULT_NAMESPACE, - GroupEntity, + Entity, + isGroupEntity, stringifyEntityRef, - UserEntity, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { @@ -49,6 +49,7 @@ import { TeamTransformer, UserTransformer, } from '../lib'; +import { areGroupEntities, areUserEntities } from '../lib/guards'; /** * Extracts teams and users out of a multiple GitHub orgs namespaced per org. @@ -147,7 +148,7 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { client, orgConfig.name, tokenType, - async (githubUser, ctx): Promise => { + async (githubUser, ctx): Promise => { const result = this.options.userTransformer ? await this.options.userTransformer(githubUser, ctx) : await defaultUserTransformer(githubUser, ctx); @@ -160,15 +161,15 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { }, ); - const { groups } = await getOrganizationTeams( + const { teams } = await getOrganizationTeams( client, orgConfig.name, - async (team, ctx): Promise => { + async (team, ctx): Promise => { const result = this.options.teamTransformer ? await this.options.teamTransformer(team, ctx) : await defaultOrganizationTeamTransformer(team, ctx); - if (result) { + if (result && isGroupEntity(result)) { result.metadata.namespace = orgConfig.groupNamespace; // Group `spec.members` inherits the namespace of it's group so need to explicitly specify refs here result.spec.members = team.members.map( @@ -185,7 +186,7 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); this.logger.debug( - `Read ${users.length} GitHub users and ${groups.length} GitHub teams from ${orgConfig.name} in ${duration} seconds`, + `Read ${users.length} GitHub users and ${teams.length} GitHub teams from ${orgConfig.name} in ${duration} seconds`, ); // Grab current users from `allUsersMap` if they already exist in our @@ -199,11 +200,15 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { return allUsersMap.get(userRef); }); - assignGroupsToUsers(pendingUsers, groups); - buildOrgHierarchy(groups); + if (areGroupEntities(teams)) { + buildOrgHierarchy(teams); + if (areUserEntities(pendingUsers)) { + assignGroupsToUsers(pendingUsers, teams); + } + } - for (const group of groups) { - emit(processingResult.entity(location, group)); + for (const team of teams) { + emit(processingResult.entity(location, team)); } } catch (e) { this.logger.error( diff --git a/plugins/catalog-backend-module-github/src/processors/GithubOrgReaderProcessor.ts b/plugins/catalog-backend-module-github/src/processors/GithubOrgReaderProcessor.ts index 654deb3079..63a273378b 100644 --- a/plugins/catalog-backend-module-github/src/processors/GithubOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-github/src/processors/GithubOrgReaderProcessor.ts @@ -37,6 +37,7 @@ import { getOrganizationUsers, parseGithubOrgUrl, } from '../lib'; +import { areGroupEntities, areUserEntities } from '../lib/guards'; type GraphQL = typeof graphql; @@ -101,19 +102,23 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { this.logger.info('Reading GitHub users and groups'); const { users } = await getOrganizationUsers(client, org, tokenType); - const { groups } = await getOrganizationTeams(client, org); + const { teams } = await getOrganizationTeams(client, org); const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); this.logger.debug( - `Read ${users.length} GitHub users and ${groups.length} GitHub groups in ${duration} seconds`, + `Read ${users.length} GitHub users and ${teams.length} GitHub teams in ${duration} seconds`, ); - assignGroupsToUsers(users, groups); - buildOrgHierarchy(groups); + if (areGroupEntities(teams)) { + buildOrgHierarchy(teams); + if (areUserEntities(users)) { + assignGroupsToUsers(users, teams); + } + } // Done! - for (const group of groups) { - emit(processingResult.entity(location, group)); + for (const team of teams) { + emit(processingResult.entity(location, team)); } for (const user of users) { emit(processingResult.entity(location, user)); diff --git a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.test.ts index 2622b2483d..504e5772dd 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.test.ts @@ -1699,31 +1699,6 @@ describe('GithubMultiOrgEntityProvider', () => { expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ type: 'delta', added: [ - { - entity: { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - annotations: { - 'backstage.io/managed-by-location': - 'url:https://github.com/a', - 'backstage.io/managed-by-origin-location': - 'url:https://github.com/a', - 'github.com/user-login': 'a', - }, - name: 'a', - }, - spec: { - memberOf: ['orga/team', 'orgb/team'], - profile: { - displayName: 'a', - email: 'user1@test.com', - picture: 'https://avatars.githubusercontent.com/u/83820368', - }, - }, - }, - locationKey: 'github-multi-org-provider:my-id', - }, { entity: { apiVersion: 'backstage.io/v1alpha1', @@ -1754,6 +1729,31 @@ describe('GithubMultiOrgEntityProvider', () => { }, locationKey: 'github-multi-org-provider:my-id', }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://github.com/a', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/a', + 'github.com/user-login': 'a', + }, + name: 'a', + }, + spec: { + memberOf: ['orga/team', 'orgb/team'], + profile: { + displayName: 'a', + email: 'user1@test.com', + picture: 'https://avatars.githubusercontent.com/u/83820368', + }, + }, + }, + locationKey: 'github-multi-org-provider:my-id', + }, ], removed: [], }); @@ -1871,31 +1871,6 @@ describe('GithubMultiOrgEntityProvider', () => { expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ type: 'delta', added: [ - { - entity: { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - annotations: { - 'backstage.io/managed-by-location': - 'url:https://github.com/a', - 'backstage.io/managed-by-origin-location': - 'url:https://github.com/a', - 'github.com/user-login': 'a', - }, - name: 'a', - }, - spec: { - memberOf: ['orgb/team'], - profile: { - displayName: 'a', - email: 'user1@test.com', - picture: 'https://avatars.githubusercontent.com/u/83820368', - }, - }, - }, - locationKey: 'github-multi-org-provider:my-id', - }, { entity: { apiVersion: 'backstage.io/v1alpha1', @@ -1926,6 +1901,31 @@ describe('GithubMultiOrgEntityProvider', () => { }, locationKey: 'github-multi-org-provider:my-id', }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://github.com/a', + 'backstage.io/managed-by-origin-location': + 'url:https://github.com/a', + 'github.com/user-login': 'a', + }, + name: 'a', + }, + spec: { + memberOf: ['orgb/team'], + profile: { + displayName: 'a', + email: 'user1@test.com', + picture: 'https://avatars.githubusercontent.com/u/83820368', + }, + }, + }, + locationKey: 'github-multi-org-provider:my-id', + }, ], removed: [], }); diff --git a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts index 4d343493f6..7e15c4ce68 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts @@ -20,10 +20,10 @@ import { ANNOTATION_ORIGIN_LOCATION, DEFAULT_NAMESPACE, Entity, - GroupEntity, + isGroupEntity, + isUserEntity, parseEntityRef, stringifyEntityRef, - UserEntity, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { @@ -78,6 +78,7 @@ import { getOrganizationTeamsFromUsers, } from '../lib/github'; import { splitTeamSlug } from '../lib/util'; +import { areGroupEntities, areUserEntities } from '../lib/guards'; /** * Options for {@link GithubMultiOrgEntityProvider}. @@ -240,7 +241,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { const { markReadComplete } = trackProgress(logger); const allUsersMap = new Map(); - const allGroups: Entity[] = []; + const allTeams: Entity[] = []; const orgsToProcess = this.options.orgs?.length ? this.options.orgs @@ -266,7 +267,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { this.options.userTransformer, ); - const { groups } = await getOrganizationTeams( + const { teams } = await getOrganizationTeams( client, org, this.defaultMultiOrgTeamTransformer.bind(this), @@ -283,10 +284,14 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { return allUsersMap.get(userRef); }); - assignGroupsToUsers(pendingUsers, groups); - buildOrgHierarchy(groups); + if (areGroupEntities(teams)) { + buildOrgHierarchy(teams); + if (areUserEntities(pendingUsers)) { + assignGroupsToUsers(pendingUsers, teams); + } + } - allGroups.push(...groups); + allTeams.push(...teams); } catch (e) { logger.error(`Failed to read GitHub org data for ${org}: ${e}`); } @@ -294,11 +299,11 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { const allUsers = Array.from(allUsersMap.values()); - const { markCommitComplete } = markReadComplete({ allUsers, allGroups }); + const { markCommitComplete } = markReadComplete({ allUsers, allTeams }); await this.connection.applyMutation({ type: 'full', - entities: [...allUsers, ...allGroups].map(entity => ({ + entities: [...allUsers, ...allTeams].map(entity => ({ locationKey: `github-multi-org-provider:${this.options.id}`, entity: withLocations( `https://${this.options.gitHubConfig.host}`, @@ -422,41 +427,45 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { this.options.userTransformer, ); - const { groups } = await getOrganizationTeams( + const { teams } = await getOrganizationTeams( client, org, this.defaultMultiOrgTeamTransformer.bind(this), ); - // Fetch group memberships of users in case they already exist and - // have memberships in groups from other applicable orgs - for (const userOrg of applicableOrgs) { - const { headers: orgHeaders } = - await this.options.githubCredentialsProvider.getCredentials({ - url: `${this.options.githubUrl}/${userOrg}`, + if (users.length) { + // Fetch group memberships of users in case they already exist and + // have memberships in groups from other applicable orgs + for (const userOrg of applicableOrgs) { + const { headers: orgHeaders } = + await this.options.githubCredentialsProvider.getCredentials({ + url: `${this.options.githubUrl}/${userOrg}`, + }); + const orgClient = graphql.defaults({ + baseUrl: this.options.gitHubConfig.apiBaseUrl, + headers: orgHeaders, }); - const orgClient = graphql.defaults({ - baseUrl: this.options.gitHubConfig.apiBaseUrl, - headers: orgHeaders, - }); - const { groups: userGroups } = await getOrganizationTeamsFromUsers( - orgClient, - userOrg, - users.map( - u => - u.metadata.annotations?.[ANNOTATION_GITHUB_USER_LOGIN] || - u.metadata.name, - ), - this.defaultMultiOrgTeamTransformer.bind(this), - ); + const { teams: userTeams } = await getOrganizationTeamsFromUsers( + orgClient, + userOrg, + users.map( + u => + u.metadata.annotations?.[ANNOTATION_GITHUB_USER_LOGIN] || + u.metadata.name, + ), + this.defaultMultiOrgTeamTransformer.bind(this), + ); - assignGroupsToUsers(users, userGroups); + if (areGroupEntities(userTeams) && areUserEntities(users)) { + assignGroupsToUsers(users, userTeams); + } + } } const { added, removed } = this.createAddEntitiesOperation([ ...users, - ...groups, + ...teams, ]); await this.connection.applyMutation({ type: 'delta', @@ -511,7 +520,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { updateMemberships = true; } - const user = (await userTransformer( + const user = await userTransformer( { name, avatarUrl, @@ -523,7 +532,11 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { client, query: '', }, - )) as UserEntity; + ); + + if (!user) { + return; + } if (updateMemberships) { for (const userOrg of userApplicableOrgs) { @@ -536,14 +549,16 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { headers: orgHeaders, }); - const { groups } = await getOrganizationTeamsFromUsers( + const { teams } = await getOrganizationTeamsFromUsers( orgClient, userOrg, [login], this.defaultMultiOrgTeamTransformer.bind(this), ); - assignGroupsToUsers([user], groups); + if (isUserEntity(user) && areGroupEntities(teams)) { + assignGroupsToUsers([user], teams); + } } } @@ -623,7 +638,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { }); const teamSlug = event.team.slug; - const { group } = await getOrganizationTeam( + const { team } = await getOrganizationTeam( client, org, teamSlug, @@ -637,37 +652,42 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { this.options.userTransformer, ); - const usersFromChangedGroup = - group.spec.members?.map(m => - stringifyEntityRef(parseEntityRef(m, { defaultKind: 'user' })), - ) || []; + const usersFromChangedGroup = isGroupEntity(team) + ? team.spec.members?.map(m => + stringifyEntityRef(parseEntityRef(m, { defaultKind: 'user' })), + ) || [] + : []; const usersToRebuild = users.filter(u => usersFromChangedGroup.includes(stringifyEntityRef(u)), ); - // Update memberships of associated members of this group in case the group entity ref changed - for (const userOrg of applicableOrgs) { - const { headers: orgHeaders } = - await this.options.githubCredentialsProvider.getCredentials({ - url: `${this.options.githubUrl}/${userOrg}`, + if (usersToRebuild.length) { + // Update memberships of associated members of this group in case the group entity ref changed + for (const userOrg of applicableOrgs) { + const { headers: orgHeaders } = + await this.options.githubCredentialsProvider.getCredentials({ + url: `${this.options.githubUrl}/${userOrg}`, + }); + const orgClient = graphql.defaults({ + baseUrl: this.options.gitHubConfig.apiBaseUrl, + headers: orgHeaders, }); - const orgClient = graphql.defaults({ - baseUrl: this.options.gitHubConfig.apiBaseUrl, - headers: orgHeaders, - }); - const { groups } = await getOrganizationTeamsFromUsers( - orgClient, - userOrg, - usersToRebuild.map( - u => - u.metadata.annotations?.[ANNOTATION_GITHUB_USER_LOGIN] || - u.metadata.name, - ), - this.defaultMultiOrgTeamTransformer.bind(this), - ); + const { teams } = await getOrganizationTeamsFromUsers( + orgClient, + userOrg, + usersToRebuild.map( + u => + u.metadata.annotations?.[ANNOTATION_GITHUB_USER_LOGIN] || + u.metadata.name, + ), + this.defaultMultiOrgTeamTransformer.bind(this), + ); - assignGroupsToUsers(usersToRebuild, groups); + if (areGroupEntities(teams) && areUserEntities(usersToRebuild)) { + assignGroupsToUsers(usersToRebuild, teams); + } + } } const oldName = event.changes.name?.from || ''; @@ -693,7 +713,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { const { removed } = this.createRemoveEntitiesOperation([oldGroup]); const { added } = this.createAddEntitiesOperation([ ...usersToRebuild, - group, + team, ]); await this.connection.applyMutation({ type: 'delta', @@ -729,7 +749,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { }); const teamSlug = event.team.slug; - const { group } = await getOrganizationTeam( + const { team } = await getOrganizationTeam( client, org, teamSlug, @@ -739,7 +759,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { const userTransformer = this.options.userTransformer || defaultUserTransformer; const { name, avatar_url: avatarUrl, email, login } = event.member; - const user = (await userTransformer( + const user = await userTransformer( { name, avatarUrl, @@ -751,31 +771,40 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { client, query: '', }, - )) as UserEntity; + ); - const { orgs } = await getOrganizationsFromUser(client, login); - const userApplicableOrgs = orgs.filter(o => applicableOrgs.includes(o)); - for (const userOrg of userApplicableOrgs) { - const { headers: orgHeaders } = - await this.options.githubCredentialsProvider.getCredentials({ - url: `${this.options.githubUrl}/${userOrg}`, + const mutationEntities = [team]; + + if (user && isUserEntity(user)) { + const { orgs } = await getOrganizationsFromUser(client, login); + const userApplicableOrgs = orgs.filter(o => applicableOrgs.includes(o)); + for (const userOrg of userApplicableOrgs) { + const { headers: orgHeaders } = + await this.options.githubCredentialsProvider.getCredentials({ + url: `${this.options.githubUrl}/${userOrg}`, + }); + const orgClient = graphql.defaults({ + baseUrl: this.options.gitHubConfig.apiBaseUrl, + headers: orgHeaders, }); - const orgClient = graphql.defaults({ - baseUrl: this.options.gitHubConfig.apiBaseUrl, - headers: orgHeaders, - }); - const { groups } = await getOrganizationTeamsFromUsers( - orgClient, - userOrg, - [login], - this.defaultMultiOrgTeamTransformer.bind(this), - ); + const { teams } = await getOrganizationTeamsFromUsers( + orgClient, + userOrg, + [login], + this.defaultMultiOrgTeamTransformer.bind(this), + ); - assignGroupsToUsers([user], groups); + if (areGroupEntities(teams)) { + assignGroupsToUsers([user], teams); + } + } + + mutationEntities.push(user); } - const { added, removed } = this.createAddEntitiesOperation([user, group]); + const { added, removed } = + this.createAddEntitiesOperation(mutationEntities); await this.connection.applyMutation({ type: 'delta', removed, @@ -812,14 +841,14 @@ export class GithubMultiOrgEntityProvider implements EntityProvider { private async defaultMultiOrgTeamTransformer( team: GithubTeam, ctx: TransformerContext, - ): Promise { + ): Promise { if (this.options.teamTransformer) { return await this.options.teamTransformer(team, ctx); } const result = await defaultOrganizationTeamTransformer(team, ctx); - if (result) { + if (result && result.spec) { result.metadata.namespace = ctx.org.toLocaleLowerCase('en-US'); // Group `spec.members` inherits the namespace of it's group so need to explicitly specify refs here result.spec.members = team.members.map( @@ -885,9 +914,9 @@ function trackProgress(logger: Logger) { function markReadComplete(read: { allUsers: unknown[]; - allGroups: unknown[]; + allTeams: unknown[]; }) { - summary = `${read.allUsers.length} GitHub users and ${read.allGroups.length} GitHub groups`; + summary = `${read.allUsers.length} GitHub users and ${read.allTeams.length} GitHub groups`; const readDuration = ((Date.now() - timestamp) / 1000).toFixed(1); timestamp = Date.now(); logger.info(`Read ${summary} in ${readDuration} seconds. Committing...`); diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts index bf82607c6b..22f1f12e8d 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts @@ -19,6 +19,7 @@ import { ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION, Entity, + isGroupEntity, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { @@ -68,6 +69,7 @@ import { } from '../lib/github'; import { assignGroupsToUsers, buildOrgHierarchy } from '../lib/org'; import { parseGithubOrgUrl, splitTeamSlug } from '../lib/util'; +import { areGroupEntities, areUserEntities } from '../lib/guards'; /** * Options for {@link GithubOrgEntityProvider}. @@ -223,20 +225,24 @@ export class GithubOrgEntityProvider tokenType, this.options.userTransformer, ); - const { groups } = await getOrganizationTeams( + const { teams } = await getOrganizationTeams( client, org, this.options.teamTransformer, ); - assignGroupsToUsers(users, groups); - buildOrgHierarchy(groups); + if (areGroupEntities(teams)) { + buildOrgHierarchy(teams); + if (areUserEntities(users)) { + assignGroupsToUsers(users, teams); + } + } - const { markCommitComplete } = markReadComplete({ users, groups }); + const { markCommitComplete } = markReadComplete({ users, teams }); await this.connection.applyMutation({ type: 'full', - entities: [...users, ...groups].map(entity => ({ + entities: [...users, ...teams].map(entity => ({ locationKey: `github-org-provider:${this.options.id}`, entity: withLocations( `https://${this.options.gitHubConfig.host}`, @@ -340,7 +346,7 @@ export class GithubOrgEntityProvider }); const { org } = parseGithubOrgUrl(this.options.orgUrl); - const { group } = await getOrganizationTeam( + const { team } = await getOrganizationTeam( client, org, teamSlug, @@ -354,20 +360,28 @@ export class GithubOrgEntityProvider this.options.userTransformer, ); - const usersFromChangedGroup = group.spec.members || []; + if (!isGroupEntity(team)) { + return; + } + + const usersFromChangedGroup = team.spec.members || []; const usersToRebuild = users.filter(u => usersFromChangedGroup.includes(u.metadata.name), ); - const { groups } = await getOrganizationTeamsFromUsers( + const { teams } = await getOrganizationTeamsFromUsers( client, org, usersToRebuild.map(u => u.metadata.name), this.options.teamTransformer, ); - assignGroupsToUsers(usersToRebuild, groups); - buildOrgHierarchy(groups); + if (areGroupEntities(teams)) { + buildOrgHierarchy(teams); + if (areUserEntities(usersToRebuild)) { + assignGroupsToUsers(usersToRebuild, teams); + } + } const oldName = event.changes.name?.from || event.team.name; const oldSlug = oldName.toLowerCase().replaceAll(/\s/gi, '-'); @@ -380,14 +394,14 @@ export class GithubOrgEntityProvider const { removed } = createDeltaOperation(org, [ { - ...group, + ...team, metadata: { name: oldSlug, description: oldDescriptionSlug, }, }, ]); - const { added } = createDeltaOperation(org, [...usersToRebuild, ...groups]); + const { added } = createDeltaOperation(org, [...usersToRebuild, ...teams]); await this.connection.applyMutation({ type: 'delta', removed, @@ -423,7 +437,7 @@ export class GithubOrgEntityProvider }); const { org } = parseGithubOrgUrl(this.options.orgUrl); - const { group } = await getOrganizationTeam( + const { team } = await getOrganizationTeam( client, org, teamSlug, @@ -439,7 +453,7 @@ export class GithubOrgEntityProvider const usersToRebuild = users.filter(u => u.metadata.name === userLogin); - const { groups } = await getOrganizationTeamsFromUsers( + const { teams } = await getOrganizationTeamsFromUsers( client, org, [userLogin], @@ -447,16 +461,20 @@ export class GithubOrgEntityProvider ); // we include group because the removed event need to update the old group too - if (!groups.some(g => g.metadata.name === group.metadata.name)) { - groups.push(group); + if (!teams.some(t => t.metadata.name === team.metadata.name)) { + teams.push(team); } - assignGroupsToUsers(usersToRebuild, groups); - buildOrgHierarchy(groups); + if (areGroupEntities(teams)) { + buildOrgHierarchy(teams); + if (areUserEntities(usersToRebuild)) { + assignGroupsToUsers(usersToRebuild, teams); + } + } const { added, removed } = createDeltaOperation(org, [ ...usersToRebuild, - ...groups, + ...teams, ]); await this.connection.applyMutation({ type: 'delta', @@ -588,10 +606,10 @@ function trackProgress(logger: Logger) { let timestamp = Date.now(); let summary: string; - logger.info('Reading GitHub users and groups'); + logger.info('Reading GitHub users and teams'); - function markReadComplete(read: { users: unknown[]; groups: unknown[] }) { - summary = `${read.users.length} GitHub users and ${read.groups.length} GitHub groups`; + function markReadComplete(read: { users: unknown[]; teams: unknown[] }) { + summary = `${read.users.length} GitHub users and ${read.teams.length} GitHub teams`; const readDuration = ((Date.now() - timestamp) / 1000).toFixed(1); timestamp = Date.now(); logger.info(`Read ${summary} in ${readDuration} seconds. Committing...`); diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md index d92ddbce43..39c8e960dd 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,80 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.2.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## 0.2.4 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- e6c721439f37: Added option to skip forked repos in GitlabDiscoveryEntityProvider +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- 2fe1f5973ff7: Filter Gitlab archived projects through APIs +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.2.4-next.2 + +### Patch Changes + +- 2fe1f5973ff7: Filter Gitlab archived projects through APIs +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + ## 0.2.4-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab/alpha-api-report.md b/plugins/catalog-backend-module-gitlab/alpha-api-report.md index f14b41d6c5..bb466a632e 100644 --- a/plugins/catalog-backend-module-gitlab/alpha-api-report.md +++ b/plugins/catalog-backend-module-gitlab/alpha-api-report.md @@ -6,7 +6,8 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; // @alpha -export const catalogModuleGitlabDiscoveryEntityProvider: () => BackendFeature; +const catalogModuleGitlabDiscoveryEntityProvider: () => BackendFeature; +export default catalogModuleGitlabDiscoveryEntityProvider; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog-backend-module-gitlab/catalog-info.yaml b/plugins/catalog-backend-module-gitlab/catalog-info.yaml new file mode 100644 index 0000000000..41968a1c34 --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-gitlab + title: '@backstage/plugin-catalog-backend-module-gitlab' + description: A Backstage catalog backend module that helps integrate towards GitLab +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 1465b430ae..788e39ba38 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab", "description": "A Backstage catalog backend module that helps integrate towards GitLab", - "version": "0.2.4-next.1", + "version": "0.2.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts index 6333184ea8..a56c229105 100644 --- a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts @@ -56,6 +56,7 @@ function setupFakeServer( listProjectsCallback: (request: { page: number; include_subgroups: boolean; + archived: boolean; }) => { data: GitLabProject[]; nextPage?: number; @@ -74,20 +75,24 @@ function setupFakeServer( } const page = req.url.searchParams.get('page'); const include_subgroups = req.url.searchParams.get('include_subgroups'); + const archived = req.url.searchParams.get('archived'); const response = listProjectsCallback({ page: parseInt(page!, 10), include_subgroups: include_subgroups === 'true', + archived: archived === 'true', }); // Filter the fake results based on the `last_activity_after` parameter const last_activity_after = req.url.searchParams.get( 'last_activity_after', ); - const filteredData = response.data.filter( - v => - !last_activity_after || - Date.parse(v.last_activity_at) >= Date.parse(last_activity_after), - ); + const filteredData = response.data + .filter( + v => + !last_activity_after || + Date.parse(v.last_activity_at) >= Date.parse(last_activity_after), + ) + .filter(v => archived || !v.archived); return res( ctx.set('x-next-page', response.nextPage?.toString() ?? ''), @@ -250,6 +255,7 @@ describe('GitlabDiscoveryProcessor', () => { }, ], }; + default: throw new Error('Invalid request'); } diff --git a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts index c9bd4d9187..02b1b1cb51 100644 --- a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts @@ -109,6 +109,7 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { const lastActivity = (await this.cache.get(this.getCacheKey())) as string; const opts = { + archived: false, group, page: 1, // We check for the existence of lastActivity and only set it if it's present to ensure @@ -125,10 +126,6 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { for await (const project of projects) { res.scanned++; - if (project.archived) { - continue; - } - if (branch === '*' && project.default_branch === undefined) { continue; } diff --git a/plugins/catalog-backend-module-gitlab/src/alpha.ts b/plugins/catalog-backend-module-gitlab/src/alpha.ts index ace970e3aa..65f5b05b49 100644 --- a/plugins/catalog-backend-module-gitlab/src/alpha.ts +++ b/plugins/catalog-backend-module-gitlab/src/alpha.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { catalogModuleGitlabDiscoveryEntityProvider } from './module/catalogModuleGitlabDiscoveryEntityProvider'; +export { catalogModuleGitlabDiscoveryEntityProvider as default } from './module/catalogModuleGitlabDiscoveryEntityProvider'; diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts index f0e169d8df..41b99e1c5b 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts @@ -30,6 +30,7 @@ export type CommonListOptions = { }; interface ListProjectOptions extends CommonListOptions { + archived?: boolean; group?: 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 da11601f09..46cfe403e8 100644 --- a/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts @@ -14,14 +14,8 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { coreServices } from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; -import { startTestBackend } from '@backstage/backend-test-utils'; -import { ConfigReader } from '@backstage/config'; +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 { catalogModuleGitlabDiscoveryEntityProvider } from './catalogModuleGitlabDiscoveryEntityProvider'; @@ -38,14 +32,14 @@ describe('catalogModuleGitlabDiscoveryEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); - const config = new ConfigReader({ + const config = { integrations: { gitlab: [ { @@ -69,16 +63,16 @@ describe('catalogModuleGitlabDiscoveryEntityProvider', () => { }, }, }, - }); + }; await startTestBackend({ extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], - services: [ - [coreServices.rootConfig, config], - [coreServices.logger, getVoidLogger()], - [coreServices.scheduler, scheduler], + features: [ + catalogModuleGitlabDiscoveryEntityProvider(), + mockServices.rootConfig.factory({ data: config }), + mockServices.logger.factory(), + scheduler.factory, ], - features: [catalogModuleGitlabDiscoveryEntityProvider()], }); expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index 08d2fe48a7..ce7a304746 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -155,6 +155,7 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { const projects = paginated( options => client.listProjects(options), { + archived: false, group: this.config.group, page: 1, per_page: 50, @@ -173,10 +174,6 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { res.scanned++; - if (project.archived) { - continue; - } - if ( this.config.skipForkedRepos && project.hasOwnProperty('forked_from_project') diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md index 0cc4dc584d..dd92105d91 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md +++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md @@ -1,5 +1,83 @@ # @backstage/plugin-catalog-backend-module-incremental-ingestion +## 0.4.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-events-node@0.2.12-next.2 + +## 0.4.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-events-node@0.2.12-next.1 + - @backstage/errors@1.2.1 + +## 0.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + - @backstage/plugin-permission-common@0.7.7 + +## 0.4.1 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- e2b6396a1274: Export new alpha `incrementalIngestionProvidersExtensionPoint` for registering incremental providers, rather than the providers being passed as options to the backend module. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-catalog-backend@1.12.0 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/plugin-events-node@0.2.9 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + +## 0.4.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.0-next.2 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-events-node@0.2.9-next.2 + ## 0.4.1-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-incremental-ingestion/README.md b/plugins/catalog-backend-module-incremental-ingestion/README.md index 3728121540..c5ed3e6367 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/README.md +++ b/plugins/catalog-backend-module-incremental-ingestion/README.md @@ -20,7 +20,7 @@ We created the Incremental Entity Provider to address all of the above issues. T Incremental Entity Providers will wait a configurable interval before proceeding to the next burst. -Once the source has no more results, Incremental Entity Provider compares all entities annotated with `@backstage/incremental-entity-provider: ` against all marked entities to determine which entities committed by same entity provider were not marked during the last ingestion cycle. All unmarked entities are deleted at the end of the cycle. The Incremental Entity Provider rests for a fixed internal before restarting the ingestion process. +Once the source has no more results, Incremental Entity Provider compares all entities annotated with `@backstage/incremental-entity-provider: ` against all marked entities to determine which entities committed by same entity provider were not marked during the last ingestion cycle. All unmarked entities are deleted at the end of the cycle. The Incremental Entity Provider rests for a fixed interval before restarting the ingestion process. ![Diagram of execution of an Incremental Entity Provider](https://user-images.githubusercontent.com/74687/185822734-ee6279c7-64fa-46b9-9aa8-d4092ab73858.png) diff --git a/plugins/catalog-backend-module-incremental-ingestion/alpha-api-report.md b/plugins/catalog-backend-module-incremental-ingestion/alpha-api-report.md index b11ec43cf5..6ab775f25b 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/alpha-api-report.md +++ b/plugins/catalog-backend-module-incremental-ingestion/alpha-api-report.md @@ -4,16 +4,24 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; +import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { IncrementalEntityProvider } from '@backstage/plugin-catalog-backend-module-incremental-ingestion'; import { IncrementalEntityProviderOptions } from '@backstage/plugin-catalog-backend-module-incremental-ingestion'; // @alpha -export const catalogModuleIncrementalIngestionEntityProvider: (options: { - providers: { - provider: IncrementalEntityProvider; +const catalogModuleIncrementalIngestionEntityProvider: () => BackendFeature; +export default catalogModuleIncrementalIngestionEntityProvider; + +// @alpha +export interface IncrementalIngestionProviderExtensionPoint { + addProvider(config: { options: IncrementalEntityProviderOptions; - }[]; -}) => BackendFeature; + provider: IncrementalEntityProvider; + }): void; +} + +// @alpha +export const incrementalIngestionProvidersExtensionPoint: ExtensionPoint; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog-backend-module-incremental-ingestion/catalog-info.yaml b/plugins/catalog-backend-module-incremental-ingestion/catalog-info.yaml new file mode 100644 index 0000000000..78c1b33d19 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-incremental-ingestion + title: '@backstage/plugin-catalog-backend-module-incremental-ingestion' + description: An entity provider for streaming large asset sources into the catalog +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json index d4cae08da0..668170ba32 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/package.json +++ b/plugins/catalog-backend-module-incremental-ingestion/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-incremental-ingestion", "description": "An entity provider for streaming large asset sources into the catalog", - "version": "0.4.1-next.1", + "version": "0.4.6-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/alpha.ts b/plugins/catalog-backend-module-incremental-ingestion/src/alpha.ts index c277a0a680..01a9b25f7c 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/alpha.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/alpha.ts @@ -15,3 +15,4 @@ */ export * from './module'; +export { default } from './module'; diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.test.ts b/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.test.ts index 0d910f664d..1f318c2590 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.test.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.test.ts @@ -24,7 +24,7 @@ import { WrapperProviders } from './WrapperProviders'; describe('WrapperProviders', () => { const applyDatabaseMigrations = jest.fn(); const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3', 'MYSQL_8'], }); const config = new ConfigReader({}); const logger = getVoidLogger(); diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/module/catalogModuleIncrementalIngestionEntityProvider.test.ts b/plugins/catalog-backend-module-incremental-ingestion/src/module/catalogModuleIncrementalIngestionEntityProvider.test.ts index ae480048d1..7d76ddf480 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/module/catalogModuleIncrementalIngestionEntityProvider.test.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/module/catalogModuleIncrementalIngestionEntityProvider.test.ts @@ -14,13 +14,14 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { coreServices } from '@backstage/backend-plugin-api'; -import { startTestBackend } from '@backstage/backend-test-utils'; -import { ConfigReader } from '@backstage/config'; +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { IncrementalEntityProvider } from '../types'; -import { catalogModuleIncrementalIngestionEntityProvider } from './catalogModuleIncrementalIngestionEntityProvider'; +import { + catalogModuleIncrementalIngestionEntityProvider, + incrementalIngestionProvidersExtensionPoint, +} from './catalogModuleIncrementalIngestionEntityProvider'; describe('catalogModuleIncrementalIngestionEntityProvider', () => { it('should register provider at the catalog extension point', async () => { @@ -35,39 +36,34 @@ describe('catalogModuleIncrementalIngestionEntityProvider', () => { }; const addEntityProvider = jest.fn(); - const httpRouterUse = jest.fn(); - const scheduler = {}; - const database = { - getClient: jest.fn(), - }; - const httpRouter = { - use: httpRouterUse, - }; + const httpRouterMock = mockServices.httpRouter.mock(); await startTestBackend({ extensionPoints: [ [catalogProcessingExtensionPoint, { addEntityProvider }], ], - services: [ - [coreServices.rootConfig, new ConfigReader({})], - [coreServices.database, database], - [coreServices.httpRouter, httpRouter], - [coreServices.logger, getVoidLogger()], - [coreServices.scheduler, scheduler], - ], features: [ - catalogModuleIncrementalIngestionEntityProvider({ - providers: [ - { - provider: provider1, - options: { - burstInterval: { seconds: 1 }, - burstLength: { seconds: 1 }, - restLength: { seconds: 1 }, + httpRouterMock.factory, + catalogModuleIncrementalIngestionEntityProvider(), + createBackendModule({ + pluginId: 'catalog', + moduleId: 'incrementalTest', + register(env) { + env.registerInit({ + deps: { extension: incrementalIngestionProvidersExtensionPoint }, + async init({ extension }) { + extension.addProvider({ + provider: provider1, + options: { + burstInterval: { seconds: 1 }, + burstLength: { seconds: 1 }, + restLength: { seconds: 1 }, + }, + }); }, - }, - ], + }); + }, }), ], }); @@ -76,6 +72,6 @@ describe('catalogModuleIncrementalIngestionEntityProvider', () => { expect(addEntityProvider.mock.calls[0][0].getProviderName()).toBe( 'provider1', ); - expect(httpRouterUse).toHaveBeenCalledTimes(1); + expect(httpRouterMock.use).toHaveBeenCalledTimes(1); }); }); diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/module/catalogModuleIncrementalIngestionEntityProvider.ts b/plugins/catalog-backend-module-incremental-ingestion/src/module/catalogModuleIncrementalIngestionEntityProvider.ts index 78d28d7b07..4e94088ef6 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/module/catalogModuleIncrementalIngestionEntityProvider.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/module/catalogModuleIncrementalIngestionEntityProvider.ts @@ -17,6 +17,7 @@ import { coreServices, createBackendModule, + createExtensionPoint, } from '@backstage/backend-plugin-api'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { @@ -25,56 +26,111 @@ import { } from '@backstage/plugin-catalog-backend-module-incremental-ingestion'; import { WrapperProviders } from './WrapperProviders'; +/** + * @alpha + * Interface for {@link incrementalIngestionProvidersExtensionPoint}. + */ +export interface IncrementalIngestionProviderExtensionPoint { + /** Adds a new incremental entity provider */ + addProvider(config: { + options: IncrementalEntityProviderOptions; + provider: IncrementalEntityProvider; + }): void; +} + +/** + * @alpha + * + * Extension point for registering incremental ingestion providers. + * The `catalogModuleIncrementalIngestionEntityProvider` must be installed for these providers to work. + * + * @example + * + * ```ts + * backend.add(createBackendModule({ + * pluginId: 'catalog', + * moduleId: 'myIncrementalProvider', + * register(env) { + * env.registerInit({ + * deps: { + * extension: incrementalIngestionProvidersExtensionPoint, + * }, + * async init({ extension }) { + * extension.addProvider({ + * burstInterval: ..., + * burstLength: ..., + * restLength: ..., + * }, { + * next(context, cursor) { + * ... + * }, + * ... + * }) + * }) + * }) + * } + * })) + * ``` + */ +export const incrementalIngestionProvidersExtensionPoint = + createExtensionPoint({ + id: 'catalog.incrementalIngestionProvider.providers', + }); + /** * Registers the incremental entity provider with the catalog processing extension point. * * @alpha */ export const catalogModuleIncrementalIngestionEntityProvider = - createBackendModule( - (options: { - providers: Array<{ + createBackendModule({ + pluginId: 'catalog', + moduleId: 'incrementalIngestionEntityProvider', + register(env) { + const addedProviders = new Array<{ provider: IncrementalEntityProvider; options: IncrementalEntityProviderOptions; - }>; - }) => ({ - pluginId: 'catalog', - moduleId: 'incrementalIngestionEntityProvider', - register(env) { - env.registerInit({ - deps: { - catalog: catalogProcessingExtensionPoint, - config: coreServices.rootConfig, - database: coreServices.database, - httpRouter: coreServices.httpRouter, - logger: coreServices.logger, - scheduler: coreServices.scheduler, - }, - async init({ - catalog, + }>(); + + env.registerExtensionPoint(incrementalIngestionProvidersExtensionPoint, { + addProvider({ options, provider }) { + addedProviders.push({ options, provider }); + }, + }); + + env.registerInit({ + deps: { + catalog: catalogProcessingExtensionPoint, + config: coreServices.rootConfig, + database: coreServices.database, + httpRouter: coreServices.httpRouter, + logger: coreServices.logger, + scheduler: coreServices.scheduler, + }, + async init({ + catalog, + config, + database, + httpRouter, + logger, + scheduler, + }) { + const client = await database.getClient(); + + const providers = new WrapperProviders({ config, - database, - httpRouter, logger, + client, scheduler, - }) { - const client = await database.getClient(); + }); - const providers = new WrapperProviders({ - config, - logger, - client, - scheduler, - }); + for (const entry of addedProviders) { + const wrapped = providers.wrap(entry.provider, entry.options); + catalog.addEntityProvider(wrapped); + } - for (const entry of options.providers) { - const wrapped = providers.wrap(entry.provider, entry.options); - catalog.addEntityProvider(wrapped); - } - - httpRouter.use(await providers.adminRouter()); - }, - }); - }, - }), - ); + httpRouter.use(await providers.adminRouter()); + }, + }); + }, + }); diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/module/index.ts b/plugins/catalog-backend-module-incremental-ingestion/src/module/index.ts index 9fcee99e01..8a395f06fe 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/module/index.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/module/index.ts @@ -14,4 +14,8 @@ * limitations under the License. */ -export { catalogModuleIncrementalIngestionEntityProvider } from './catalogModuleIncrementalIngestionEntityProvider'; +export { + catalogModuleIncrementalIngestionEntityProvider as default, + incrementalIngestionProvidersExtensionPoint, + type IncrementalIngestionProviderExtensionPoint, +} from './catalogModuleIncrementalIngestionEntityProvider'; diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/run.ts b/plugins/catalog-backend-module-incremental-ingestion/src/run.ts index e7e7a900b9..c24deb6adb 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/run.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/run.ts @@ -20,12 +20,14 @@ import { createBackend } from '@backstage/backend-defaults'; import { coreServices, + createBackendModule, createServiceFactory, } from '@backstage/backend-plugin-api'; import { ConfigReader } from '@backstage/config'; -import { catalogPlugin } from '@backstage/plugin-catalog-backend/alpha'; import { IncrementalEntityProvider } from '.'; -import { catalogModuleIncrementalIngestionEntityProvider } from './alpha'; +import catalogModuleIncrementalIngestionEntityProvider, { + incrementalIngestionProvidersExtensionPoint, +} from './alpha'; const provider: IncrementalEntityProvider = { getProviderName: () => 'test-provider', @@ -51,29 +53,36 @@ async function main() { }, }; - const backend = createBackend({ - services: [ - createServiceFactory({ - service: coreServices.rootConfig, - deps: {}, - factory: () => new ConfigReader(config), - }), - ], - }); + const backend = createBackend(); - backend.add(catalogPlugin()); backend.add( - catalogModuleIncrementalIngestionEntityProvider({ - providers: [ - { - provider: provider, - options: { - burstInterval: { seconds: 1 }, - burstLength: { seconds: 10 }, - restLength: { seconds: 10 }, + createServiceFactory({ + service: coreServices.rootConfig, + deps: {}, + factory: () => new ConfigReader(config), + }), + ); + backend.add(import('@backstage/plugin-catalog-backend/alpha')); + backend.add(catalogModuleIncrementalIngestionEntityProvider()); + backend.add( + createBackendModule({ + pluginId: 'catalog', + moduleId: 'incrementalTestProvider', + register(reg) { + reg.registerInit({ + deps: { extension: incrementalIngestionProvidersExtensionPoint }, + async init({ extension }) { + extension.addProvider({ + provider: provider, + options: { + burstInterval: { seconds: 1 }, + burstLength: { seconds: 10 }, + restLength: { seconds: 10 }, + }, + }); }, - }, - ], + }); + }, }), ); diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index 9cb5788b30..0acf538b9a 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,65 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.5.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + +## 0.5.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.5.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## 0.5.15 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + +## 0.5.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + ## 0.5.15-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/catalog-info.yaml b/plugins/catalog-backend-module-ldap/catalog-info.yaml new file mode 100644 index 0000000000..b3032ca72e --- /dev/null +++ b/plugins/catalog-backend-module-ldap/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-ldap + title: '@backstage/plugin-catalog-backend-module-ldap' + description: A Backstage catalog backend module that helps integrate towards LDAP +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 8a963923ff..158c43fcf3 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", "description": "A Backstage catalog backend module that helps integrate towards LDAP", - "version": "0.5.15-next.1", + "version": "0.5.18-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index f2e0173a13..91a4b6f004 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,70 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + +## 0.5.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.0 + +## 0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## 0.5.7 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- fb93323201bf: The alpha `catalogModuleMicrosoftGraphOrgEntityProvider` export no longer accepts options. Transformers are now instead configured via the `microsoftGraphOrgEntityProviderTransformExtensionPoint`. +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-common@1.0.15 + +## 0.5.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + ## 0.5.7-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/alpha-api-report.md b/plugins/catalog-backend-module-msgraph/alpha-api-report.md index 0fa093a1bc..b9c96e7798 100644 --- a/plugins/catalog-backend-module-msgraph/alpha-api-report.md +++ b/plugins/catalog-backend-module-msgraph/alpha-api-report.md @@ -4,20 +4,31 @@ ```ts import { BackendFeature } 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 { UserTransformer } from '@backstage/plugin-catalog-backend-module-msgraph'; // @alpha -export const catalogModuleMicrosoftGraphOrgEntityProvider: () => BackendFeature; +const catalogModuleMicrosoftGraphOrgEntityProvider: () => BackendFeature; +export default catalogModuleMicrosoftGraphOrgEntityProvider; // @alpha -export interface CatalogModuleMicrosoftGraphOrgEntityProviderOptions { - groupTransformer?: GroupTransformer | Record; - organizationTransformer?: - | OrganizationTransformer - | Record; - userTransformer?: UserTransformer | Record; +export const microsoftGraphOrgEntityProviderTransformExtensionPoint: ExtensionPoint; + +// @alpha +export interface MicrosoftGraphOrgEntityProviderTransformsExtensionPoint { + setGroupTransformer( + transformer: GroupTransformer | Record, + ): void; + setOrganizationTransformer( + transformer: + | OrganizationTransformer + | Record, + ): void; + setUserTransformer( + transformer: UserTransformer | Record, + ): void; } // (No @packageDocumentation comment for this package) diff --git a/plugins/catalog-backend-module-msgraph/catalog-info.yaml b/plugins/catalog-backend-module-msgraph/catalog-info.yaml new file mode 100644 index 0000000000..acaf5e73b9 --- /dev/null +++ b/plugins/catalog-backend-module-msgraph/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-msgraph + title: '@backstage/plugin-catalog-backend-module-msgraph' + description: >- + A Backstage catalog backend module that helps integrate towards Microsoft + Graph +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: pjungermann diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 002531f141..a159b2942a 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", - "version": "0.5.7-next.1", + "version": "0.5.10-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-msgraph/src/alpha.ts b/plugins/catalog-backend-module-msgraph/src/alpha.ts index c277a0a680..01a9b25f7c 100644 --- a/plugins/catalog-backend-module-msgraph/src/alpha.ts +++ b/plugins/catalog-backend-module-msgraph/src/alpha.ts @@ -15,3 +15,4 @@ */ export * from './module'; +export { default } from './module'; 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 9f1a4ead8b..d36995b1c8 100644 --- a/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts @@ -14,14 +14,8 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { coreServices } from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; -import { startTestBackend } from '@backstage/backend-test-utils'; -import { ConfigReader } from '@backstage/config'; +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'; @@ -38,14 +32,14 @@ describe('catalogModuleMicrosoftGraphOrgEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); - const config = new ConfigReader({ + const config = { catalog: { providers: { microsoftGraphOrg: { @@ -62,16 +56,15 @@ describe('catalogModuleMicrosoftGraphOrgEntityProvider', () => { }, }, }, - }); + }; await startTestBackend({ extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], - services: [ - [coreServices.rootConfig, config], - [coreServices.logger, getVoidLogger()], - [coreServices.scheduler, scheduler], + features: [ + catalogModuleMicrosoftGraphOrgEntityProvider(), + mockServices.rootConfig.factory({ data: config }), + scheduler.factory, ], - features: [catalogModuleMicrosoftGraphOrgEntityProvider()], }); expect(usedSchedule?.frequency).toEqual(Duration.fromISO('PT30M')); diff --git a/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.ts b/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.ts index 2d3e1dd4d4..f13081d3c6 100644 --- a/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.ts @@ -17,6 +17,7 @@ import { coreServices, createBackendModule, + createExtensionPoint, } from '@backstage/backend-plugin-api'; import { loggerToWinstonLogger } from '@backstage/backend-common'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; @@ -28,32 +29,50 @@ import { import { MicrosoftGraphOrgEntityProvider } from '../processors'; /** - * Options for {@link catalogModuleMicrosoftGraphOrgEntityProvider}. + * Interface for {@link microsoftGraphOrgEntityProviderTransformExtensionPoint}. * * @alpha */ -export interface CatalogModuleMicrosoftGraphOrgEntityProviderOptions { +export interface MicrosoftGraphOrgEntityProviderTransformsExtensionPoint { /** - * The function that transforms a user entry in msgraph to an entity. + * Set the function that transforms a user entry in msgraph to an entity. * Optionally, you can pass separate transformers per provider ID. */ - userTransformer?: UserTransformer | Record; + setUserTransformer( + transformer: UserTransformer | Record, + ): void; /** - * The function that transforms a group entry in msgraph to an entity. + * Set the function that transforms a group entry in msgraph to an entity. * Optionally, you can pass separate transformers per provider ID. */ - groupTransformer?: GroupTransformer | Record; + setGroupTransformer( + transformer: GroupTransformer | Record, + ): void; /** - * The function that transforms an organization entry in msgraph to an entity. + * Set the function that transforms an organization entry in msgraph to an entity. * Optionally, you can pass separate transformers per provider ID. */ - organizationTransformer?: - | OrganizationTransformer - | Record; + setOrganizationTransformer( + transformer: + | OrganizationTransformer + | Record, + ): void; } +/** + * Extension point used to customize the transforms used by the module. + * + * @alpha + */ +export const microsoftGraphOrgEntityProviderTransformExtensionPoint = + createExtensionPoint( + { + id: 'catalog.microsoftGraphOrgEntityProvider.transforms', + }, + ); + /** * Registers the MicrosoftGraphOrgEntityProvider with the catalog processing extension point. * @@ -63,10 +82,44 @@ export const catalogModuleMicrosoftGraphOrgEntityProvider = createBackendModule( { pluginId: 'catalog', moduleId: 'microsoftGraphOrgEntityProvider', - register( - env, - options?: CatalogModuleMicrosoftGraphOrgEntityProviderOptions, - ) { + register(env) { + let userTransformer: + | UserTransformer + | Record + | undefined; + let groupTransformer: + | GroupTransformer + | Record + | undefined; + let organizationTransformer: + | OrganizationTransformer + | Record + | undefined; + + env.registerExtensionPoint( + microsoftGraphOrgEntityProviderTransformExtensionPoint, + { + setUserTransformer(transformer) { + if (userTransformer) { + throw new Error('User transformer may only be set once'); + } + userTransformer = transformer; + }, + setGroupTransformer(transformer) { + if (groupTransformer) { + throw new Error('Group transformer may only be set once'); + } + groupTransformer = transformer; + }, + setOrganizationTransformer(transformer) { + if (organizationTransformer) { + throw new Error('Organization transformer may only be set once'); + } + organizationTransformer = transformer; + }, + }, + ); + env.registerInit({ deps: { catalog: catalogProcessingExtensionPoint, @@ -77,11 +130,11 @@ export const catalogModuleMicrosoftGraphOrgEntityProvider = createBackendModule( async init({ catalog, config, logger, scheduler }) { catalog.addEntityProvider( MicrosoftGraphOrgEntityProvider.fromConfig(config, { - groupTransformer: options?.groupTransformer, logger: loggerToWinstonLogger(logger), - organizationTransformer: options?.organizationTransformer, scheduler, - userTransformer: options?.userTransformer, + userTransformer: userTransformer, + groupTransformer: groupTransformer, + organizationTransformer: organizationTransformer, }), ); }, diff --git a/plugins/catalog-backend-module-msgraph/src/module/index.ts b/plugins/catalog-backend-module-msgraph/src/module/index.ts index f53f41141c..9b92588297 100644 --- a/plugins/catalog-backend-module-msgraph/src/module/index.ts +++ b/plugins/catalog-backend-module-msgraph/src/module/index.ts @@ -14,5 +14,8 @@ * limitations under the License. */ -export { catalogModuleMicrosoftGraphOrgEntityProvider } from './catalogModuleMicrosoftGraphOrgEntityProvider'; -export type { CatalogModuleMicrosoftGraphOrgEntityProviderOptions } from './catalogModuleMicrosoftGraphOrgEntityProvider'; +export { + catalogModuleMicrosoftGraphOrgEntityProvider as default, + microsoftGraphOrgEntityProviderTransformExtensionPoint, + type MicrosoftGraphOrgEntityProviderTransformsExtensionPoint, +} from './catalogModuleMicrosoftGraphOrgEntityProvider'; diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md index 1e63a1193d..0ea9fc4edf 100644 --- a/plugins/catalog-backend-module-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md @@ -1,5 +1,70 @@ # @backstage/plugin-catalog-backend-module-openapi +## 0.1.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + +## 0.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/types@1.1.0 + +## 0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## 0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-catalog-backend@1.12.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/integration@1.6.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + +## 0.1.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + ## 0.1.14-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-openapi/catalog-info.yaml b/plugins/catalog-backend-module-openapi/catalog-info.yaml new file mode 100644 index 0000000000..507bf555db --- /dev/null +++ b/plugins/catalog-backend-module-openapi/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-openapi + title: '@backstage/plugin-catalog-backend-module-openapi' + description: A Backstage catalog backend module that helps with OpenAPI specifications +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index 13f0305cae..9c182234c6 100644 --- a/plugins/catalog-backend-module-openapi/package.json +++ b/plugins/catalog-backend-module-openapi/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-openapi", "description": "A Backstage catalog backend module that helps with OpenAPI specifications", - "version": "0.1.14-next.1", + "version": "0.1.19-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md index 8d86cc9c2f..0137c41830 100644 --- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md +++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md @@ -1,5 +1,73 @@ # @backstage/plugin-catalog-backend-module-puppetdb +## 0.1.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + +## 0.1.5 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.1.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + ## 0.1.5-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-puppetdb/alpha-api-report.md b/plugins/catalog-backend-module-puppetdb/alpha-api-report.md index 0856fdb836..2f3921012a 100644 --- a/plugins/catalog-backend-module-puppetdb/alpha-api-report.md +++ b/plugins/catalog-backend-module-puppetdb/alpha-api-report.md @@ -6,7 +6,8 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; // @alpha -export const catalogModulePuppetDbEntityProvider: () => BackendFeature; +const catalogModulePuppetDbEntityProvider: () => BackendFeature; +export default catalogModulePuppetDbEntityProvider; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog-backend-module-puppetdb/catalog-info.yaml b/plugins/catalog-backend-module-puppetdb/catalog-info.yaml new file mode 100644 index 0000000000..11021ea68f --- /dev/null +++ b/plugins/catalog-backend-module-puppetdb/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-puppetdb + title: '@backstage/plugin-catalog-backend-module-puppetdb' + description: A Backstage catalog backend module that helps integrate towards PuppetDB +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index d17b788737..9e830c0902 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-puppetdb", "description": "A Backstage catalog backend module that helps integrate towards PuppetDB", - "version": "0.1.5-next.1", + "version": "0.1.8-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-puppetdb/src/alpha.ts b/plugins/catalog-backend-module-puppetdb/src/alpha.ts index c277a0a680..01a9b25f7c 100644 --- a/plugins/catalog-backend-module-puppetdb/src/alpha.ts +++ b/plugins/catalog-backend-module-puppetdb/src/alpha.ts @@ -15,3 +15,4 @@ */ export * from './module'; +export { default } from './module'; diff --git a/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.test.ts b/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.test.ts index c167035917..f931b2c741 100644 --- a/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.test.ts +++ b/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.test.ts @@ -14,14 +14,8 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { coreServices } from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; -import { startTestBackend } from '@backstage/backend-test-utils'; -import { ConfigReader } from '@backstage/config'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { catalogModulePuppetDbEntityProvider } from './catalogModulePuppetDbEntityProvider'; import { PuppetDbEntityProvider } from '../providers/PuppetDbEntityProvider'; @@ -37,14 +31,14 @@ describe('catalogModulePuppetDbEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); - const config = new ConfigReader({ + const config = { catalog: { providers: { puppetdb: { @@ -56,16 +50,15 @@ describe('catalogModulePuppetDbEntityProvider', () => { }, }, }, - }); + }; await startTestBackend({ extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], - services: [ - [coreServices.rootConfig, config], - [coreServices.logger, getVoidLogger()], - [coreServices.scheduler, scheduler], + features: [ + catalogModulePuppetDbEntityProvider(), + mockServices.rootConfig.factory({ data: config }), + scheduler.factory, ], - features: [catalogModulePuppetDbEntityProvider()], }); expect(usedSchedule?.frequency).toEqual({ minutes: 10 }); diff --git a/plugins/catalog-backend-module-puppetdb/src/module/index.ts b/plugins/catalog-backend-module-puppetdb/src/module/index.ts index f30f236df4..21dc43561d 100644 --- a/plugins/catalog-backend-module-puppetdb/src/module/index.ts +++ b/plugins/catalog-backend-module-puppetdb/src/module/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { catalogModulePuppetDbEntityProvider } from './catalogModulePuppetDbEntityProvider'; +export { catalogModulePuppetDbEntityProvider as default } from './catalogModulePuppetDbEntityProvider'; diff --git a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md index 0e4ac7a0e2..cc800f60a5 100644 --- a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md +++ b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-catalog-backend-module-unprocessed +## 0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + +## 0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + +## 0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + +## 0.2.0 + +### Minor Changes + +- 5156a94c2e2a: **BREAKING**: Fixing typo in exported module. You will have to rename the import to the correct spelling. `UnprocessedEntites` -> `UnprocessedEntities` + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/catalog-model@1.4.1 + +## 0.2.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + ## 0.2.0-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-unprocessed/api-report.md b/plugins/catalog-backend-module-unprocessed/api-report.md index 7fd7119276..6e5e4f843b 100644 --- a/plugins/catalog-backend-module-unprocessed/api-report.md +++ b/plugins/catalog-backend-module-unprocessed/api-report.md @@ -8,7 +8,8 @@ import { HttpRouterService } from '@backstage/backend-plugin-api'; import { Knex } from 'knex'; // @public -export const catalogModuleUnprocessedEntities: () => BackendFeature; +const catalogModuleUnprocessedEntities: () => BackendFeature; +export default catalogModuleUnprocessedEntities; // @public export class UnprocessedEntitiesModule { diff --git a/plugins/catalog-backend-module-unprocessed/catalog-info.yaml b/plugins/catalog-backend-module-unprocessed/catalog-info.yaml new file mode 100644 index 0000000000..a889859494 --- /dev/null +++ b/plugins/catalog-backend-module-unprocessed/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-unprocessed + title: '@backstage/plugin-catalog-backend-module-unprocessed' + description: Backstage Catalog module to view unprocessed entities +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: catalog-maintainers diff --git a/plugins/catalog-backend-module-unprocessed/package.json b/plugins/catalog-backend-module-unprocessed/package.json index 6052015439..6d0e25107e 100644 --- a/plugins/catalog-backend-module-unprocessed/package.json +++ b/plugins/catalog-backend-module-unprocessed/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-unprocessed", "description": "Backstage Catalog module to view unprocessed entities", - "version": "0.2.0-next.1", + "version": "0.2.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-unprocessed/src/index.ts b/plugins/catalog-backend-module-unprocessed/src/index.ts index 21cb2dbcd6..20132d9b27 100644 --- a/plugins/catalog-backend-module-unprocessed/src/index.ts +++ b/plugins/catalog-backend-module-unprocessed/src/index.ts @@ -20,5 +20,5 @@ * @packageDocumentation */ -export * from './module'; +export { catalogModuleUnprocessedEntities as default } from './module'; export * from './UnprocessedEntitiesModule'; diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index cedbc1ed08..cfc1bc40f4 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,146 @@ # @backstage/plugin-catalog-backend +## 1.13.0-next.2 + +### Patch Changes + +- acffa17027b6: Added some examples to the catalog OpenAPI definition +- 45947d3b2759: Fixes an issue where `order` was not a recognized parameter for the `/entities` endpoint. +- 814feeed7343: Update to handle invalid luxon values +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.7-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-events-node@0.2.12-next.2 + - @backstage/plugin-scaffolder-common@1.4.1-next.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## 1.13.0-next.1 + +### Minor Changes + +- 62f448edb0b5: Allow configuring the processing interval in your app-config, under the `catalog.processingInterval` key. + +### Patch Changes + +- 1fd2109739c1: Changed the processing loop task pipeline implementation from recursive to iterative +- 0f8a97777489: Update OpenAPI schema to relax the encoding validation of all request parameters. +- 2d32d8a611e3: Fixed validation of the `fullTextFilterFields` query parameter. +- 618257f3e413: Fix issue with `catalogFileName` not being a required property for `/analyze-location` +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.7-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/plugin-events-node@0.2.12-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/plugin-scaffolder-common@1.4.1-next.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-search-common@1.2.6-next.0 + +## 1.12.2-next.0 + +### Patch Changes + +- 149361e81622: Fix to the `limit` parameter on entity queries. +- 0198aa596fd9: Fixed a link to the frontend Backstage plugin that had pointed to itself. +- 41d1b2d628ea: Fix OpenAPI schema for the facets endpoint +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-events-node@0.2.11-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/plugin-search-backend-module-catalog@0.1.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## 1.12.0 + +### Minor Changes + +- b8cccd8ee858: Support configuring applicable kinds for `AnnotateScmSlugEntityProcessor` +- f32252cdf631: Added OpenTelemetry spans for catalog processing +- ebeb77586975: Now performs request validation based on OpenAPI schema through `@backstage/backend-openapi-utils`. Error responses for invalid input, like `"a"` instead of a number, may have changed. + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- b8d6b22acd57: Internal refactor for load test +- Updated dependencies + - @backstage/plugin-search-backend-module-catalog@0.1.4 + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/plugin-events-node@0.2.9 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-common@1.2.5 + +## 1.12.0-next.2 + +### Minor Changes + +- b8cccd8ee858: Support configuring applicable kinds for `AnnotateScmSlugEntityProcessor` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-module-catalog@0.1.4-next.2 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-events-node@0.2.9-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + ## 1.12.0-next.1 ### Minor Changes diff --git a/plugins/catalog-backend/README.md b/plugins/catalog-backend/README.md index 1e2bce3142..cd921499d3 100644 --- a/plugins/catalog-backend/README.md +++ b/plugins/catalog-backend/README.md @@ -86,5 +86,5 @@ some example entities. ## Links -- [catalog](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend) +- [catalog](https://github.com/backstage/backstage/tree/master/plugins/catalog) is the frontend interface for this plugin. diff --git a/plugins/catalog-backend/alpha-api-report.md b/plugins/catalog-backend/alpha-api-report.md index df3e809ade..ff826508b2 100644 --- a/plugins/catalog-backend/alpha-api-report.md +++ b/plugins/catalog-backend/alpha-api-report.md @@ -74,7 +74,8 @@ export type CatalogPermissionRule< > = PermissionRule; // @alpha -export const catalogPlugin: () => BackendFeature; +const catalogPlugin: () => BackendFeature; +export default catalogPlugin; // @alpha export const createCatalogConditionalDecision: ( diff --git a/plugins/catalog-backend/catalog-info.yaml b/plugins/catalog-backend/catalog-info.yaml new file mode 100644 index 0000000000..b94d3b458a --- /dev/null +++ b/plugins/catalog-backend/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend + title: '@backstage/plugin-catalog-backend' + description: The Backstage backend plugin that provides the Backstage catalog +spec: + lifecycle: production + type: backstage-backend-plugin + owner: catalog-maintainers diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 03e47f8e61..4b9f606043 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { HumanDuration } from '@backstage/types'; + export interface Config { /** * Configuration options for the catalog plugin. @@ -142,5 +144,30 @@ export interface Config { * "keep". */ orphanStrategy?: 'keep' | 'delete'; + + /** + * The interval at which the catalog should process its entities. + * + * @remarks + * + * Example: + * + * ```yaml + * catalog: + * processingInterval: { minutes: 30 } + * ``` + * + * Note that this is only a suggested minimum, and the actual interval may + * be longer. Internally, the catalog will scale up this number by a small + * factor and choose random numbers in that range to spread out the load. If + * the catalog is overloaded and cannot process all entities during the + * interval, the time taken between processing runs of any given entity may + * also be longer than specified 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. + */ + processingInterval?: HumanDuration; }; } diff --git a/plugins/catalog-backend/migrations/20210302150147_refresh_state.js b/plugins/catalog-backend/migrations/20210302150147_refresh_state.js index 1c8081c0ef..f600667727 100644 --- a/plugins/catalog-backend/migrations/20210302150147_refresh_state.js +++ b/plugins/catalog-backend/migrations/20210302150147_refresh_state.js @@ -37,7 +37,7 @@ exports.up = async function up(knex) { .notNullable() .comment('The unprocessed entity (in original form) as JSON'); table - .text('processed_entity') + .text('processed_entity', 'longtext') .nullable() .comment('The processed entity (not yet stitched) as JSON'); table @@ -83,7 +83,7 @@ exports.up = async function up(knex) { .notNullable() .comment('Random value representing a unique stitch attempt ticket'); table - .text('final_entity') + .text('final_entity', 'longtext') .nullable() .comment('The JSON encoded final entity'); table.index('entity_id', 'final_entities_entity_id_idx'); diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 2fee3c3a46..3e13c4948f 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend", "description": "The Backstage backend plugin that provides the Backstage catalog", - "version": "1.12.0-next.1", + "version": "1.13.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend/src/alpha.ts b/plugins/catalog-backend/src/alpha.ts index c90f4d1cdf..06c9571bfa 100644 --- a/plugins/catalog-backend/src/alpha.ts +++ b/plugins/catalog-backend/src/alpha.ts @@ -21,4 +21,4 @@ import type { EntitiesSearchFilter } from './catalog/types'; export type { /** @alpha */ EntitiesSearchFilter }; export * from './permissions'; -export { catalogPlugin } from './service/CatalogPlugin'; +export { catalogPlugin as default } from './service/CatalogPlugin'; diff --git a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts index 4959a33eef..01025f73dd 100644 --- a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts +++ b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts @@ -138,6 +138,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { type, }), ); + await tx.batchInsert( 'relations', this.deduplicateRelations(relationRows), @@ -374,7 +375,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { entityRef, newLocationKey: locationKey, existingLocationKey: conflictingKey, - lastConflictAt: DateTime.now().toISO(), + lastConflictAt: DateTime.now().toISO()!, }, }; await this.options.eventBroker?.publish(eventParams); diff --git a/plugins/catalog-backend/src/index.ts b/plugins/catalog-backend/src/index.ts index 345102adcd..55c1c672d3 100644 --- a/plugins/catalog-backend/src/index.ts +++ b/plugins/catalog-backend/src/index.ts @@ -36,13 +36,13 @@ import { /** * @public - * @deprecated import from `@backstage/search-backend-module-catalog` instead + * @deprecated import from `@backstage/plugin-search-backend-module-catalog` instead */ export const DefaultCatalogCollatorFactory = _DefaultCatalogCollatorFactory; /** * @public - * @deprecated import from `@backstage/search-backend-module-catalog` instead + * @deprecated import from `@backstage/plugin-search-backend-module-catalog` instead */ export const defaultCatalogCollatorEntityTransformer = _defaultCatalogCollatorEntityTransformer; @@ -54,14 +54,14 @@ import type { /** * @public - * @deprecated import from `@backstage/search-backend-module-catalog` instead + * @deprecated import from `@backstage/plugin-search-backend-module-catalog` instead */ export type DefaultCatalogCollatorFactoryOptions = _DefaultCatalogCollatorFactoryOptions; /** * @public - * @deprecated import from `@backstage/search-backend-module-catalog` instead + * @deprecated import from `@backstage/plugin-search-backend-module-catalog` instead */ export type CatalogCollatorEntityTransformer = _CatalogCollatorEntityTransformer; diff --git a/plugins/catalog-backend/src/processing/TaskPipeline.test.ts b/plugins/catalog-backend/src/processing/TaskPipeline.test.ts index f814e8a608..43ec118bdb 100644 --- a/plugins/catalog-backend/src/processing/TaskPipeline.test.ts +++ b/plugins/catalog-backend/src/processing/TaskPipeline.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { startTaskPipeline } from './TaskPipeline'; +import { startTaskPipeline, createBarrier } from './TaskPipeline'; function createLimitedLoader(count: number, loadDelay?: number) { const items = new Array(count).fill(0).map((_, index) => index); @@ -119,3 +119,88 @@ describe('startTaskPipeline', () => { }).toThrow('must be lower'); }); }); + +describe('createBarrier', () => { + const tick = (millis: number) => + new Promise(resolve => setTimeout(resolve, millis)); + + it('abandons a wait after the timeout expires', async () => { + const abortController = new AbortController(); + const signal = abortController.signal; + const barrier = createBarrier({ waitTimeoutMillis: 100, signal }); + + const fn1 = jest.fn(); + barrier.wait().then(fn1); + + await tick(0); + expect(fn1).not.toHaveBeenCalled(); + + await tick(50); + expect(fn1).not.toHaveBeenCalled(); + + // start a new wait mid-way through the timeout + // should NOT resolve when the first one times out + const fn2 = jest.fn(); + barrier.wait().then(fn2); + + await tick(0); + expect(fn2).not.toHaveBeenCalled(); + + await tick(50); + expect(fn1).toHaveBeenCalledTimes(1); + expect(fn2).not.toHaveBeenCalled(); + + await tick(50); + expect(fn1).toHaveBeenCalledTimes(1); + expect(fn2).toHaveBeenCalledTimes(1); + }); + + it('abandons a wait after aborted', async () => { + const abortController = new AbortController(); + const signal = abortController.signal; + const barrier = createBarrier({ waitTimeoutMillis: 100, signal }); + + const fn1 = jest.fn(); + barrier.wait().then(fn1); + + // should resolve immediately, not after timeout + await tick(0); + expect(fn1).not.toHaveBeenCalled(); + abortController.abort(); + await tick(0); + expect(fn1).toHaveBeenCalledTimes(1); + + // subsequent waits should be immediate no matter what + const fn2 = jest.fn(); + barrier.wait().then(fn2); + await tick(0); + expect(fn2).toHaveBeenCalledTimes(1); + }); + + it('release immediately unblocks all waits', async () => { + const abortController = new AbortController(); + const signal = abortController.signal; + const barrier = createBarrier({ waitTimeoutMillis: 100, signal }); + + const fn1 = jest.fn(); + barrier.wait().then(fn1); + + await tick(50); + expect(fn1).not.toHaveBeenCalled(); + + // start a new wait mid-way through the timeout + // SHOULD resolve when releasing + const fn2 = jest.fn(); + barrier.wait().then(fn2); + + await tick(0); + expect(fn1).not.toHaveBeenCalled(); + expect(fn2).not.toHaveBeenCalled(); + + barrier.release(); + + await tick(0); + expect(fn1).toHaveBeenCalledTimes(1); + expect(fn2).toHaveBeenCalledTimes(1); + }); +}); diff --git a/plugins/catalog-backend/src/processing/TaskPipeline.ts b/plugins/catalog-backend/src/processing/TaskPipeline.ts index 4afe2408a8..44030be9e0 100644 --- a/plugins/catalog-backend/src/processing/TaskPipeline.ts +++ b/plugins/catalog-backend/src/processing/TaskPipeline.ts @@ -72,50 +72,112 @@ export function startTaskPipeline(options: Options) { throw new Error('lowWatermark must be lower than highWatermark'); } - let loading = false; - let stopped = false; - let inFlightCount = 0; + // State is in an object so that it can be stably referenced from within + // callbacks below + const state = { inFlightCount: 0 }; + const abortController = new AbortController(); + const abortSignal = abortController.signal; - async function maybeLoadMore() { - if (stopped || loading || inFlightCount > lowWatermark) { - return; - } + const barrier = createBarrier({ + waitTimeoutMillis: pollingIntervalMs, + signal: abortSignal, + }); - // Once we hit the low watermark we load in enough items to reach the high watermark - loading = true; - const loadCount = highWatermark - inFlightCount; - const loadedItems = await loadTasks(loadCount); - loading = false; - - // We might not reach the high watermark here, in case there weren't enough items to load - inFlightCount += loadedItems.length; - loadedItems.forEach(item => { - processTask(item).finally(() => { - if (stopped) { - return; + async function pipelineLoop() { + while (!abortSignal.aborted) { + if (state.inFlightCount <= lowWatermark) { + const loadCount = highWatermark - state.inFlightCount; + const loadedItems = await Promise.resolve() + .then(() => loadTasks(loadCount)) + .catch(() => { + // Silently swallow errors and go back to sleep to try again; we + // delegate to the loadTasks function itself to catch errors and log + // if it so desires + return []; + }); + if (loadedItems.length && !abortSignal.aborted) { + state.inFlightCount += loadedItems.length; + for (const item of loadedItems) { + Promise.resolve() + .then(() => processTask(item)) + .catch(() => { + // Silently swallow errors and go back to sleep to try again; we + // delegate to the processTask function itself to catch errors + // and log if it so desires + }) + .finally(() => { + state.inFlightCount -= 1; + barrier.release(); + }); + } } + } - // For each item we complete we check if it's time to load more - inFlightCount -= 1; - maybeLoadMore(); - }); - }); - - // We might have processed some tasks while we where loading, so check if we can load more - if (loadedItems.length > 1) { - maybeLoadMore(); + await barrier.wait(); } } - // This interval makes sure that we load in new items if the loop runs - // dry because of the lack of available tasks. As long as there are - // enough items to process this will be a noop. - const intervalId = setInterval(() => { - maybeLoadMore(); - }, pollingIntervalMs); + pipelineLoop().catch(error => { + // This should be impossible, but if it did happen, it would signal a + // programming error inside the loop (errors should definitely be caught + // inside of it). Let's rethrow with more information, and let it be caught + // by the process' uncaught exception handler, which will log the occurrence + // at a high level. + throw new Error(`Unexpected error in processing pipeline loop`, error); + }); return () => { - stopped = true; - clearInterval(intervalId); + abortController.abort(); + barrier.destroy(); + }; +} + +/** + * Creates a barrier with a timeout, that can be awaited or prematurely + * released either manually or by an abort signal. + */ +export function createBarrier(options: { + waitTimeoutMillis: number; + signal: AbortSignal; +}): { + wait: () => Promise; + release: () => void; + destroy: () => void; +} { + const { waitTimeoutMillis, signal } = options; + const resolvers = new Set<() => void>(); + + function wait() { + if (signal.aborted || !(waitTimeoutMillis > 0)) { + return Promise.resolve(); + } + + return new Promise(resolve => { + const timeoutHandle = setTimeout(done, waitTimeoutMillis); + + function done() { + resolvers.delete(done); + clearTimeout(timeoutHandle); + resolve(); + } + + resolvers.add(done); + }); + } + + function release() { + const resolversToCall = new Set(resolvers); + resolvers.clear(); + for (const resolver of resolversToCall) { + resolver(); + } + } + + signal.addEventListener('abort', release); + + return { + wait, + release, + destroy: () => signal.removeEventListener('abort', release), }; } diff --git a/plugins/catalog-backend/src/schema/openapi.generated.ts b/plugins/catalog-backend/src/schema/openapi.generated.ts index 7428e5ace2..2278a30e62 100644 --- a/plugins/catalog-backend/src/schema/openapi.generated.ts +++ b/plugins/catalog-backend/src/schema/openapi.generated.ts @@ -45,6 +45,7 @@ export const spec = { name: 'kind', in: 'path', required: true, + allowReserved: true, schema: { type: 'string', }, @@ -53,6 +54,7 @@ export const spec = { name: 'namespace', in: 'path', required: true, + allowReserved: true, schema: { type: 'string', }, @@ -61,6 +63,7 @@ export const spec = { name: 'name', in: 'path', required: true, + allowReserved: true, schema: { type: 'string', }, @@ -69,6 +72,7 @@ export const spec = { name: 'uid', in: 'path', required: true, + allowReserved: true, schema: { type: 'string', }, @@ -78,6 +82,7 @@ export const spec = { in: 'query', description: 'Cursor to a set page of results.', required: false, + allowReserved: true, schema: { type: 'string', minLength: 1, @@ -88,6 +93,7 @@ export const spec = { in: 'query', description: 'Pointer to the previous page of results.', required: false, + allowReserved: true, schema: { type: 'string', minLength: 1, @@ -105,24 +111,44 @@ export const spec = { type: 'string', }, }, + examples: { + 'Get name and the entire relations collection': { + value: ['metadata.name', 'relations'], + }, + 'Get kind, name and namespace': { + value: ['kind', 'metadata.name', 'metadata.namespace'], + }, + }, }, filter: { name: 'filter', in: 'query', description: 'Filter for just the entities defined by this filter.', required: false, + allowReserved: true, schema: { type: 'array', items: { type: 'string', }, }, + examples: { + 'Get groups': { + value: ['kind=group'], + }, + 'Get orphaned components': { + value: [ + 'kind=component,metadata.annotations.backstage.io/orphan=true', + ], + }, + }, }, offset: { name: 'offset', in: 'query', description: 'Number of records to skip in the query page.', required: false, + allowReserved: true, schema: { type: 'integer', minimum: 0, @@ -133,9 +159,10 @@ export const spec = { in: 'query', description: 'Number of records to return in the response.', required: false, + allowReserved: true, schema: { type: 'integer', - minimum: 1, + minimum: 0, }, }, orderField: { @@ -153,6 +180,14 @@ export const spec = { }, explode: true, style: 'form', + examples: { + 'Order ascending by name': { + value: ['metadata.name,asc'], + }, + 'Order descending by owner': { + value: ['spec.owner,desc'], + }, + }, }, }, requestBodies: {}, @@ -817,6 +852,18 @@ export const spec = { { $ref: '#/components/parameters/after', }, + { + name: 'order', + in: 'query', + allowReserved: true, + required: false, + schema: { + type: 'array', + items: { + type: 'string', + }, + }, + }, ], }, }, @@ -989,6 +1036,22 @@ export const spec = { }, }, }, + examples: { + 'Fetch Backstage entities': { + value: { + entityRefs: [ + 'component:default/backstage', + 'api:default/backstage', + ], + }, + }, + 'Fetch annotations for backstage entity': { + value: { + entityRefs: ['component:default/backstage'], + fields: ['metadata.annotations'], + }, + }, + }, }, }, }, @@ -1037,6 +1100,7 @@ export const spec = { in: 'query', description: 'Text search term.', required: false, + allowReserved: true, schema: { type: 'string', }, @@ -1047,6 +1111,7 @@ export const spec = { description: 'A comma separated list of fields to sort returned results by.', required: false, + allowReserved: true, schema: { type: 'array', items: { @@ -1086,8 +1151,20 @@ export const spec = { in: 'query', name: 'facet', required: true, + allowReserved: true, schema: { - type: 'string', + type: 'array', + items: { + type: 'string', + }, + }, + examples: { + 'Entities by kind': { + value: ['kind'], + }, + 'Entities by spec type': { + value: ['spec.type'], + }, }, }, { @@ -1138,6 +1215,7 @@ export const spec = { in: 'query', name: 'dryRun', required: false, + allowReserved: true, schema: { type: 'string', }, @@ -1223,6 +1301,7 @@ export const spec = { in: 'path', name: 'id', required: true, + allowReserved: true, schema: { type: 'string', }, @@ -1248,6 +1327,7 @@ export const spec = { in: 'path', name: 'id', required: true, + allowReserved: true, schema: { type: 'string', }, @@ -1292,7 +1372,7 @@ export const spec = { $ref: '#/components/schemas/LocationInput', }, }, - required: ['catalogFileName', 'location'], + required: ['location'], }, }, }, diff --git a/plugins/catalog-backend/src/schema/openapi.yaml b/plugins/catalog-backend/src/schema/openapi.yaml index 34e21adffd..4db3147a97 100644 --- a/plugins/catalog-backend/src/schema/openapi.yaml +++ b/plugins/catalog-backend/src/schema/openapi.yaml @@ -20,24 +20,28 @@ components: name: kind in: path required: true + allowReserved: true schema: type: string namespace: name: namespace in: path required: true + allowReserved: true schema: type: string name: name: name in: path required: true + allowReserved: true schema: type: string uid: name: uid in: path required: true + allowReserved: true schema: type: string cursor: @@ -45,6 +49,7 @@ components: in: query description: Cursor to a set page of results. required: false + allowReserved: true schema: type: string minLength: 1 @@ -53,6 +58,7 @@ components: in: query description: Pointer to the previous page of results. required: false + allowReserved: true schema: type: string minLength: 1 @@ -66,20 +72,33 @@ components: type: array items: type: string + examples: + 'Get name and the entire relations collection': + value: ['metadata.name', 'relations'] + 'Get kind, name and namespace': + value: ['kind', 'metadata.name', 'metadata.namespace'] filter: name: filter in: query description: Filter for just the entities defined by this filter. required: false + allowReserved: true schema: type: array items: type: string + examples: + 'Get groups': + value: ['kind=group'] + 'Get orphaned components': + value: + ['kind=component,metadata.annotations.backstage.io/orphan=true'] offset: name: offset in: query description: Number of records to skip in the query page. required: false + allowReserved: true schema: type: integer minimum: 0 @@ -88,9 +107,10 @@ components: in: query description: Number of records to return in the response. required: false + allowReserved: true schema: type: integer - minimum: 1 + minimum: 0 orderField: name: orderField in: query @@ -104,6 +124,11 @@ components: description: A two-item tuple of [field, order]. explode: true style: form + examples: + 'Order ascending by name': + value: ['metadata.name,asc'] + 'Order descending by owner': + value: ['spec.owner,desc'] requestBodies: {} responses: {} schemas: @@ -668,6 +693,14 @@ paths: - $ref: '#/components/parameters/filter' - $ref: '#/components/parameters/offset' - $ref: '#/components/parameters/after' + - name: order + in: query + allowReserved: true + required: false + schema: + type: array + items: + type: string /entities/by-uid/{uid}: get: operationId: GetEntityByUid @@ -764,6 +797,15 @@ paths: type: array items: type: string + examples: + 'Fetch Backstage entities': + value: + entityRefs: + ['component:default/backstage', 'api:default/backstage'] + 'Fetch annotations for backstage entity': + value: + entityRefs: ['component:default/backstage'] + fields: ['metadata.annotations'] /entities/by-query: get: operationId: GetEntitiesByQuery @@ -788,12 +830,14 @@ paths: in: query description: Text search term. required: false + allowReserved: true schema: type: string - name: fullTextFilterFields in: query description: A comma separated list of fields to sort returned results by. required: false + allowReserved: true schema: type: array items: @@ -818,8 +862,16 @@ paths: - in: query name: facet required: true + allowReserved: true schema: - type: string + type: array + items: + type: string + examples: + 'Entities by kind': + value: ['kind'] + 'Entities by spec type': + value: ['spec.type'] - $ref: '#/components/parameters/filter' /locations: post: @@ -851,6 +903,7 @@ paths: - in: query name: dryRun required: false + allowReserved: true schema: type: string requestBody: @@ -906,6 +959,7 @@ paths: - in: path name: id required: true + allowReserved: true schema: type: string delete: @@ -921,6 +975,7 @@ paths: - in: path name: id required: true + allowReserved: true schema: type: string /analyze-location: @@ -950,7 +1005,6 @@ paths: location: $ref: '#/components/schemas/LocationInput' required: - - catalogFileName - location /validate-entity: post: diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 9c351a76f2..531bb91dd2 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -74,7 +74,7 @@ import { createRouter } from './createRouter'; import { DefaultRefreshService } from './DefaultRefreshService'; import { AuthorizedRefreshService } from './AuthorizedRefreshService'; import { DefaultCatalogRulesEnforcer } from '../ingestion/CatalogRules'; -import { Config } from '@backstage/config'; +import { Config, readDurationFromConfig } from '@backstage/config'; import { Logger } from 'winston'; import { connectEntityProviders } from '../processing/connectEntityProviders'; import { PermissionRuleParams } from '@backstage/plugin-permission-common'; @@ -100,6 +100,7 @@ import { AuthorizedLocationService } from './AuthorizedLocationService'; import { DefaultProviderDatabase } from '../database/DefaultProviderDatabase'; import { DefaultCatalogDatabase } from '../database/DefaultCatalogDatabase'; import { EventBroker } from '@backstage/plugin-events-node'; +import { durationToMilliseconds } from '@backstage/types'; /** * This is a duplicate of the alpha `CatalogPermissionRule` type, for use in the stable API. @@ -160,11 +161,7 @@ export class CatalogBuilder { unprocessedEntity: Entity; errors: Error[]; }) => Promise | void; - private processingInterval: ProcessingIntervalFunction = - createRandomProcessingInterval({ - minSeconds: 100, - maxSeconds: 150, - }); + private processingInterval: ProcessingIntervalFunction; private locationAnalyzer: LocationAnalyzer | undefined = undefined; private readonly permissionRules: CatalogPermissionRuleInput[]; private allowedLocationType: string[]; @@ -191,6 +188,10 @@ export class CatalogBuilder { this.parser = undefined; this.permissionRules = Object.values(catalogPermissionRules); this.allowedLocationType = ['url']; + + this.processingInterval = CatalogBuilder.getDefaultProcessingInterval( + env.config, + ); } /** @@ -422,7 +423,7 @@ export class CatalogBuilder { } /** - * Enables the publishing of events for cloflicts in the DefaultProcessingDatabase + * Enables the publishing of events for conflicts in the DefaultProcessingDatabase */ setEventBroker(broker: EventBroker): CatalogBuilder { this.eventBroker = broker; @@ -758,4 +759,30 @@ export class CatalogBuilder { 'https://backstage.io/docs/integrations/azure/org', ); } + + private static getDefaultProcessingInterval( + config: Config, + ): ProcessingIntervalFunction { + const processingIntervalKey = 'catalog.processingInterval'; + + if (!config.has(processingIntervalKey)) { + return createRandomProcessingInterval({ + minSeconds: 100, + maxSeconds: 150, + }); + } + + const duration = readDurationFromConfig(config, { + key: processingIntervalKey, + }); + const seconds = Math.max( + 1, + Math.round(durationToMilliseconds(duration) / 1000), + ); + + return createRandomProcessingInterval({ + minSeconds: seconds, + maxSeconds: seconds * 1.5, + }); + } } diff --git a/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.test.ts b/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.test.ts index dfcfd58c7e..e11656681a 100644 --- a/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.test.ts +++ b/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.test.ts @@ -28,11 +28,11 @@ describe('parseQueryEntitiesParams', () => { const validRequest = { authorizationToken: 'to_not_be_returned', fields: ['kind'], - limit: '3', + limit: 3, filter: ['a=1', 'b=2'], orderField: ['metadata.name,desc'], fullTextFilterTerm: 'query', - fullTextFilterFields: 'metadata.name,metadata.namespace', + fullTextFilterFields: ['metadata.name', 'metadata.namespace'], }; const parsedObj = parseQueryEntitiesParams( validRequest, @@ -65,9 +65,7 @@ describe('parseQueryEntitiesParams', () => { { filter: 3 }, { orderField: ['metadata.uid,diagonal'] }, { fields: [4] }, - { fullTextFilterTerm: [] }, - { fullTextFilterFields: 3 }, - ])('should throw if some parameter is not valid %p', params => { + ])('should throw if some parameter is not valid %p', (params: any) => { expect(() => parseQueryEntitiesParams(params)).toThrow(); }); }); @@ -83,7 +81,7 @@ describe('parseQueryEntitiesParams', () => { const validRequest = { authorizationToken: 'to_not_be_returned', fields: ['kind'], - limit: '3', + limit: 3, cursor: encodeCursor(cursor), }; const parsedObj = parseQueryEntitiesParams( @@ -103,10 +101,10 @@ describe('parseQueryEntitiesParams', () => { const validRequest = { authorizationToken: 'to_not_be_returned', fields: ['kind'], - limit: '3', + limit: 3, cursor: encodeCursor(cursor), filter: ['a=1', 'b=2'], - orderField: 'orderField,desc', + orderField: ['orderField,desc'], query: 'query', }; const parsedObj = parseQueryEntitiesParams( @@ -128,7 +126,7 @@ describe('parseQueryEntitiesParams', () => { it.each([{ cursor: [] }, { fields: [4] }])( 'should throw if some parameter is not valid %p', - params => { + (params: any) => { expect(() => parseQueryEntitiesParams(params)).toThrow(); }, ); diff --git a/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts b/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts index 9b55f193b9..b1d5e8dd1f 100644 --- a/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts +++ b/plugins/catalog-backend/src/service/request/parseQueryEntitiesParams.ts @@ -20,19 +20,19 @@ import { QueryEntitiesRequest, } from '../../catalog/types'; import { decodeCursor } from '../util'; -import { parseStringParam } from './common'; import { parseEntityFilterParams } from './parseEntityFilterParams'; import { parseEntityOrderFieldParams } from './parseEntityOrderFieldParams'; import { parseEntityTransformParams } from './parseEntityTransformParams'; -import { parseFullTextFilterFields } from './parseFullTextFilterFields'; +import { spec } from '../../schema/openapi.generated'; +import { internal } from '@backstage/backend-openapi-utils'; export function parseQueryEntitiesParams( - params: Record, + params: internal.QuerySchema, ): Omit { const fields = parseEntityTransformParams(params); - const cursor = parseStringParam(params.cursor, 'cursor'); - if (cursor) { - const decodedCursor = decodeCursor(cursor); + + if (params.cursor) { + const decodedCursor = decodeCursor(params.cursor); const response: Omit = { cursor: decodedCursor, fields, @@ -41,12 +41,6 @@ export function parseQueryEntitiesParams( } const filter = parseEntityFilterParams(params); - const fullTextFilterTerm = parseStringParam( - params.fullTextFilterTerm, - 'fullTextFilterTerm', - ); - const fullTextFilterFields = parseFullTextFilterFields(params); - const orderFields = parseEntityOrderFieldParams(params); const response: Omit = { @@ -54,8 +48,8 @@ export function parseQueryEntitiesParams( filter, orderFields, fullTextFilter: { - term: fullTextFilterTerm || '', - fields: fullTextFilterFields, + term: params.fullTextFilterTerm || '', + fields: params.fullTextFilterFields, }, }; diff --git a/plugins/catalog-backend/src/tests/performance/lib/catalogModuleSyntheticLoadEntities.ts b/plugins/catalog-backend/src/tests/performance/lib/catalogModuleSyntheticLoadEntities.ts index 4d38b28bcf..7925b0b552 100644 --- a/plugins/catalog-backend/src/tests/performance/lib/catalogModuleSyntheticLoadEntities.ts +++ b/plugins/catalog-backend/src/tests/performance/lib/catalogModuleSyntheticLoadEntities.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { createBackendModule } from '@backstage/backend-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { LocationSpec } from '@backstage/plugin-catalog-common'; import { @@ -26,7 +25,6 @@ import { EntityProviderConnection, processingResult, } from '@backstage/plugin-catalog-node'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; /** * Options for a fixed initial load of entities. @@ -143,12 +141,15 @@ export const common = { /** * The entity provider that drives the initial base entity injection + * @internal */ -class SyntheticLoadEntitiesProvider implements EntityProvider { +export class SyntheticLoadEntitiesProvider implements EntityProvider { constructor( private readonly load: SyntheticLoadOptions, private readonly events: SyntheticLoadEvents, - ) {} + ) { + validateSyntheticLoadOptions(load); + } getProviderName(): string { return 'SyntheticLoadEntitiesProvider'; @@ -180,9 +181,12 @@ class SyntheticLoadEntitiesProvider implements EntityProvider { /** * Supporting processor for emitting children and relations + * @internal */ -class SyntheticLoadEntitiesProcessor implements CatalogProcessor { - constructor(private readonly load: SyntheticLoadOptions) {} +export class SyntheticLoadEntitiesProcessor implements CatalogProcessor { + constructor(private readonly load: SyntheticLoadOptions) { + validateSyntheticLoadOptions(load); + } getProcessorName(): string { return 'SyntheticLoadEntitiesProcessor'; @@ -231,27 +235,3 @@ class SyntheticLoadEntitiesProcessor implements CatalogProcessor { return entity; } } - -export const catalogModuleSyntheticLoadEntities = createBackendModule( - (options: { load: SyntheticLoadOptions; events?: SyntheticLoadEvents }) => ({ - moduleId: 'syntheticLoadEntities', - pluginId: 'catalog', - register(reg) { - reg.registerInit({ - deps: { - catalog: catalogProcessingExtensionPoint, - }, - async init({ catalog }) { - const { load, events = {} } = options; - - validateSyntheticLoadOptions(load); - const provider = new SyntheticLoadEntitiesProvider(load, events); - const processor = new SyntheticLoadEntitiesProcessor(load); - - catalog.addEntityProvider(provider); - catalog.addProcessor(processor); - }, - }); - }, - }), -); diff --git a/plugins/catalog-backend/src/tests/performance/stitchingPerformance.test.ts b/plugins/catalog-backend/src/tests/performance/stitchingPerformance.test.ts index 2f32fcd907..e60c0a33d6 100644 --- a/plugins/catalog-backend/src/tests/performance/stitchingPerformance.test.ts +++ b/plugins/catalog-backend/src/tests/performance/stitchingPerformance.test.ts @@ -16,16 +16,18 @@ import { coreServices, + createBackendModule, createServiceFactory, } from '@backstage/backend-plugin-api'; import { TestDatabases, startTestBackend } from '@backstage/backend-test-utils'; -import { catalogPlugin } from '@backstage/plugin-catalog-backend/alpha'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Knex } from 'knex'; import { applyDatabaseMigrations } from '../../database/migrations'; import { SyntheticLoadEvents, SyntheticLoadOptions, - catalogModuleSyntheticLoadEntities, + SyntheticLoadEntitiesProvider, + SyntheticLoadEntitiesProcessor, } from './lib/catalogModuleSyntheticLoadEntities'; import { describePerformanceTest, performanceTraceEnabled } from './lib/env'; @@ -177,13 +179,28 @@ describePerformanceTest('stitchingPerformance', () => { const tracker = new Tracker(knex, load); const backend = await startTestBackend({ - services: [staticDatabase(knex)], features: [ - catalogPlugin(), - catalogModuleSyntheticLoadEntities({ - load, - events: tracker.events(), + import('@backstage/plugin-catalog-backend/alpha'), + createBackendModule({ + moduleId: 'syntheticLoadEntities', + pluginId: 'catalog', + register(reg) { + reg.registerInit({ + deps: { + catalog: catalogProcessingExtensionPoint, + }, + async init({ catalog }) { + catalog.addEntityProvider( + new SyntheticLoadEntitiesProvider(load, tracker.events()), + ); + catalog.addProcessor( + new SyntheticLoadEntitiesProcessor(load), + ); + }, + }); + }, }), + staticDatabase(knex), ], }); diff --git a/plugins/catalog-common/CHANGELOG.md b/plugins/catalog-common/CHANGELOG.md index 8fa7f6e886..8de4b1a4e1 100644 --- a/plugins/catalog-common/CHANGELOG.md +++ b/plugins/catalog-common/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-common +## 1.0.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## 1.0.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-search-common@1.2.6-next.0 + ## 1.0.15 ### Patch Changes diff --git a/plugins/catalog-common/catalog-info.yaml b/plugins/catalog-common/catalog-info.yaml new file mode 100644 index 0000000000..85ee85917b --- /dev/null +++ b/plugins/catalog-common/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-common + title: '@backstage/plugin-catalog-common' + description: Common functionalities for the catalog plugin +spec: + lifecycle: production + type: backstage-common-library + owner: catalog-maintainers diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json index b4a2b75420..8c4955d7f1 100644 --- a/plugins/catalog-common/package.json +++ b/plugins/catalog-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-common", "description": "Common functionalities for the catalog plugin", - "version": "1.0.15", + "version": "1.0.16-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,6 +35,7 @@ "keywords": [ "backstage" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/plugins/catalog-customized/CHANGELOG.md b/plugins/catalog-customized/CHANGELOG.md index a488e91148..ef9df78bb8 100644 --- a/plugins/catalog-customized/CHANGELOG.md +++ b/plugins/catalog-customized/CHANGELOG.md @@ -1,5 +1,45 @@ # @internal/plugin-catalog-customized +## 0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.13.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + +## 0.0.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/plugin-catalog@1.12.5-next.1 + +## 0.0.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.12.4-next.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.12.1 + - @backstage/plugin-catalog-react@1.8.1 + +## 0.0.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/plugin-catalog@1.12.1-next.2 + ## 0.0.13-next.1 ### Patch Changes diff --git a/plugins/catalog-customized/catalog-info.yaml b/plugins/catalog-customized/catalog-info.yaml new file mode 100644 index 0000000000..1a6d0e0a00 --- /dev/null +++ b/plugins/catalog-customized/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: internal-plugin-catalog-customized + title: '@internal/plugin-catalog-customized' + description: >- + The internal Backstage Customizable plugin for browsing the Backstage + catalog +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: catalog-maintainers diff --git a/plugins/catalog-customized/package.json b/plugins/catalog-customized/package.json index c849e6c2a3..3628844481 100644 --- a/plugins/catalog-customized/package.json +++ b/plugins/catalog-customized/package.json @@ -1,7 +1,7 @@ { "name": "@internal/plugin-catalog-customized", "description": "The internal Backstage Customizable plugin for browsing the Backstage catalog", - "version": "0.0.13-next.1", + "version": "0.0.14-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,6 +23,7 @@ "keywords": [ "backstage" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index 118600f318..80572558eb 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,68 @@ # @backstage/plugin-catalog-graph +## 0.2.36-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## 0.2.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## 0.2.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.2.33 + +### Patch Changes + +- 64ee2c0c7ca5: Propagate entity spec to EntityNode so that spec info such as type can be used for graph node customization +- 62dc7a2b1ad1: Added maximum depth parameter to the catalogGraphParams in CatalogGraphCard. +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## 0.2.33-next.2 + +### Patch Changes + +- 62dc7a2b1ad1: Added maximum depth parameter to the catalogGraphParams in CatalogGraphCard. +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.2.33-next.1 ### Patch Changes diff --git a/plugins/catalog-graph/api-report.md b/plugins/catalog-graph/api-report.md index 80578db6ed..ce56245c49 100644 --- a/plugins/catalog-graph/api-report.md +++ b/plugins/catalog-graph/api-report.md @@ -11,8 +11,10 @@ import { DependencyGraphTypes } from '@backstage/core-components'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; import { JsonObject } from '@backstage/types'; +import { JSX as JSX_2 } from 'react'; import { MouseEvent as MouseEvent_2 } from 'react'; import { MouseEventHandler } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public @@ -35,7 +37,7 @@ export const CatalogGraphPage: ( } | undefined; } & Partial, -) => JSX.Element; +) => JSX_2.Element; // @public export const catalogGraphPlugin: BackstagePlugin< @@ -73,7 +75,7 @@ export const EntityCatalogGraphCard: ( height?: number | undefined; title?: string | undefined; }, -) => JSX.Element; +) => JSX_2.Element; // @public export type EntityEdge = DependencyGraphTypes.DependencyEdge; @@ -102,7 +104,7 @@ export type EntityNodeData = { // @public export const EntityRelationsGraph: ( props: EntityRelationsGraphProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type EntityRelationsGraphProps = { diff --git a/plugins/catalog-graph/catalog-info.yaml b/plugins/catalog-graph/catalog-info.yaml new file mode 100644 index 0000000000..6dda99634b --- /dev/null +++ b/plugins/catalog-graph/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-graph + title: '@backstage/plugin-catalog-graph' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: sda-se-reviewers diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 0e8f891dab..5c339c2d35 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.2.33-next.1", + "version": "0.2.36-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,6 +18,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/catalog-graph" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx index adee03e298..cdd55e300f 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx @@ -133,7 +133,7 @@ describe('', () => { expect(button).toBeInTheDocument(); expect(button.closest('a')).toHaveAttribute( 'href', - '/catalog-graph?rootEntityRefs%5B%5D=b%3Ad%2Fc&unidirectional=true&mergeRelations=true&direction=LR', + '/catalog-graph?rootEntityRefs%5B%5D=b%3Ad%2Fc&maxDepth=1&unidirectional=true&mergeRelations=true&direction=LR', ); }); @@ -157,7 +157,7 @@ describe('', () => { expect(button).toBeInTheDocument(); expect(button.closest('a')).toHaveAttribute( 'href', - '/catalog-graph?rootEntityRefs%5B%5D=b%3Ad%2Fc&unidirectional=true&mergeRelations=false&direction=LR', + '/catalog-graph?rootEntityRefs%5B%5D=b%3Ad%2Fc&maxDepth=2&unidirectional=true&mergeRelations=false&direction=LR', ); }); diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx index ae4ad850a4..7976fa66bb 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx @@ -108,6 +108,7 @@ export const CatalogGraphCard = ( const catalogGraphParams = qs.stringify( { rootEntityRefs: [stringifyEntityRef(entity)], + maxDepth: maxDepth, unidirectional, mergeRelations, selectedKinds: kinds, diff --git a/plugins/catalog-graphql/CHANGELOG.md b/plugins/catalog-graphql/CHANGELOG.md index 0ce6148bc2..6726bf5126 100644 --- a/plugins/catalog-graphql/CHANGELOG.md +++ b/plugins/catalog-graphql/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-graphql +## 0.3.23-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/types@1.1.0 + +## 0.3.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/types@1.1.0 + ## 0.3.22 ### Patch Changes diff --git a/plugins/catalog-graphql/catalog-info.yaml b/plugins/catalog-graphql/catalog-info.yaml new file mode 100644 index 0000000000..202acc8c95 --- /dev/null +++ b/plugins/catalog-graphql/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-graphql + title: '@backstage/plugin-catalog-graphql' + description: An experimental Backstage catalog GraphQL module +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: catalog-maintainers diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index 0b4bebc0f7..3daf62470a 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-graphql", "description": "An experimental Backstage catalog GraphQL module", - "version": "0.3.22", + "version": "0.3.23-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 139c8b0615..77ea442018 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,77 @@ # @backstage/plugin-catalog-import +## 0.9.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.19-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + +## 0.9.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + +## 0.9.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.9.11 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.16 + - @backstage/integration@1.6.0 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.15 + +## 0.9.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/integration-react@1.1.16-next.1 + ## 0.9.11-next.1 ### Patch Changes diff --git a/plugins/catalog-import/api-report.md b/plugins/catalog-import/api-report.md index b4c8e854aa..f507967d37 100644 --- a/plugins/catalog-import/api-report.md +++ b/plugins/catalog-import/api-report.md @@ -16,6 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { FieldErrors } from 'react-hook-form'; import { IdentityApi } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmAuthApi } from '@backstage/integration-react'; @@ -46,7 +47,7 @@ export type AnalyzeResult = // @public export const AutocompleteTextField: ( props: AutocompleteTextFieldProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface AutocompleteTextFieldProps { @@ -126,7 +127,7 @@ export class CatalogImportClient implements CatalogImportApi { } // @public -export const CatalogImportPage: () => JSX.Element; +export const CatalogImportPage: () => JSX_2.Element; // @public const catalogImportPlugin: BackstagePlugin< @@ -148,12 +149,12 @@ export function defaultGenerateStepper( ): StepperProvider; // @public -export const DefaultImportPage: () => JSX.Element; +export const DefaultImportPage: () => React_2.JSX.Element; // @public export const EntityListComponent: ( props: EntityListComponentProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface EntityListComponentProps { @@ -182,7 +183,9 @@ export type ImportFlows = | 'no-location'; // @public -export const ImportInfoCard: (props: ImportInfoCardProps) => JSX.Element; +export const ImportInfoCard: ( + props: ImportInfoCardProps, +) => React_2.JSX.Element; // @public export interface ImportInfoCardProps { @@ -205,7 +208,7 @@ export type ImportState = State & { }; // @public -export const ImportStepper: (props: ImportStepperProps) => JSX.Element; +export const ImportStepper: (props: ImportStepperProps) => React_2.JSX.Element; // @public export interface ImportStepperProps { @@ -223,7 +226,7 @@ export interface ImportStepperProps { // @public export const PreparePullRequestForm: >( props: PreparePullRequestFormProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type PreparePullRequestFormProps< @@ -266,7 +269,7 @@ export type PrepareResult = // @public export const PreviewCatalogInfoComponent: ( props: PreviewCatalogInfoComponentProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface PreviewCatalogInfoComponentProps { @@ -284,7 +287,7 @@ export interface PreviewCatalogInfoComponentProps { // @public export const PreviewPullRequestComponent: ( props: PreviewPullRequestComponentProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface PreviewPullRequestComponentProps { @@ -302,7 +305,7 @@ export interface PreviewPullRequestComponentProps { // @public export const StepInitAnalyzeUrl: ( props: StepInitAnalyzeUrlProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface StepInitAnalyzeUrlProps { @@ -326,7 +329,7 @@ export interface StepInitAnalyzeUrlProps { // @public export const StepPrepareCreatePullRequest: ( props: StepPrepareCreatePullRequestProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface StepPrepareCreatePullRequestProps { diff --git a/plugins/catalog-import/catalog-info.yaml b/plugins/catalog-import/catalog-info.yaml new file mode 100644 index 0000000000..763dec9c45 --- /dev/null +++ b/plugins/catalog-import/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-import + title: '@backstage/plugin-catalog-import' + description: A Backstage plugin the helps you import entities into your catalog +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: catalog-maintainers diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 55d596c955..2cdc0fc786 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-import", "description": "A Backstage plugin the helps you import entities into your catalog", - "version": "0.9.11-next.1", + "version": "0.9.14-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,6 +22,7 @@ "keywords": [ "backstage" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", diff --git a/plugins/catalog-node/CHANGELOG.md b/plugins/catalog-node/CHANGELOG.md index 6299cd2424..74e0208707 100644 --- a/plugins/catalog-node/CHANGELOG.md +++ b/plugins/catalog-node/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-catalog-node +## 1.4.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + +## 1.4.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + +## 1.4.1 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + +## 1.4.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + ## 1.4.1-next.1 ### Patch Changes diff --git a/plugins/catalog-node/catalog-info.yaml b/plugins/catalog-node/catalog-info.yaml new file mode 100644 index 0000000000..0dd6686eba --- /dev/null +++ b/plugins/catalog-node/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-node + title: '@backstage/plugin-catalog-node' + description: The plugin-catalog-node module for @backstage/plugin-catalog-backend +spec: + lifecycle: production + type: backstage-node-library + owner: catalog-maintainers diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index 5a10c9e000..627ed88823 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-node", "description": "The plugin-catalog-node module for @backstage/plugin-catalog-backend", - "version": "1.4.1-next.1", + "version": "1.4.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 6ae43f297c..9970fc1754 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,86 @@ # @backstage/plugin-catalog-react +## 1.8.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-permission-react@0.4.15-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-catalog-common@1.0.16-next.1 + +## 1.8.4-next.1 + +### Patch Changes + +- bd817209ddd7: Export the `EntityAutocompletePicker` component. +- 7029ba1ce0af: Added delete alert popup when user delete the entity +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/integration@1.7.0-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-react@0.4.15-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## 1.8.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.15-next.0 + +## 1.8.1 + +### Patch Changes + +- aa3feedce10a: Allow specifying screen size when catalog filters are hidden in drawer +- Updated dependencies + - @backstage/integration@1.6.0 + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.14 + +## 1.8.1-next.1 + +### Patch Changes + +- aa3feedce10a: Allow specifying screen size when catalog filters are hidden in drawer + ## 1.8.1-next.0 ### Patch Changes diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 9bf87778dd..a72df6bb3d 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -3,8 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { ApiRef } from '@backstage/core-plugin-api'; import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; import { CatalogApi } from '@backstage/catalog-client'; @@ -26,11 +24,21 @@ import { StyleRules } from '@material-ui/core/styles/withStyles'; import { SystemEntity } from '@backstage/catalog-model'; import { TableColumn } from '@backstage/core-components'; import { TableOptions } from '@backstage/core-components'; +import { TextFieldProps } from '@material-ui/core'; + +// @public (undocumented) +export type AllowedEntityFilters = { + [K in keyof T]-?: NonNullable extends EntityFilter & { + values: string[]; + } + ? K + : never; +}[keyof T]; // @public export const AsyncEntityProvider: ( props: AsyncEntityProviderProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface AsyncEntityProviderProps { @@ -62,9 +70,15 @@ export const catalogApiRef: ApiRef; // @public (undocumented) export const CatalogFilterLayout: { - (props: { children: React_2.ReactNode }): JSX.Element; - Filters: (props: { children: React_2.ReactNode }) => JSX.Element; - Content: (props: { children: React_2.ReactNode }) => JSX.Element; + (props: { children: React_2.ReactNode }): React_2.JSX.Element; + Filters: (props: { + children: React_2.ReactNode; + options?: { + drawerBreakpoint?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | number; + drawerAnchor?: 'left' | 'right' | 'top' | 'bottom'; + }; + }) => React_2.JSX.Element; + Content: (props: { children: React_2.ReactNode }) => React_2.JSX.Element; }; // @public (undocumented) @@ -138,6 +152,28 @@ export type DefaultEntityFilters = { namespace?: EntityNamespaceFilter; }; +// @public (undocumented) +export function EntityAutocompletePicker< + T extends DefaultEntityFilters = DefaultEntityFilters, + Name extends AllowedEntityFilters = AllowedEntityFilters, +>(props: EntityAutocompletePickerProps): React_2.JSX.Element | null; + +// @public (undocumented) +export type EntityAutocompletePickerProps< + T extends DefaultEntityFilters = DefaultEntityFilters, + Name extends AllowedEntityFilters = AllowedEntityFilters, +> = { + label: string; + name: Name; + path: string; + showCounts?: boolean; + Filter: { + new (values: string[]): NonNullable; + }; + InputProps?: TextFieldProps; + initialSelectedOptions?: string[]; +}; + // @public export class EntityErrorFilter implements EntityFilter { constructor(value: boolean); @@ -171,7 +207,7 @@ export class EntityKindFilter implements EntityFilter { // @public (undocumented) export const EntityKindPicker: ( props: EntityKindPickerProps, -) => JSX.Element | null; +) => React_2.JSX.Element | null; // @public export interface EntityKindPickerProps { @@ -196,7 +232,7 @@ export class EntityLifecycleFilter implements EntityFilter { // @public (undocumented) export const EntityLifecyclePicker: (props: { initialFilter?: string[]; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public export const EntityListContext: React_2.Context< @@ -223,7 +259,7 @@ export type EntityListContextProps< // @public export const EntityListProvider: ( props: PropsWithChildren<{}>, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type EntityLoadingStatus = { @@ -245,7 +281,7 @@ export class EntityNamespaceFilter implements EntityFilter { } // @public (undocumented) -export const EntityNamespacePicker: () => JSX.Element; +export const EntityNamespacePicker: () => React_2.JSX.Element; // @public export class EntityOrphanFilter implements EntityFilter { @@ -269,7 +305,7 @@ export class EntityOwnerFilter implements EntityFilter { // @public (undocumented) export const EntityOwnerPicker: ( props?: EntityOwnerPickerProps, -) => JSX.Element | null; +) => React_2.JSX.Element | null; // @public (undocumented) export type EntityOwnerPickerProps = { @@ -279,7 +315,7 @@ export type EntityOwnerPickerProps = { // @public export const EntityPeekAheadPopover: ( props: EntityPeekAheadPopoverProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type EntityPeekAheadPopoverProps = PropsWithChildren<{ @@ -288,10 +324,12 @@ export type EntityPeekAheadPopoverProps = PropsWithChildren<{ }>; // @public (undocumented) -export const EntityProcessingStatusPicker: () => JSX.Element; +export const EntityProcessingStatusPicker: () => React_2.JSX.Element; // @public -export const EntityProvider: (props: EntityProviderProps) => JSX.Element; +export const EntityProvider: ( + props: EntityProviderProps, +) => React_2.JSX.Element; // @public export interface EntityProviderProps { @@ -315,7 +353,7 @@ export type EntityRefLinkProps = { // @public export function EntityRefLinks< TRef extends string | CompoundEntityRef | Entity, ->(props: EntityRefLinksProps): JSX.Element; +>(props: EntityRefLinksProps): React_2.JSX.Element; // @public export type EntityRefLinksProps< @@ -351,7 +389,7 @@ export const entityRouteRef: RouteRef<{ }>; // @public -export const EntitySearchBar: () => JSX.Element; +export const EntitySearchBar: () => React_2.JSX.Element; // @public (undocumented) export type EntitySourceLocation = { @@ -361,7 +399,7 @@ export type EntitySourceLocation = { // @public export const EntityTable: { - (props: EntityTableProps): JSX.Element; + (props: EntityTableProps): React_2.JSX.Element; columns: Readonly<{ createEntityRefColumn(options: { defaultKind?: string | undefined; @@ -415,7 +453,9 @@ export class EntityTagFilter implements EntityFilter { } // @public (undocumented) -export const EntityTagPicker: (props: EntityTagPickerProps) => JSX.Element; +export const EntityTagPicker: ( + props: EntityTagPickerProps, +) => React_2.JSX.Element; // @public (undocumented) export type EntityTagPickerProps = { @@ -447,7 +487,7 @@ export class EntityTypeFilter implements EntityFilter { // @public (undocumented) export const EntityTypePicker: ( props: EntityTypePickerProps, -) => JSX.Element | null; +) => React_2.JSX.Element | null; // @public export interface EntityTypePickerProps { @@ -458,7 +498,9 @@ export interface EntityTypePickerProps { } // @public -export const FavoriteEntity: (props: FavoriteEntityProps) => JSX.Element; +export const FavoriteEntity: ( + props: FavoriteEntityProps, +) => React_2.JSX.Element; // @public (undocumented) export type FavoriteEntityProps = ComponentProps & { @@ -494,7 +536,7 @@ export function InspectEntityDialog(props: { open: boolean; entity: Entity; onClose: () => void; -}): JSX.Element | null; +}): React_2.JSX.Element | null; // @public (undocumented) export function MockEntityListContextProvider< @@ -503,7 +545,7 @@ export function MockEntityListContextProvider< props: PropsWithChildren<{ value?: Partial>; }>, -): JSX.Element; +): React_2.JSX.Element; // @public export class MockStarredEntitiesApi implements StarredEntitiesApi { @@ -525,7 +567,7 @@ export const starredEntitiesApiRef: ApiRef; // @public (undocumented) export const UnregisterEntityDialog: ( props: UnregisterEntityDialogProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type UnregisterEntityDialogProps = { @@ -601,7 +643,9 @@ export class UserListFilter implements EntityFilter { export type UserListFilterKind = 'owned' | 'starred' | 'all'; // @public (undocumented) -export const UserListPicker: (props: UserListPickerProps) => JSX.Element; +export const UserListPicker: ( + props: UserListPickerProps, +) => React_2.JSX.Element; // @public (undocumented) export type UserListPickerProps = { diff --git a/plugins/catalog-react/catalog-info.yaml b/plugins/catalog-react/catalog-info.yaml new file mode 100644 index 0000000000..0fcfcbfb8e --- /dev/null +++ b/plugins/catalog-react/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-react + title: '@backstage/plugin-catalog-react' + description: >- + A frontend library that helps other Backstage plugins interact with the + catalog +spec: + lifecycle: production + type: backstage-web-library + owner: catalog-maintainers diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 67a3409830..f1858b024d 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-react", "description": "A frontend library that helps other Backstage plugins interact with the catalog", - "version": "1.8.1-next.0", + "version": "1.8.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,6 +35,7 @@ "keywords": [ "backstage" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/plugins/catalog-react/src/components/CatalogFilterLayout/CatalogFilterLayout.tsx b/plugins/catalog-react/src/components/CatalogFilterLayout/CatalogFilterLayout.tsx index 751eb35f1d..1a1404edf2 100644 --- a/plugins/catalog-react/src/components/CatalogFilterLayout/CatalogFilterLayout.tsx +++ b/plugins/catalog-react/src/components/CatalogFilterLayout/CatalogFilterLayout.tsx @@ -28,14 +28,20 @@ import FilterListIcon from '@material-ui/icons/FilterList'; import { BackstageTheme } from '@backstage/theme'; /** @public */ -export const Filters = (props: { children: React.ReactNode }) => { - const isMidSizeScreen = useMediaQuery(theme => - theme.breakpoints.down('md'), +export const Filters = (props: { + children: React.ReactNode; + options?: { + drawerBreakpoint?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | number; + drawerAnchor?: 'left' | 'right' | 'top' | 'bottom'; + }; +}) => { + const isScreenSmallerThanBreakpoint = useMediaQuery(theme => + theme.breakpoints.down(props.options?.drawerBreakpoint ?? 'md'), ); const theme = useTheme(); const [filterDrawerOpen, setFilterDrawerOpen] = useState(false); - return isMidSizeScreen ? ( + return isScreenSmallerThanBreakpoint ? ( <>
); diff --git a/plugins/home/src/components/CustomHomepage/index.ts b/plugins/home/src/components/CustomHomepage/index.ts index eed597cca3..380fdb4b25 100644 --- a/plugins/home/src/components/CustomHomepage/index.ts +++ b/plugins/home/src/components/CustomHomepage/index.ts @@ -14,5 +14,8 @@ * limitations under the License. */ export { CustomHomepageGrid } from './CustomHomepageGrid'; -export type { CustomHomepageGridProps, Breakpoint } from './CustomHomepageGrid'; -export type { LayoutConfiguration } from './types'; +export type { + CustomHomepageGridProps, + Breakpoint, + LayoutConfiguration, +} from './types'; diff --git a/plugins/home/src/components/CustomHomepage/types.ts b/plugins/home/src/components/CustomHomepage/types.ts index 8e9c6f403e..79e94c5f03 100644 --- a/plugins/home/src/components/CustomHomepage/types.ts +++ b/plugins/home/src/components/CustomHomepage/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ReactElement } from 'react'; +import React, { ReactElement, ReactNode } from 'react'; import { Layout } from 'react-grid-layout'; import { z } from 'zod'; import { RJSFSchema, UiSchema } from '@rjsf/utils'; @@ -24,12 +24,91 @@ const RSJFTypeUiSchema: z.ZodType = z.any(); const ReactElementSchema: z.ZodType = z.any(); const LayoutSchema: z.ZodType = z.any(); +/** + * Breakpoint options for + * + * @public + */ +export type Breakpoint = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + +/** + * Props customizing the component. + * + * @public + */ +export type CustomHomepageGridProps = { + /** + * Children contain all widgets user can configure on their own homepage. + */ + children?: ReactNode; + /** + * Default layout for the homepage before users have modified it. + */ + config?: LayoutConfiguration[]; + /** + * Height of grid row in pixels. + * @defaultValue 60 + */ + rowHeight?: number; + /** + * Screen width in pixels for different breakpoints. + * @defaultValue theme breakpoints + */ + breakpoints?: Record; + /** + * Number of grid columns for different breakpoints. + * @defaultValue \{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 \} + */ + cols?: Record; + /** + * Grid container padding (x, y) in pixels for all or specific breakpoints. + * @defaultValue [0, 0] + * @example [10, 10] + * @example \{ lg: [10, 10] \} + */ + containerPadding?: [number, number] | Record; + /** + * Grid container margin (x, y) in pixels for all or specific breakpoints. + * @defaultValue [0, 0] + * @example [10, 10] + * @example \{ lg: [10, 10] \} + */ + containerMargin?: [number, number] | Record; + /** + * Maximum number of rows user can have in the grid. + * @defaultValue unlimited + */ + maxRows?: number; + /** + * Custom style for grid. + */ + style?: React.CSSProperties; + /** + * Compaction type of widgets in the grid. This controls where widgets are moved in case + * they are overlapping in the grid. + */ + compactType?: 'vertical' | 'horizontal' | null; + /** + * Controls if widgets can overlap in the grid. If true, grid can be placed one over the other. + * @defaultValue false + */ + allowOverlap?: boolean; + /** + * Controls if widgets can collide with each other. If true, grid items won't change position when being dragged over. + * @defaultValue false + */ + preventCollision?: boolean; +}; + export const LayoutConfigurationSchema = z.object({ component: ReactElementSchema, x: z.number().nonnegative('x must be positive number'), y: z.number().nonnegative('y must be positive number'), width: z.number().positive('width must be positive number'), height: z.number().positive('height must be positive number'), + movable: z.boolean().optional(), + deletable: z.boolean().optional(), + resizable: z.boolean().optional(), }); /** @@ -43,6 +122,9 @@ export type LayoutConfiguration = { y: number; width: number; height: number; + movable?: boolean; + deletable?: boolean; + resizable?: boolean; }; export const WidgetSchema = z.object({ @@ -64,6 +146,9 @@ export const WidgetSchema = z.object({ .optional(), settingsSchema: RSJFTypeSchema.optional(), uiSchema: RSJFTypeUiSchema.optional(), + movable: z.boolean().optional(), + deletable: z.boolean().optional(), + resizable: z.boolean().optional(), }); export type Widget = z.infer; @@ -72,6 +157,9 @@ const GridWidgetSchema = z.object({ id: z.string(), layout: LayoutSchema, settings: z.record(z.string(), z.any()), + movable: z.boolean().optional(), + deletable: z.boolean().optional(), + resizable: z.boolean().optional(), }); export type GridWidget = z.infer; diff --git a/plugins/ilert/CHANGELOG.md b/plugins/ilert/CHANGELOG.md index 5a30c11db7..e273244b70 100644 --- a/plugins/ilert/CHANGELOG.md +++ b/plugins/ilert/CHANGELOG.md @@ -1,5 +1,64 @@ # @backstage/plugin-ilert +## 0.2.13-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- 814feeed7343: Update to handle invalid luxon values +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.2.12-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.2.10 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.2.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.2.10-next.1 ### Patch Changes diff --git a/plugins/ilert/api-report.md b/plugins/ilert/api-report.md index 6cc0c12a8f..b2081e049e 100644 --- a/plugins/ilert/api-report.md +++ b/plugins/ilert/api-report.md @@ -11,6 +11,8 @@ import { ConfigApi } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { IconComponent } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -302,7 +304,7 @@ export type AlertStatus = typeof PENDING | typeof ACCEPTED | typeof RESOLVED; export const DEGRADED = 'DEGRADED'; // @public (undocumented) -export const EntityILertCard: () => JSX.Element; +export const EntityILertCard: () => JSX_2.Element; // @public (undocumented) export interface EscalationPolicy { @@ -442,7 +444,7 @@ export interface ILertApi { export const ilertApiRef: ApiRef; // @public (undocumented) -export const ILertCard: () => JSX.Element; +export const ILertCard: () => React_2.JSX.Element; // @public (undocumented) export class ILertClient implements ILertApi { @@ -530,7 +532,7 @@ export class ILertClient implements ILertApi { export const ILertIcon: IconComponent; // @public (undocumented) -export const ILertPage: () => JSX.Element; +export const ILertPage: () => JSX_2.Element; // @public (undocumented) const ilertPlugin: BackstagePlugin< @@ -649,7 +651,7 @@ export interface Responder { } // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; // @public (undocumented) export interface Schedule { diff --git a/plugins/ilert/catalog-info.yaml b/plugins/ilert/catalog-info.yaml new file mode 100644 index 0000000000..a73c82cbfe --- /dev/null +++ b/plugins/ilert/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-ilert + title: '@backstage/plugin-ilert' + description: A Backstage plugin that integrates towards iLert +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 1ff95f665d..4562774da1 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-ilert", "description": "A Backstage plugin that integrates towards iLert", - "version": "0.2.10-next.1", + "version": "0.2.13-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,6 +19,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/ilert" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -40,6 +41,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@material-ui/pickers": "^3.3.10", + "@types/react": "^16.13.1 || ^17.0.0", "humanize-duration": "^3.26.0", "luxon": "^3.0.0", "prop-types": "^15.7.2", @@ -59,8 +61,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx b/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx index 618c35dbc3..63af27c725 100644 --- a/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx +++ b/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; import Button from '@material-ui/core/Button'; @@ -161,7 +162,7 @@ export const ShiftOverrideModal = ({ value={start} className={classes.formControl} onChange={date => { - setStart(date ? date.toISO() : ''); + setStart(date?.toISO() ?? ''); }} /> { - setEnd(date ? date.toISO() : ''); + setEnd(date?.toISO() ?? ''); }} /> diff --git a/plugins/jenkins-backend/CHANGELOG.md b/plugins/jenkins-backend/CHANGELOG.md index d03a2646df..3d1b1f3556 100644 --- a/plugins/jenkins-backend/CHANGELOG.md +++ b/plugins/jenkins-backend/CHANGELOG.md @@ -1,5 +1,76 @@ # @backstage/plugin-jenkins-backend +## 0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-jenkins-common@0.1.19-next.1 + +## 0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + - @backstage/plugin-jenkins-common@0.1.19-next.0 + +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-jenkins-common@0.1.18 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + +## 0.2.3 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-jenkins-common@0.1.18 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + +## 0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + ## 0.2.3-next.1 ### Patch Changes diff --git a/plugins/jenkins-backend/catalog-info.yaml b/plugins/jenkins-backend/catalog-info.yaml new file mode 100644 index 0000000000..d3345d60dd --- /dev/null +++ b/plugins/jenkins-backend/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-jenkins-backend + title: '@backstage/plugin-jenkins-backend' + description: A Backstage backend plugin that integrates towards Jenkins +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: maintainers diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index 942a623ad0..9ea6a51cb8 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins-backend", "description": "A Backstage backend plugin that integrates towards Jenkins", - "version": "0.2.3-next.1", + "version": "0.2.6-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/jenkins-common/CHANGELOG.md b/plugins/jenkins-common/CHANGELOG.md index b7c7da027b..95db160a9f 100644 --- a/plugins/jenkins-common/CHANGELOG.md +++ b/plugins/jenkins-common/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-jenkins-common +## 0.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + +## 0.1.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + +## 0.1.18 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + ## 0.1.18-next.0 ### Patch Changes diff --git a/plugins/jenkins-common/catalog-info.yaml b/plugins/jenkins-common/catalog-info.yaml new file mode 100644 index 0000000000..1fbf7971f1 --- /dev/null +++ b/plugins/jenkins-common/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-jenkins-common + title: '@backstage/plugin-jenkins-common' +spec: + lifecycle: experimental + type: backstage-common-library + owner: maintainers diff --git a/plugins/jenkins-common/package.json b/plugins/jenkins-common/package.json index 6d99a808bf..231e1cd73e 100644 --- a/plugins/jenkins-common/package.json +++ b/plugins/jenkins-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins-common", - "version": "0.1.18-next.0", + "version": "0.1.19-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,6 +19,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/jenkins-common" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index bb28eeeaf5..c8e5a45dce 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-jenkins +## 0.8.6-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-jenkins-common@0.1.19-next.1 + +## 0.8.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-jenkins-common@0.1.19-next.0 + +## 0.8.5-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-jenkins-common@0.1.18 + +## 0.8.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-jenkins-common@0.1.18 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.8.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.8.3-next.1 ### Patch Changes diff --git a/plugins/jenkins/api-report.md b/plugins/jenkins/api-report.md index a2ae16988b..0782f0abd2 100644 --- a/plugins/jenkins/api-report.md +++ b/plugins/jenkins/api-report.md @@ -12,16 +12,18 @@ import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { IdentityApi } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const EntityJenkinsContent: () => JSX.Element; +export const EntityJenkinsContent: () => JSX_2.Element; // @public (undocumented) export const EntityLatestJenkinsRunCard: (props: { branch: string; variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) const isJenkinsAvailable: (entity: Entity) => boolean; @@ -109,7 +111,7 @@ export { jenkinsPlugin as plugin }; export const LatestRunCard: (props: { branch: string; variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "LEGACY_JENKINS_ANNOTATION" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -119,5 +121,5 @@ export const LEGACY_JENKINS_ANNOTATION = 'jenkins.io/github-folder'; // Warning: (ae-missing-release-tag) "Router" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; ``` diff --git a/plugins/jenkins/catalog-info.yaml b/plugins/jenkins/catalog-info.yaml new file mode 100644 index 0000000000..097034481d --- /dev/null +++ b/plugins/jenkins/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-jenkins + title: '@backstage/plugin-jenkins' + description: A Backstage plugin that integrates towards Jenkins +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index a44861de23..fd2ab214d3 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins", "description": "A Backstage plugin that integrates towards Jenkins", - "version": "0.8.3-next.1", + "version": "0.8.6-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,6 +23,7 @@ "backstage", "jenkins" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -43,6 +44,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "react-use": "^17.2.4" }, @@ -60,8 +62,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "@types/testing-library__jest-dom": "^5.9.1", "cross-fetch": "^3.1.5", "msw": "^1.0.0" diff --git a/plugins/kafka-backend/CHANGELOG.md b/plugins/kafka-backend/CHANGELOG.md index 48291adcc1..89f8672d0e 100644 --- a/plugins/kafka-backend/CHANGELOG.md +++ b/plugins/kafka-backend/CHANGELOG.md @@ -1,5 +1,58 @@ # @backstage/plugin-kafka-backend +## 0.2.44-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + +## 0.2.44-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/errors@1.2.1 + +## 0.2.43-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## 0.2.41 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## 0.2.41-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + ## 0.2.41-next.1 ### Patch Changes diff --git a/plugins/kafka-backend/api-report.md b/plugins/kafka-backend/api-report.md index a748a543d9..b03bad12e5 100644 --- a/plugins/kafka-backend/api-report.md +++ b/plugins/kafka-backend/api-report.md @@ -12,7 +12,8 @@ import { Logger } from 'winston'; export function createRouter(options: RouterOptions): Promise; // @alpha -export const kafkaPlugin: () => BackendFeature; +const kafkaPlugin: () => BackendFeature; +export default kafkaPlugin; // @public (undocumented) export interface RouterOptions { diff --git a/plugins/kafka-backend/catalog-info.yaml b/plugins/kafka-backend/catalog-info.yaml new file mode 100644 index 0000000000..13dc9fd963 --- /dev/null +++ b/plugins/kafka-backend/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-kafka-backend + title: '@backstage/plugin-kafka-backend' + description: A Backstage backend plugin that integrates towards Kafka +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: andrewthauer diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index c3cf662208..f2f023332e 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka-backend", "description": "A Backstage backend plugin that integrates towards Kafka", - "version": "0.2.41-next.1", + "version": "0.2.44-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/kafka-backend/src/index.ts b/plugins/kafka-backend/src/index.ts index 2000c8b89a..d87d313941 100644 --- a/plugins/kafka-backend/src/index.ts +++ b/plugins/kafka-backend/src/index.ts @@ -22,4 +22,4 @@ export type { RouterOptions } from './service/router'; export { createRouter } from './service/router'; -export { kafkaPlugin } from './plugin'; +export { kafkaPlugin as default } from './plugin'; diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index 80751d23b8..d11e53714b 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,63 @@ # @backstage/plugin-kafka +## 0.3.24-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## 0.3.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## 0.3.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.3.21 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## 0.3.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.3.21-next.1 ### Patch Changes diff --git a/plugins/kafka/api-report.md b/plugins/kafka/api-report.md index 18b51b77fa..990a4da295 100644 --- a/plugins/kafka/api-report.md +++ b/plugins/kafka/api-report.md @@ -7,10 +7,12 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const EntityKafkaContent: () => JSX.Element; +export const EntityKafkaContent: () => JSX_2.Element; // @public (undocumented) const isPluginApplicableToEntity: (entity: Entity) => boolean; @@ -33,5 +35,5 @@ export { kafkaPlugin }; export { kafkaPlugin as plugin }; // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; ``` diff --git a/plugins/kafka/catalog-info.yaml b/plugins/kafka/catalog-info.yaml new file mode 100644 index 0000000000..cf0e0a6d9d --- /dev/null +++ b/plugins/kafka/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-kafka + title: '@backstage/plugin-kafka' + description: A Backstage plugin that integrates towards Kafka +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: andrewthauer diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 46ec0e8e51..b029e42bb6 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka", "description": "A Backstage plugin that integrates towards Kafka", - "version": "0.3.21-next.1", + "version": "0.3.24-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,6 +20,7 @@ "directory": "plugins/kafka" }, "configSchema": "config.d.ts", + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -39,6 +40,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -56,8 +58,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "jest-when": "^3.1.0", "msw": "^1.0.0" }, diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 532d0e0618..77fe7dec76 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,94 @@ # @backstage/plugin-kubernetes-backend +## 0.11.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/integration-aws-node@0.1.6-next.1 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-kubernetes-common@0.6.6-next.1 + +## 0.11.6-next.1 + +### Patch Changes + +- ccf00accb408: Add AWS Annotations to Kubernetes Cluster Resource +- 47ea122590f5: fix "undefined" kind for custom resources +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.6-next.0 + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/integration-aws-node@0.1.6-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.11.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/integration-aws-node@0.1.5 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-kubernetes-common@0.6.5 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + +## 0.11.3 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- bbf4e9c894b5: Fixed a bug where the proxy was not rewriting WebSocket request paths properly. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/integration-aws-node@0.1.5 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-kubernetes-common@0.6.5 + - @backstage/plugin-permission-common@0.7.7 + +## 0.11.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + ## 0.11.3-next.1 ### Patch Changes diff --git a/plugins/kubernetes-backend/alpha-api-report.md b/plugins/kubernetes-backend/alpha-api-report.md index f95c1414f3..f6eeec5b4c 100644 --- a/plugins/kubernetes-backend/alpha-api-report.md +++ b/plugins/kubernetes-backend/alpha-api-report.md @@ -6,7 +6,8 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; // @alpha -export const kubernetesPlugin: () => BackendFeature; +const kubernetesPlugin: () => BackendFeature; +export default kubernetesPlugin; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/kubernetes-backend/catalog-info.yaml b/plugins/kubernetes-backend/catalog-info.yaml new file mode 100644 index 0000000000..cd1026d01d --- /dev/null +++ b/plugins/kubernetes-backend/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-kubernetes-backend + title: '@backstage/plugin-kubernetes-backend' + description: A Backstage backend plugin that integrates towards Kubernetes +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: kubernetes-maintainers diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 6663479f5e..c75b7cbd69 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,14 +1,12 @@ { "name": "@backstage/plugin-kubernetes-backend", "description": "A Backstage backend plugin that integrates towards Kubernetes", - "version": "0.11.3-next.1", + "version": "0.11.6-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" }, "exports": { ".": "./src/index.ts", @@ -65,6 +63,7 @@ "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", "@backstage/plugin-permission-node": "workspace:^", + "@backstage/types": "workspace:^", "@google-cloud/container": "^4.0.0", "@jest-mock/express": "^2.0.1", "@kubernetes/client-node": "0.18.1", diff --git a/plugins/kubernetes-backend/src/alpha.ts b/plugins/kubernetes-backend/src/alpha.ts index 3041a5682c..9171964f7c 100644 --- a/plugins/kubernetes-backend/src/alpha.ts +++ b/plugins/kubernetes-backend/src/alpha.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { kubernetesPlugin } from './plugin'; +export { kubernetesPlugin as default } from './plugin'; diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts index 81ee863cb7..f436024131 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts @@ -29,7 +29,7 @@ const mockCatalogApi = { annotations: { 'kubernetes.io/api-server': 'https://apiserver.com', 'kubernetes.io/api-server-certificate-authority': 'caData', - 'kubernetes.io/auth-provider': 'aws', + 'kubernetes.io/auth-provider': 'oidc', 'kubernetes.io/oidc-token-provider': 'google', 'kubernetes.io/skip-metrics-lookup': 'true', 'kubernetes.io/skip-tls-verify': 'true', @@ -40,6 +40,24 @@ const mockCatalogApi = { namespace: 'default', }, }, + { + apiVersion: 'version', + kind: 'User', + metadata: { + annotations: { + 'kubernetes.io/api-server': 'https://apiserver.com', + 'kubernetes.io/api-server-certificate-authority': 'caData', + 'kubernetes.io/auth-provider': 'aws', + 'kubernetes.io/aws-assume-role': 'my-role', + 'kubernetes.io/aws-external-id': 'my-id', + 'kubernetes.io/oidc-token-provider': 'google', + 'kubernetes.io/dashboard-url': 'my-url', + 'kubernetes.io/dashboard-app': 'my-app', + }, + name: 'owned', + namespace: 'default', + }, + }, ], }), } as unknown as CatalogApi; @@ -67,12 +85,12 @@ describe('CatalogClusterLocator', () => { const result = await clusterSupplier.getClusters(); - expect(result).toHaveLength(1); + expect(result).toHaveLength(2); expect(result[0]).toStrictEqual({ name: 'owned', url: 'https://apiserver.com', caData: 'caData', - authProvider: 'aws', + authProvider: 'oidc', oidcTokenProvider: 'google', skipMetricsLookup: true, skipTLSVerify: true, @@ -80,4 +98,25 @@ describe('CatalogClusterLocator', () => { dashboardApp: 'my-app', }); }); + + it('returns the aws cluster details provided by annotations', async () => { + const clusterSupplier = CatalogClusterLocator.fromConfig(mockCatalogApi); + + const result = await clusterSupplier.getClusters(); + + expect(result).toHaveLength(2); + expect(result[1]).toStrictEqual({ + name: 'owned', + url: 'https://apiserver.com', + caData: 'caData', + authProvider: 'aws', + assumeRole: 'my-role', + externalId: 'my-id', + oidcTokenProvider: 'google', + skipMetricsLookup: false, + skipTLSVerify: false, + dashboardUrl: 'my-url', + dashboardApp: 'my-app', + }); + }); }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts index b952aefccf..c48cc99eb6 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; +import { + AWSClusterDetails, + ClusterDetails, + KubernetesClustersSupplier, +} from '../types/types'; import { CATALOG_FILTER_EXISTS, CatalogApi } from '@backstage/catalog-client'; import { ANNOTATION_KUBERNETES_API_SERVER, @@ -25,6 +29,8 @@ import { ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY, ANNOTATION_KUBERNETES_DASHBOARD_URL, ANNOTATION_KUBERNETES_DASHBOARD_APP, + ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID, + ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE, } from '@backstage/plugin-kubernetes-common'; export class CatalogClusterLocator implements KubernetesClustersSupplier { @@ -69,21 +75,31 @@ export class CatalogClusterLocator implements KubernetesClustersSupplier { skipMetricsLookup: entity.metadata.annotations![ ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP - ]! === 'true' - ? true - : false, + ]! === 'true', skipTLSVerify: entity.metadata.annotations![ ANNOTATION_KUBERNETES_SKIP_TLS_VERIFY - ]! === 'true' - ? true - : false, + ]! === 'true', dashboardUrl: entity.metadata.annotations![ANNOTATION_KUBERNETES_DASHBOARD_URL]!, dashboardApp: entity.metadata.annotations![ANNOTATION_KUBERNETES_DASHBOARD_APP]!, }; + if (clusterDetails.authProvider === 'aws') { + return { + ...clusterDetails, + assumeRole: + entity.metadata.annotations![ + ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE + ]!, + externalId: + entity.metadata.annotations![ + ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID + ]!, + } as AWSClusterDetails; + } + return clusterDetails; }); } diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index 9800780077..1a603a7a98 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -351,6 +351,7 @@ describe('KubernetesFetcher', () => { res( checkToken(req, ctx, 'token'), withLabels(req, ctx, { + kind: 'PodList', items: [{ metadata: { name: 'pod-name' } }], }), ), @@ -359,6 +360,7 @@ describe('KubernetesFetcher', () => { res( checkToken(req, ctx, 'token'), withLabels(req, ctx, { + kind: 'ServiceList', items: [{ metadata: { name: 'service-name' } }], }), ), @@ -369,6 +371,7 @@ describe('KubernetesFetcher', () => { res( checkToken(req, ctx, 'token'), withLabels(req, ctx, { + kind: 'ThingList', items: [{ metadata: { name: 'something-else' } }], }), ), @@ -424,6 +427,7 @@ describe('KubernetesFetcher', () => { type: 'customresources', resources: [ { + kind: 'Thing', metadata: { name: 'something-else', labels: { 'backstage.io/kubernetes-id': 'some-service' }, diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index b2be199d50..25bade3e97 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -41,6 +41,7 @@ import { import fetch, { RequestInit, Response } from 'node-fetch'; import * as https from 'https'; import fs from 'fs-extra'; +import { JsonObject } from '@backstage/types'; export interface KubernetesClientBasedFetcherOptions { logger: Logger; @@ -104,9 +105,15 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher { (r: Response): Promise => r.ok ? r.json().then( - ({ items }): FetchResponse => ({ + ({ kind, items }): FetchResponse => ({ type: objectType, - resources: items, + resources: + objectType === 'customresources' + ? items.map((item: JsonObject) => ({ + ...item, + kind: kind.replace(/(List)$/, ''), + })) + : items, }), ) : this.handleUnsuccessfulResponse(params.clusterDetails.name, r), diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md index 6ff3e84e07..610e94cca3 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-kubernetes-common +## 0.6.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + +## 0.6.6-next.0 + +### Patch Changes + +- ccf00accb408: Add AWS Annotations to Kubernetes Cluster Resource +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-permission-common@0.7.8-next.0 + ## 0.6.5 ### Patch Changes diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index 56faeea7d9..aae72d6f1e 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -31,6 +31,14 @@ export const ANNOTATION_KUBERNETES_API_SERVER_CA = export const ANNOTATION_KUBERNETES_AUTH_PROVIDER = 'kubernetes.io/auth-provider'; +// @public +export const ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE = + 'kubernetes.io/aws-assume-role'; + +// @public +export const ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID = + 'kubernetes.io/aws-external-id'; + // @public export const ANNOTATION_KUBERNETES_DASHBOARD_APP = 'kubernetes.io/dashboard-app'; diff --git a/plugins/kubernetes-common/catalog-info.yaml b/plugins/kubernetes-common/catalog-info.yaml new file mode 100644 index 0000000000..be2e47bbbe --- /dev/null +++ b/plugins/kubernetes-common/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-kubernetes-common + title: '@backstage/plugin-kubernetes-common' + description: >- + Common functionalities for kubernetes, to be shared between kubernetes and + kubernetes-backend plugin +spec: + lifecycle: experimental + type: backstage-common-library + owner: kubernetes-maintainers diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index 0e59a22987..8b934533b1 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-common", "description": "Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin", - "version": "0.6.5", + "version": "0.6.6-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -26,6 +26,7 @@ "files": [ "dist" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/plugins/kubernetes-common/src/catalog-entity-constants.ts b/plugins/kubernetes-common/src/catalog-entity-constants.ts index a1a1996777..8a826d4316 100644 --- a/plugins/kubernetes-common/src/catalog-entity-constants.ts +++ b/plugins/kubernetes-common/src/catalog-entity-constants.ts @@ -76,3 +76,19 @@ export const ANNOTATION_KUBERNETES_DASHBOARD_URL = */ export const ANNOTATION_KUBERNETES_DASHBOARD_APP = 'kubernetes.io/dashboard-app'; + +/** + * Annotation for specifying the assume role use to authenticate with AWS. + * + * @public + */ +export const ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE = + 'kubernetes.io/aws-assume-role'; + +/** + * Annotation for specifying an external id when communicating with AWS + * + * @public + */ +export const ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID = + 'kubernetes.io/aws-external-id'; diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 600363e960..bc64f49fc2 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,78 @@ # @backstage/plugin-kubernetes +## 0.10.3-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-kubernetes-common@0.6.6-next.1 + +## 0.10.3-next.1 + +### Patch Changes + +- 6a5e04e20e6e: fix logs dialog min height +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.6.6-next.0 + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.10.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-kubernetes-common@0.6.5 + +## 0.10.0 + +### Minor Changes + +- 4b6d41737aad: **BREAKING**: Fixed typos in exported module attributes. Many users may see no changes, but if you have customized the plugin output, you may have to rename the following: + + - Component attribute: `` to `` + - Interface attribute: `DetectedError.occuranceCount` to `DetectedError.occurrenceCount`. + +### Patch Changes + +- 562e34d2693f: Fix container memory limits +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-kubernetes-common@0.6.5 + +## 0.9.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.9.4-next.0 ### Patch Changes diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md index 7cef17af7b..77ec8dd19c 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -30,6 +30,7 @@ import { OpenIdConnectApi } from '@backstage/core-plugin-api'; import { Pod } from 'kubernetes-models/v1/Pod'; import { Pod as Pod_2 } from 'kubernetes-models/v1'; import { default as React_2 } from 'react'; +import * as React_3 from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { TypeMeta } from '@kubernetes-models/base'; import { V1ConfigMap } from '@kubernetes/client-node'; @@ -52,7 +53,7 @@ import { WorkloadsByEntityRequest } from '@backstage/plugin-kubernetes-common'; export const Cluster: ({ clusterObjects, podsWithErrors, -}: ClusterProps) => JSX.Element; +}: ClusterProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "ClusterContext" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -110,13 +111,13 @@ export interface ContainerScope extends PodScope { // Warning: (ae-missing-release-tag) "CronJobsAccordions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const CronJobsAccordions: ({}: CronJobsAccordionsProps) => JSX.Element; +export const CronJobsAccordions: ({}: CronJobsAccordionsProps) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "CustomResourcesProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "CustomResources" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const CustomResources: ({}: CustomResourcesProps) => JSX.Element; +export const CustomResources: ({}: CustomResourcesProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "DeploymentResources" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -137,7 +138,7 @@ export interface DetectedError { // (undocumented) message: string; // (undocumented) - occuranceCount: number; + occurrenceCount: number; // Warning: (ae-forgotten-export) The symbol "ProposedFix" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -174,7 +175,9 @@ export type EntityKubernetesContentProps = { }; // @public -export const ErrorList: ({ podAndErrors }: ErrorListProps) => JSX.Element; +export const ErrorList: ({ + podAndErrors, +}: ErrorListProps) => React_2.JSX.Element; // @public export interface ErrorListProps { @@ -195,7 +198,7 @@ export const ErrorPanel: ({ entityName, errorMessage, clustersWithErrors, -}: ErrorPanelProps) => JSX.Element; +}: ErrorPanelProps) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "ErrorReportingProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ErrorReporting" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -203,7 +206,7 @@ export const ErrorPanel: ({ // @public (undocumented) export const ErrorReporting: ({ detectedErrors, -}: ErrorReportingProps) => JSX.Element; +}: ErrorReportingProps) => React_3.JSX.Element; // @public export type ErrorSeverity = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; @@ -214,13 +217,13 @@ export const Events: ({ namespace, clusterName, warningEventsOnly, -}: EventsProps) => JSX.Element; +}: EventsProps) => React_2.JSX.Element; // @public export const EventsContent: ({ events, warningEventsOnly, -}: EventsContentProps) => JSX.Element; +}: EventsContentProps) => React_2.JSX.Element; // @public export interface EventsContentProps { @@ -323,13 +326,13 @@ export const HorizontalPodAutoscalerDrawer: (props: { hpa: V1HorizontalPodAutoscaler; expanded?: boolean; children?: React_2.ReactNode; -}) => JSX.Element; +}) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "IngressesAccordionsProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "IngressesAccordions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const IngressesAccordions: ({}: IngressesAccordionsProps) => JSX.Element; +export const IngressesAccordions: ({}: IngressesAccordionsProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "isKubernetesAvailable" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -340,7 +343,9 @@ export const isKubernetesAvailable: (entity: Entity) => boolean; // Warning: (ae-missing-release-tag) "JobsAccordions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const JobsAccordions: ({ jobs }: JobsAccordionsProps) => JSX.Element; +export const JobsAccordions: ({ + jobs, +}: JobsAccordionsProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "KubernetesApi" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -464,7 +469,7 @@ export class KubernetesBackendClient implements KubernetesApi { export const KubernetesContent: ({ entity, refreshIntervalMs, -}: KubernetesContentProps) => JSX.Element; +}: KubernetesContentProps) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "KubernetesDrawerProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "KubernetesDrawer" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -476,7 +481,7 @@ export const KubernetesDrawer: ({ drawerContentsHeader, kubernetesObject, children, -}: KubernetesDrawerProps) => JSX.Element; +}: KubernetesDrawerProps) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "KubernetesDrawerContentProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "KubernetesDrawerContent" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -487,7 +492,7 @@ export const KubernetesDrawerContent: ({ header, kubernetesObject, close, -}: KubernetesDrawerContentProps) => JSX.Element; +}: KubernetesDrawerContentProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "KubernetesObjects" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -586,7 +591,7 @@ export const KubernetesStructuredMetadataTableDrawer: < buttonVariant, expanded, children, -}: KubernetesStructuredMetadataTableDrawerProps) => JSX.Element; +}: KubernetesStructuredMetadataTableDrawerProps) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "ErrorPanelProps_2" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "LinkErrorPanel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -595,12 +600,12 @@ export const KubernetesStructuredMetadataTableDrawer: < export const LinkErrorPanel: ({ cluster, errorMessage, -}: ErrorPanelProps_2) => JSX.Element; +}: ErrorPanelProps_2) => React_2.JSX.Element; // @public export const PendingPodContent: ({ pod, -}: PendingPodContentProps) => JSX.Element; +}: PendingPodContentProps) => React_2.JSX.Element; // @public export interface PendingPodContentProps { @@ -621,7 +626,32 @@ export interface PodAndErrors { // Warning: (ae-forgotten-export) The symbol "PodDrawerProps" needs to be exported by the entry point index.d.ts // // @public -export const PodDrawer: ({ podAndErrors, open }: PodDrawerProps) => JSX.Element; +export const PodDrawer: ({ + podAndErrors, + open, +}: PodDrawerProps) => React_2.JSX.Element; + +// @public +export const PodExecTerminal: ( + props: PodExecTerminalProps, +) => React_2.JSX.Element; + +// @public +export const PodExecTerminalDialog: ( + props: PodExecTerminalProps, +) => React_2.JSX.Element; + +// @public +export interface PodExecTerminalProps { + // (undocumented) + clusterName: string; + // (undocumented) + containerName: string; + // (undocumented) + podName: string; + // (undocumented) + podNamespace: string; +} // @public export const PodLogs: React_2.FC; @@ -629,7 +659,7 @@ export const PodLogs: React_2.FC; // @public export const PodLogsDialog: ({ containerScope, -}: PodLogsDialogProps) => JSX.Element; +}: PodLogsDialogProps) => React_2.JSX.Element; // @public export interface PodLogsDialogProps { @@ -692,7 +722,7 @@ export interface PodScope { export const PodsTable: ({ pods, extraColumns, -}: PodsTablesProps) => JSX.Element; +}: PodsTablesProps) => React_2.JSX.Element; // @public export interface ResourceRef { @@ -714,13 +744,15 @@ export const ResourceUtilization: ({ title, usage, total, - totalFormated, -}: ResourceUtilizationProps) => JSX.Element; + totalFormatted, +}: ResourceUtilizationProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "Router" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const Router: (props: { refreshIntervalMs?: number }) => JSX.Element; +export const Router: (props: { + refreshIntervalMs?: number; +}) => React_2.JSX.Element; // @public export class ServerSideKubernetesAuthProvider @@ -738,7 +770,7 @@ export class ServerSideKubernetesAuthProvider // Warning: (ae-missing-release-tag) "ServicesAccordions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ServicesAccordions: ({}: ServicesAccordionsProps) => JSX.Element; +export const ServicesAccordions: ({}: ServicesAccordionsProps) => React_2.JSX.Element; // @public export const useCustomResources: ( diff --git a/plugins/kubernetes/catalog-info.yaml b/plugins/kubernetes/catalog-info.yaml new file mode 100644 index 0000000000..8f96a0c10c --- /dev/null +++ b/plugins/kubernetes/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-kubernetes + title: '@backstage/plugin-kubernetes' + description: A Backstage plugin that integrates towards Kubernetes +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: kubernetes-maintainers diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 1f137a6240..46a52c37bd 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes", "description": "A Backstage plugin that integrates towards Kubernetes", - "version": "0.9.4-next.0", + "version": "0.10.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,6 +23,7 @@ "backstage", "kubernetes" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -53,7 +54,10 @@ "kubernetes-models": "^4.1.0", "lodash": "^4.17.21", "luxon": "^3.0.0", - "react-use": "^17.2.4" + "react-use": "^17.2.4", + "xterm": "^5.2.1", + "xterm-addon-attach": "^0.8.0", + "xterm-addon-fit": "^0.7.0" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0", @@ -71,6 +75,7 @@ "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", + "jest-websocket-mock": "^2.4.1", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/kubernetes/src/components/PodExecTerminal/PodExecTerminal.test.tsx b/plugins/kubernetes/src/components/PodExecTerminal/PodExecTerminal.test.tsx new file mode 100644 index 0000000000..1c913a7726 --- /dev/null +++ b/plugins/kubernetes/src/components/PodExecTerminal/PodExecTerminal.test.tsx @@ -0,0 +1,95 @@ +/* + * 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 { DiscoveryApi, discoveryApiRef } from '@backstage/core-plugin-api'; +import { + renderInTestApp, + TestApiProvider, + textContentMatcher, +} from '@backstage/test-utils'; +import '@testing-library/jest-dom'; +import { screen } from '@testing-library/react'; +import WS from 'jest-websocket-mock'; +import React from 'react'; +import './matchMedia.mock'; +import { PodExecTerminal } from './PodExecTerminal'; + +const textEncoder = new TextEncoder(); + +describe('PodExecTerminal', () => { + const clusterName = 'cluster1'; + const containerName = 'container2'; + const podName = 'pod1'; + const podNamespace = 'podNamespace'; + + const mockDiscoveryApi: Partial = { + getBaseUrl: () => Promise.resolve('http://localhost'), + }; + + it('Should render an XTerm web terminal', async () => { + await renderInTestApp( + + + , + ); + + await expect( + screen.findByText( + textContentMatcher('Starting terminal, please wait...'), + ), + ).resolves.toBeInTheDocument(); + }); + + it('Should connect to WebSocket server & render response', async () => { + const server = new WS( + 'ws://localhost/proxy/api/v1/namespaces/podNamespace/pods/pod1/exec?container=container2&stdin=true&stdout=true&stderr=true&tty=true&command=%2Fbin%2Fsh', + ); + + await renderInTestApp( + + + , + ); + + // xterm uses a "W" character as a "measure element" -- if it exists, the terminal rendered correctly + await expect( + screen.findByText(textContentMatcher('W')), + ).resolves.toBeInTheDocument(); + + await server.connected; + + const { buffer } = Uint8Array.from([ + 1, + ...textEncoder.encode('hello world'), + ]); + + server.send(buffer); + + await expect( + screen.findByText(textContentMatcher('hello world')), + ).resolves.toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/PodExecTerminal/PodExecTerminal.tsx b/plugins/kubernetes/src/components/PodExecTerminal/PodExecTerminal.tsx new file mode 100644 index 0000000000..a741f4e189 --- /dev/null +++ b/plugins/kubernetes/src/components/PodExecTerminal/PodExecTerminal.tsx @@ -0,0 +1,131 @@ +/* + * 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 'xterm/css/xterm.css'; + +import { discoveryApiRef, useApi } from '@backstage/core-plugin-api'; +import React, { useEffect, useMemo, useState } from 'react'; +import { Terminal } from 'xterm'; +import { FitAddon } from 'xterm-addon-fit'; + +import { PodExecTerminalAttachAddon } from './PodExecTerminalAttachAddon'; + +/** + * Props drilled down to the PodExecTerminal component + * + * @public + */ +export interface PodExecTerminalProps { + clusterName: string; + containerName: string; + podName: string; + podNamespace: string; +} + +const hasSocketProtocol = (url: string | URL) => + /wss?:\/\//.test(url.toString()); + +/** + * Executes a `/bin/sh` process in the given pod's container and opens a terminal connected to it + * + * @public + */ +export const PodExecTerminal = (props: PodExecTerminalProps) => { + const { containerName, podNamespace, podName } = props; + + const [baseUrl, setBaseUrl] = useState(window.location.host); + + const terminalRef = React.useRef(null); + const discoveryApi = useApi(discoveryApiRef); + const namespace = podNamespace ?? 'default'; + + useEffect(() => { + discoveryApi + .getBaseUrl('kubernetes') + .then(url => url ?? window.location.host) + .then(url => url.replace(/^http(s?):\/\//, 'ws$1://')) + .then(url => setBaseUrl(url)); + }, [discoveryApi]); + + const urlParams = useMemo(() => { + const params = new URLSearchParams({ + container: containerName, + stdin: 'true', + stdout: 'true', + stderr: 'true', + tty: 'true', + command: '/bin/sh', + }); + return params; + }, [containerName]); + + const socketUrl = useMemo(() => { + if (!hasSocketProtocol(baseUrl)) { + return ''; + } + + return new URL( + `${baseUrl}/proxy/api/v1/namespaces/${namespace}/pods/${podName}/exec?${urlParams}`, + ); + }, [baseUrl, namespace, podName, urlParams]); + + useEffect(() => { + if (!hasSocketProtocol(socketUrl)) { + return () => {}; + } + + const terminal = new Terminal(); + const fitAddon = new FitAddon(); + terminal.loadAddon(fitAddon); + + if (terminalRef.current) { + terminal.open(terminalRef.current); + fitAddon.fit(); + } + + terminal.writeln('Starting terminal, please wait...'); + + const socket = new WebSocket(socketUrl, ['channel.k8s.io']); + + socket.onopen = () => { + terminal.clear(); + const attachAddon = new PodExecTerminalAttachAddon(socket, { + bidirectional: true, + }); + + terminal.loadAddon(attachAddon); + }; + + socket.onclose = () => { + terminal.writeln('Socket connection closed'); + }; + + return () => { + terminal?.clear(); + socket?.close(); + }; + }, [baseUrl, socketUrl]); + + return ( +
+ ); +}; diff --git a/plugins/kubernetes/src/components/PodExecTerminal/PodExecTerminalAttachAddon.ts b/plugins/kubernetes/src/components/PodExecTerminal/PodExecTerminalAttachAddon.ts new file mode 100644 index 0000000000..fb80472f73 --- /dev/null +++ b/plugins/kubernetes/src/components/PodExecTerminal/PodExecTerminalAttachAddon.ts @@ -0,0 +1,46 @@ +/* + * 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 { AttachAddon, IAttachOptions } from 'xterm-addon-attach'; + +const textEncoder = new TextEncoder(); + +export class PodExecTerminalAttachAddon extends AttachAddon { + constructor(socket: WebSocket, options?: IAttachOptions) { + super(socket, options); + + const thisAddon = this as any; + + // These methods are private in the original AttachAddon, + // thus have to override at runtime like this + thisAddon._sendBinary = (data: string) => { + if (!thisAddon._checkOpenSocket()) { + return; + } + + const buffer = Uint8Array.from([0, ...textEncoder.encode(data)]); + + thisAddon._socket.send(buffer); + }; + + thisAddon._sendData = (data: string) => { + if (!thisAddon._checkOpenSocket()) { + return; + } + + thisAddon._sendBinary(data); + }; + } +} diff --git a/plugins/kubernetes/src/components/PodExecTerminal/PodExecTerminalDialog.tsx b/plugins/kubernetes/src/components/PodExecTerminal/PodExecTerminalDialog.tsx new file mode 100644 index 0000000000..5ed8c8c0d1 --- /dev/null +++ b/plugins/kubernetes/src/components/PodExecTerminal/PodExecTerminalDialog.tsx @@ -0,0 +1,110 @@ +/* + * 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 { + Button, + createStyles, + Dialog, + DialogContent, + DialogTitle, + IconButton, + makeStyles, + Theme, +} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; +import React, { useState } from 'react'; + +import { useIsPodExecTerminalSupported } from '../../hooks'; +import { PodExecTerminal, PodExecTerminalProps } from './PodExecTerminal'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + dialogPaper: { minHeight: 'calc(100% - 64px)' }, + dialogContent: { flexBasis: 0 }, + closeButton: { + position: 'absolute', + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, + }), +); + +/** + * Opens a terminal connected to the given pod's container in a dialog + * + * @public + */ +export const PodExecTerminalDialog = (props: PodExecTerminalProps) => { + const classes = useStyles(); + const { clusterName, containerName, podName } = props; + + const [open, setOpen] = useState(false); + + const isPodExecTerminalSupported = useIsPodExecTerminalSupported(); + + const openDialog = () => { + setOpen(true); + }; + + const closeDialog = () => { + setOpen(false); + }; + + return ( + <> + {!isPodExecTerminalSupported.loading && + isPodExecTerminalSupported.value && ( + + + {podName} - {containerName} terminal shell on cluster{' '} + {clusterName} + + + + + + + + + )} + + + + ); +}; diff --git a/plugins/kubernetes/src/components/PodExecTerminal/index.ts b/plugins/kubernetes/src/components/PodExecTerminal/index.ts new file mode 100644 index 0000000000..2581a35669 --- /dev/null +++ b/plugins/kubernetes/src/components/PodExecTerminal/index.ts @@ -0,0 +1,17 @@ +/* + * 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 './PodExecTerminal'; +export * from './PodExecTerminalDialog'; diff --git a/plugins/kubernetes/src/components/PodExecTerminal/matchMedia.mock.ts b/plugins/kubernetes/src/components/PodExecTerminal/matchMedia.mock.ts new file mode 100644 index 0000000000..52661d07e8 --- /dev/null +++ b/plugins/kubernetes/src/components/PodExecTerminal/matchMedia.mock.ts @@ -0,0 +1,31 @@ +/* + * 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. + */ + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // deprecated + removeListener: jest.fn(), // deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); + +export {}; diff --git a/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.test.tsx b/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.test.tsx index 3ee755becb..b05b4d2d9e 100644 --- a/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.test.tsx +++ b/plugins/kubernetes/src/components/Pods/ErrorList/ErrorList.test.tsx @@ -38,7 +38,7 @@ describe('ErrorList', () => { type: 'some-error', severity: 10, message: 'some error message', - occuranceCount: 1, + occurrenceCount: 1, sourceRef: { name: 'some-pod', namespace: 'some-namespace', diff --git a/plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.test.tsx b/plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.test.tsx index e779ab5f0d..68bfce9c96 100644 --- a/plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.test.tsx +++ b/plugins/kubernetes/src/components/Pods/FixDialog/FixDialog.test.tsx @@ -50,7 +50,7 @@ describe('FixDialog', () => { type: 'some error type', severity: 10, message: 'some error message', - occuranceCount: 1, + occurrenceCount: 1, sourceRef: { name: 'some-pod', namespace: 'some-namespace', @@ -91,7 +91,7 @@ describe('FixDialog', () => { type: 'some error type', severity: 10, message: 'some error message', - occuranceCount: 1, + occurrenceCount: 1, sourceRef: { name: 'some-pod', namespace: 'some-namespace', @@ -132,7 +132,7 @@ describe('FixDialog', () => { type: 'some error type', severity: 10, message: 'some error message', - occuranceCount: 1, + occurrenceCount: 1, sourceRef: { name: 'some-pod', namespace: 'some-namespace', diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.test.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.test.tsx index 7bc7c989ed..bdea9dd0be 100644 --- a/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.test.tsx +++ b/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.test.tsx @@ -22,6 +22,8 @@ import { wrapInTestApp } from '@backstage/test-utils'; import { ContainerCard } from './ContainerCard'; import { DateTime } from 'luxon'; +jest.mock('../../../hooks/useIsPodExecTerminalSupported'); + const now = DateTime.now(); const oneHourAgo = now.minus({ hours: 1 }).toISO(); const twoHoursAgo = now.minus({ hours: 2 }).toISO(); diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.tsx index 9c4272248b..db70f92160 100644 --- a/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.tsx +++ b/plugins/kubernetes/src/components/Pods/PodDrawer/ContainerCard.tsx @@ -13,9 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; - -import { IContainer, IContainerStatus } from 'kubernetes-models/v1'; +import { StructuredMetadataTable } from '@backstage/core-components'; +import { ClientContainerStatus } from '@backstage/plugin-kubernetes-common'; import { Card, CardActions, @@ -24,14 +23,14 @@ import { Grid, Typography, } from '@material-ui/core'; - +import { IContainer, IContainerStatus } from 'kubernetes-models/v1'; import { DateTime } from 'luxon'; +import React from 'react'; -import { PodScope, PodLogsDialog } from '../PodLogs'; -import { StructuredMetadataTable } from '@backstage/core-components'; -import { ClientContainerStatus } from '@backstage/plugin-kubernetes-common'; +import { bytesToMiB, formatMillicores } from '../../../utils/resources'; +import { PodExecTerminalDialog } from '../../PodExecTerminal/PodExecTerminalDialog'; import { ResourceUtilization } from '../../ResourceUtilization'; -import { bytesToMiB, formatMilicores } from '../../../utils/resources'; +import { PodLogsDialog, PodScope } from '../PodLogs'; const getContainerHealthChecks = ( containerSpec: IContainer, @@ -186,7 +185,7 @@ export const ContainerCard: React.FC = ({ title="CPU requests" usage={containerMetrics.cpuUsage.currentUsage} total={containerMetrics.cpuUsage.requestTotal} - totalFormated={formatMilicores( + totalFormatted={formatMillicores( containerMetrics.cpuUsage.requestTotal, )} /> @@ -195,7 +194,7 @@ export const ContainerCard: React.FC = ({ title="CPU limits" usage={containerMetrics.cpuUsage.currentUsage} total={containerMetrics.cpuUsage.limitTotal} - totalFormated={formatMilicores( + totalFormatted={formatMillicores( containerMetrics.cpuUsage.limitTotal, )} /> @@ -204,7 +203,7 @@ export const ContainerCard: React.FC = ({ title="Memory requests" usage={containerMetrics.memoryUsage.currentUsage} total={containerMetrics.memoryUsage.requestTotal} - totalFormated={bytesToMiB( + totalFormatted={bytesToMiB( containerMetrics.memoryUsage.requestTotal, )} /> @@ -213,8 +212,8 @@ export const ContainerCard: React.FC = ({ title="Memory limits" usage={containerMetrics.memoryUsage.currentUsage} total={containerMetrics.memoryUsage.limitTotal} - totalFormated={bytesToMiB( - containerMetrics.memoryUsage.requestTotal, + totalFormatted={bytesToMiB( + containerMetrics.memoryUsage.limitTotal, )} /> @@ -222,13 +221,19 @@ export const ContainerCard: React.FC = ({ )} - + + ); diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/PendingPodContent.test.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer/PendingPodContent.test.tsx index e4d042217b..8896654696 100644 --- a/plugins/kubernetes/src/components/Pods/PodDrawer/PendingPodContent.test.tsx +++ b/plugins/kubernetes/src/components/Pods/PodDrawer/PendingPodContent.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { render } from '@testing-library/react'; @@ -44,7 +45,7 @@ const podWithConditions = (conditions: IPodCondition[]): any => { describe('PendingPodContent', () => { it('show startup conditions - all healthy', async () => { - const oneDayAgo = DateTime.now().minus({ days: 1 }).toISO(); + const oneDayAgo = DateTime.now().minus({ days: 1 }).toISO()!; const { getByText, queryByLabelText, queryAllByLabelText } = render( wrapInTestApp( { expect(queryByLabelText('Status error')).not.toBeInTheDocument(); }); it('show startup conditions - all fail', async () => { - const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO(); + const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO()!; const { getByText, queryByLabelText, queryAllByLabelText } = render( wrapInTestApp( { expect(queryAllByLabelText('Status error')).toHaveLength(4); }); it('show startup conditions - show unknown', async () => { - const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO(); + const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO()!; const { getByText, queryByLabelText, getByLabelText } = render( wrapInTestApp( { it('Should show title and container names', async () => { + const mockDiscoveryApi: Partial = { + getBaseUrl: () => Promise.resolve('http://localhost'), + }; + const { getAllByText, getByText } = render( wrapInTestApp( - + , + errors: [ + { + type: 'some-error', + severity: 10, + message: 'some error message', + occurrenceCount: 1, + sourceRef: { + name: 'some-pod', + namespace: 'some-namespace', + kind: 'Pod', + apiGroup: 'v1', + }, + proposedFix: [ + { + type: 'logs', + container: 'some-container', + errorType: 'some error type', + rootCauseExplanation: 'some root cause', + actions: ['fix1', 'fix2'], + }, + ], + }, + ], + }, + } as any)} + /> + , ), ); diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.tsx index 82739be0fb..30588cd347 100644 --- a/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.tsx +++ b/plugins/kubernetes/src/components/Pods/PodDrawer/PodDrawer.tsx @@ -35,7 +35,7 @@ import { PendingPodContent } from './PendingPodContent'; import { ErrorList } from '../ErrorList'; import { usePodMetrics } from '../../../hooks/usePodMetrics'; import { ResourceUtilization } from '../../ResourceUtilization'; -import { bytesToMiB, formatMilicores } from '../../../utils/resources'; +import { bytesToMiB, formatMillicores } from '../../../utils/resources'; const useDrawerContentStyles = makeStyles((_theme: Theme) => createStyles({ @@ -109,13 +109,13 @@ export const PodDrawer = ({ podAndErrors, open }: PodDrawerProps) => { title="CPU requests" usage={podMetrics.cpu.currentUsage} total={podMetrics.cpu.requestTotal} - totalFormated={formatMilicores(podMetrics.cpu.requestTotal)} + totalFormatted={formatMillicores(podMetrics.cpu.requestTotal)} /> @@ -123,13 +123,13 @@ export const PodDrawer = ({ podAndErrors, open }: PodDrawerProps) => { title="Memory requests" usage={podMetrics.memory.currentUsage} total={podMetrics.memory.requestTotal} - totalFormated={bytesToMiB(podMetrics.memory.requestTotal)} + totalFormatted={bytesToMiB(podMetrics.memory.requestTotal)} /> diff --git a/plugins/kubernetes/src/components/Pods/PodLogs/PodLogs.tsx b/plugins/kubernetes/src/components/Pods/PodLogs/PodLogs.tsx index de9341508e..5c08de047c 100644 --- a/plugins/kubernetes/src/components/Pods/PodLogs/PodLogs.tsx +++ b/plugins/kubernetes/src/components/Pods/PodLogs/PodLogs.tsx @@ -64,7 +64,7 @@ export const PodLogs: React.FC = ({ )} {loading && } {!loading && diff --git a/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx b/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx index 4478d8c031..35402527f8 100644 --- a/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx +++ b/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx @@ -124,7 +124,7 @@ describe('PodsTable', () => { expect(getByText('requests: 1% of 64MiB')).toBeInTheDocument(); expect(getByText('limits: 0% of 128MiB')).toBeInTheDocument(); }); - it('should render placehoplder when empty metrics context', async () => { + it('should render placeholder when empty metrics context', async () => { const podNameToClientPodStatus = new Map(); const wrapper = kubernetesProviders( diff --git a/plugins/kubernetes/src/components/ResourceUtilization/ResourceUtilization.test.tsx b/plugins/kubernetes/src/components/ResourceUtilization/ResourceUtilization.test.tsx index 56ba0b7ef0..60087022a5 100644 --- a/plugins/kubernetes/src/components/ResourceUtilization/ResourceUtilization.test.tsx +++ b/plugins/kubernetes/src/components/ResourceUtilization/ResourceUtilization.test.tsx @@ -27,7 +27,7 @@ describe('ResourceUtilization', () => { title="some-title" usage="1000" total="10000" - totalFormated="15%" + totalFormatted="15%" />, ), ); @@ -43,7 +43,7 @@ describe('ResourceUtilization', () => { title="some-title" usage="1000" total="10000" - totalFormated="15%" + totalFormatted="15%" />, ), ); diff --git a/plugins/kubernetes/src/components/ResourceUtilization/ResourceUtilization.tsx b/plugins/kubernetes/src/components/ResourceUtilization/ResourceUtilization.tsx index 492ec0469b..75927958e6 100644 --- a/plugins/kubernetes/src/components/ResourceUtilization/ResourceUtilization.tsx +++ b/plugins/kubernetes/src/components/ResourceUtilization/ResourceUtilization.tsx @@ -29,7 +29,7 @@ export interface ResourceUtilizationProps { title: string; usage: number | string; total: number | string; - totalFormated: string; + totalFormatted: string; } // Visible for testing @@ -64,7 +64,7 @@ export const ResourceUtilization = ({ title, usage, total, - totalFormated, + totalFormatted, }: ResourceUtilizationProps) => { const utilization = currentToDeclaredResourceToPerc(usage, total); return ( @@ -72,7 +72,7 @@ export const ResourceUtilization = ({ {`${title}: ${totalFormated}`} + >{`${title}: ${totalFormatted}`} [] = [ kind: 'Deployment', apiGroup: 'apps/v1', }, - occuranceCount: 1, + occurrenceCount: 1, })); }, }, diff --git a/plugins/kubernetes/src/error-detection/error-detection.test.ts b/plugins/kubernetes/src/error-detection/error-detection.test.ts index c3dc060b9b..ae4605c3c5 100644 --- a/plugins/kubernetes/src/error-detection/error-detection.test.ts +++ b/plugins/kubernetes/src/error-detection/error-detection.test.ts @@ -164,7 +164,7 @@ describe('detectErrors', () => { 'The container other-side-car has crashed many times, it will be exponentially restarted until it stops crashing', type: 'logs', }, - occuranceCount: 1, + occurrenceCount: 1, type: 'container-waiting', }); @@ -186,7 +186,7 @@ describe('detectErrors', () => { message: 'back-off 5m0s restarting failed container=side-car pod=dice-roller-canary-7d64cd756c-55rfq_default(65ad28e3-5d51-4b4b-9bf8-4cb069803034)', severity: 4, - occuranceCount: 1, + occurrenceCount: 1, type: 'container-waiting', }); @@ -199,7 +199,7 @@ describe('detectErrors', () => { }, message: 'container=other-side-car restarted 123 times', severity: 4, - occuranceCount: 123, + occurrenceCount: 123, proposedFix: { container: 'other-side-car', errorType: 'Error', @@ -228,7 +228,7 @@ describe('detectErrors', () => { }, message: 'container=side-car restarted 38 times', severity: 4, - occuranceCount: 38, + occurrenceCount: 38, type: 'containers-restarting', }); }); @@ -246,7 +246,7 @@ describe('detectErrors', () => { expect(err1).toStrictEqual({ message: 'configmap "some-cm" not found', - occuranceCount: 1, + occurrenceCount: 1, proposedFix: { docsLink: 'https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/', @@ -300,7 +300,7 @@ describe('detectErrors', () => { }, message: 'Deployment does not have minimum availability.', severity: 6, - occuranceCount: 1, + occurrenceCount: 1, type: 'condition-message-present', }); }); @@ -336,7 +336,7 @@ describe('detectErrors', () => { message: 'Current number of replicas (10) is equal to the configured max number of replicas (10)', severity: 8, - occuranceCount: 1, + occurrenceCount: 1, type: 'hpa-max-current-replicas', }); }); @@ -469,7 +469,7 @@ describe('detectErrors', () => { expect(err1).toStrictEqual({ message: 'The container some-container failed to start properly, but is not crashing', - occuranceCount: 1, + occurrenceCount: 1, proposedFix: { errorType: 'ReadinessProbeFailed', podName: '', diff --git a/plugins/kubernetes/src/error-detection/hpas.ts b/plugins/kubernetes/src/error-detection/hpas.ts index 671e379a5a..6f15c119b6 100644 --- a/plugins/kubernetes/src/error-detection/hpas.ts +++ b/plugins/kubernetes/src/error-detection/hpas.ts @@ -37,7 +37,7 @@ const hpaErrorMappers: ErrorMapper[] = [ kind: 'HorizontalPodAutoscaler', apiGroup: 'autoscaling/v1', }, - occuranceCount: 1, + occurrenceCount: 1, }, ]; } diff --git a/plugins/kubernetes/src/error-detection/pods.ts b/plugins/kubernetes/src/error-detection/pods.ts index 638d1da56c..7c0a85db30 100644 --- a/plugins/kubernetes/src/error-detection/pods.ts +++ b/plugins/kubernetes/src/error-detection/pods.ts @@ -209,7 +209,7 @@ const podErrorMappers: ErrorMapper[] = [ kind: 'Pod', apiGroup: 'v1', }, - occuranceCount: 1, + occurrenceCount: 1, })); }, }, @@ -228,7 +228,7 @@ const podErrorMappers: ErrorMapper[] = [ kind: 'Pod', apiGroup: 'v1', }, - occuranceCount: 1, + occurrenceCount: 1, })); }, }, @@ -247,7 +247,7 @@ const podErrorMappers: ErrorMapper[] = [ kind: 'Pod', apiGroup: 'v1', }, - occuranceCount: cs.restartCount, + occurrenceCount: cs.restartCount, })); }, }, diff --git a/plugins/kubernetes/src/error-detection/types.ts b/plugins/kubernetes/src/error-detection/types.ts index 27dcbdd55b..4148056171 100644 --- a/plugins/kubernetes/src/error-detection/types.ts +++ b/plugins/kubernetes/src/error-detection/types.ts @@ -51,7 +51,7 @@ export interface DetectedError { message: string; proposedFix?: ProposedFix; sourceRef: ResourceRef; - occuranceCount: number; + occurrenceCount: number; } export type ProposedFix = LogSolution | DocsSolution | EventsSolution; diff --git a/plugins/kubernetes/src/hooks/__mocks__/useIsPodExecTerminalSupported.ts b/plugins/kubernetes/src/hooks/__mocks__/useIsPodExecTerminalSupported.ts new file mode 100644 index 0000000000..e1dc3b4fc5 --- /dev/null +++ b/plugins/kubernetes/src/hooks/__mocks__/useIsPodExecTerminalSupported.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. + */ +import { AsyncState } from 'react-use/lib/useAsync'; + +export const useIsPodExecTerminalSupported: () => AsyncState = jest.fn( + () => ({ loading: false, value: false } as AsyncState), +); diff --git a/plugins/kubernetes/src/hooks/index.ts b/plugins/kubernetes/src/hooks/index.ts index 12400132aa..141bc3815b 100644 --- a/plugins/kubernetes/src/hooks/index.ts +++ b/plugins/kubernetes/src/hooks/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export * from './useIsPodExecTerminalSupported'; export * from './useKubernetesObjects'; export * from './useCustomResources'; export * from './PodNamesWithErrors'; diff --git a/plugins/kubernetes/src/hooks/useIsPodExecTerminalSupported.test.ts b/plugins/kubernetes/src/hooks/useIsPodExecTerminalSupported.test.ts new file mode 100644 index 0000000000..1772e0d4f2 --- /dev/null +++ b/plugins/kubernetes/src/hooks/useIsPodExecTerminalSupported.test.ts @@ -0,0 +1,83 @@ +/* + * 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 { useApi } from '@backstage/core-plugin-api'; +import { renderHook } from '@testing-library/react-hooks'; + +import { useIsPodExecTerminalSupported } from './useIsPodExecTerminalSupported'; + +jest.mock('@backstage/core-plugin-api'); + +describe('useIsClusterShellEnabled', () => { + let clusters: { authProvider: string }[] = []; + + beforeEach(() => { + (useApi as any).mockReturnValue({ + getClusters: () => Promise.resolve(clusters), + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it.each([ + { + condition: 'valid Shell setup', + returnValue: true, + testClusters: [{ authProvider: 'localKubectlProxy' }], + }, + { condition: 'clusters empty', returnValue: false, testClusters: [] }, + { + condition: 'multi cluster setup', + returnValue: false, + testClusters: [ + { authProvider: 'localKubectlProxy' }, + { authProvider: 'serviceAccount' }, + ], + }, + { + condition: 'AuthProvider is aks', + returnValue: false, + testClusters: [{ authProvider: 'aks' }], + }, + { + condition: 'AuthProvider is google', + returnValue: false, + testClusters: [{ authProvider: 'google' }], + }, + { + condition: 'AuthProvider start with oidc', + returnValue: false, + testClusters: [{ authProvider: 'oidc.okta' }], + }, + ])( + 'Should return $returnValue if $condition', + async ({ testClusters, returnValue }) => { + clusters = testClusters; + + const { result, waitForNextUpdate } = renderHook(() => + useIsPodExecTerminalSupported(), + ); + + expect(result.current.loading).toEqual(true); + + await waitForNextUpdate(); + + expect(result.current.loading).toEqual(false); + expect(result.current.value).toBe(returnValue); + }, + ); +}); diff --git a/plugins/kubernetes/src/hooks/useIsPodExecTerminalSupported.ts b/plugins/kubernetes/src/hooks/useIsPodExecTerminalSupported.ts new file mode 100644 index 0000000000..17f64bf0dd --- /dev/null +++ b/plugins/kubernetes/src/hooks/useIsPodExecTerminalSupported.ts @@ -0,0 +1,43 @@ +/* + * 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 { useApi } from '@backstage/core-plugin-api'; +import useAsync, { AsyncState } from 'react-use/lib/useAsync'; + +import { kubernetesApiRef } from '../api'; + +/** + * Check if conditions for a pod exec call through the proxy endpoint are met + * + * @internal + */ +export const useIsPodExecTerminalSupported = (): AsyncState => { + const kubernetesApi = useApi(kubernetesApiRef); + + return useAsync(async () => { + const clusters = await kubernetesApi.getClusters(); + + if (clusters.length !== 1) { + return false; + } + + const { authProvider } = clusters[0]; + const isClientAuthProvider = ['aks', 'google', 'oidc'].some( + authProviderName => authProvider.includes(authProviderName), + ); + + return !isClientAuthProvider; + }); +}; diff --git a/plugins/kubernetes/src/hooks/useMatchingErrors.test.tsx b/plugins/kubernetes/src/hooks/useMatchingErrors.test.tsx index b823492fff..4ed4dbd73c 100644 --- a/plugins/kubernetes/src/hooks/useMatchingErrors.test.tsx +++ b/plugins/kubernetes/src/hooks/useMatchingErrors.test.tsx @@ -24,7 +24,7 @@ const genericErrorWithRef = (resourceRef: ResourceRef): DetectedError => { type: 'some-error', severity: 10, message: 'some error message', - occuranceCount: 1, + occurrenceCount: 1, sourceRef: resourceRef, proposedFix: { type: 'logs', diff --git a/plugins/kubernetes/src/utils/pod.tsx b/plugins/kubernetes/src/utils/pod.tsx index f0517f4ec0..9c0ce898a3 100644 --- a/plugins/kubernetes/src/utils/pod.tsx +++ b/plugins/kubernetes/src/utils/pod.tsx @@ -29,7 +29,7 @@ import { } from '@backstage/core-components'; import { ClientPodStatus } from '@backstage/plugin-kubernetes-common'; import { Pod } from 'kubernetes-models/v1/Pod'; -import { bytesToMiB, formatMilicores } from './resources'; +import { bytesToMiB, formatMillicores } from './resources'; export const imageChips = (pod: V1Pod): ReactNode => { const containerStatuses = pod.status?.containerStatuses ?? []; @@ -148,11 +148,11 @@ export const podStatusToCpuUtil = (podStatus: ClientPodStatus): ReactNode => { value={`requests: ${currentToDeclaredResourceToPerc( currentUsage, cpuUtil.requestTotal, - )} of ${formatMilicores(cpuUtil.requestTotal)}`} + )} of ${formatMillicores(cpuUtil.requestTotal)}`} subvalue={`limits: ${currentToDeclaredResourceToPerc( currentUsage, cpuUtil.limitTotal, - )} of ${formatMilicores(cpuUtil.limitTotal)}`} + )} of ${formatMillicores(cpuUtil.limitTotal)}`} /> ); }; diff --git a/plugins/kubernetes/src/utils/resources.ts b/plugins/kubernetes/src/utils/resources.ts index b1d8dfdc2e..916aa3463c 100644 --- a/plugins/kubernetes/src/utils/resources.ts +++ b/plugins/kubernetes/src/utils/resources.ts @@ -33,6 +33,6 @@ export const bytesToMiB = (value: string | number): string => { return `${(parseFloat(value.toString()) / 1024 / 1024).toFixed(0)}MiB`; }; -export const formatMilicores = (value: string | number): string => { +export const formatMillicores = (value: string | number): string => { return `${(parseFloat(value.toString()) * 1000).toFixed(0)}m`; }; diff --git a/plugins/lighthouse-backend/CHANGELOG.md b/plugins/lighthouse-backend/CHANGELOG.md index 80c54e4501..b4fd2653ff 100644 --- a/plugins/lighthouse-backend/CHANGELOG.md +++ b/plugins/lighthouse-backend/CHANGELOG.md @@ -1,5 +1,76 @@ # @backstage/plugin-lighthouse-backend +## 0.2.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-lighthouse-common@0.1.3-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/types@1.1.0 + +## 0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-lighthouse-common@0.1.3-next.0 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/types@1.1.0 + +## 0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-lighthouse-common@0.1.2 + +## 0.2.4 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-lighthouse-common@0.1.2 + +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + ## 0.2.4-next.1 ### Patch Changes diff --git a/plugins/lighthouse-backend/api-report.md b/plugins/lighthouse-backend/api-report.md index 5d9d948cf4..d382e40def 100644 --- a/plugins/lighthouse-backend/api-report.md +++ b/plugins/lighthouse-backend/api-report.md @@ -30,7 +30,8 @@ export function createScheduler( ): Promise; // @public -export const lighthousePlugin: () => BackendFeature; +const lighthousePlugin: () => BackendFeature; +export default lighthousePlugin; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/lighthouse-backend/catalog-info.yaml b/plugins/lighthouse-backend/catalog-info.yaml new file mode 100644 index 0000000000..a44ce9fa44 --- /dev/null +++ b/plugins/lighthouse-backend/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-lighthouse-backend + title: '@backstage/plugin-lighthouse-backend' + description: Backend functionalities for lighthouse +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: maintainers diff --git a/plugins/lighthouse-backend/package.json b/plugins/lighthouse-backend/package.json index c5b9bb49ca..65f2ace22a 100644 --- a/plugins/lighthouse-backend/package.json +++ b/plugins/lighthouse-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-lighthouse-backend", "description": "Backend functionalities for lighthouse", - "version": "0.2.4-next.1", + "version": "0.2.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/lighthouse-backend/src/index.ts b/plugins/lighthouse-backend/src/index.ts index 5773beed56..5d2bff7ed6 100644 --- a/plugins/lighthouse-backend/src/index.ts +++ b/plugins/lighthouse-backend/src/index.ts @@ -15,4 +15,4 @@ */ export * from './service/createScheduler'; -export { lighthousePlugin } from './plugin'; +export { lighthousePlugin as default } from './plugin'; diff --git a/plugins/lighthouse-common/CHANGELOG.md b/plugins/lighthouse-common/CHANGELOG.md index 5d3950af16..ee90593ebc 100644 --- a/plugins/lighthouse-common/CHANGELOG.md +++ b/plugins/lighthouse-common/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-lighthouse-common +## 0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + +## 0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/lighthouse-common/catalog-info.yaml b/plugins/lighthouse-common/catalog-info.yaml new file mode 100644 index 0000000000..f5e8ead471 --- /dev/null +++ b/plugins/lighthouse-common/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-lighthouse-common + title: '@backstage/plugin-lighthouse-common' + description: >- + Common functionalities for lighthouse, to be shared between lighthouse and + lighthouse-backend plugin +spec: + lifecycle: experimental + type: backstage-common-library + owner: maintainers diff --git a/plugins/lighthouse-common/package.json b/plugins/lighthouse-common/package.json index c282fa1148..dbe9614656 100644 --- a/plugins/lighthouse-common/package.json +++ b/plugins/lighthouse-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-lighthouse-common", "description": "Common functionalities for lighthouse, to be shared between lighthouse and lighthouse-backend plugin", - "version": "0.1.2", + "version": "0.1.3-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -26,6 +26,7 @@ "files": [ "dist" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index e45facf69f..9796bee15d 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-lighthouse +## 0.4.9-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-lighthouse-common@0.1.3-next.1 + - @backstage/theme@0.4.1 + +## 0.4.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-lighthouse-common@0.1.3-next.0 + - @backstage/theme@0.4.1 + +## 0.4.8-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-lighthouse-common@0.1.2 + +## 0.4.6 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-lighthouse-common@0.1.2 + +## 0.4.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.4.6-next.0 ### Patch Changes diff --git a/plugins/lighthouse/api-report.md b/plugins/lighthouse/api-report.md index 4ab851472a..9794fe1728 100644 --- a/plugins/lighthouse/api-report.md +++ b/plugins/lighthouse/api-report.md @@ -9,20 +9,22 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; import { LighthouseApi } from '@backstage/plugin-lighthouse-common'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const EmbeddedRouter: () => JSX.Element; +export const EmbeddedRouter: () => React_2.JSX.Element; // @public (undocumented) export const EntityLastLighthouseAuditCard: (props: { dense?: boolean | undefined; variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) -export const EntityLighthouseContent: () => JSX.Element; +export const EntityLighthouseContent: () => JSX_2.Element; // @public (undocumented) const isLighthouseAvailable: (entity: Entity) => boolean; @@ -33,13 +35,13 @@ export { isLighthouseAvailable as isPluginApplicableToEntity }; export const LastLighthouseAuditCard: (props: { dense?: boolean; variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const lighthouseApiRef: ApiRef; // @public (undocumented) -export const LighthousePage: () => JSX.Element; +export const LighthousePage: () => JSX_2.Element; // @public (undocumented) const lighthousePlugin: BackstagePlugin< @@ -54,5 +56,5 @@ export { lighthousePlugin }; export { lighthousePlugin as plugin }; // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; ``` diff --git a/plugins/lighthouse/catalog-info.yaml b/plugins/lighthouse/catalog-info.yaml new file mode 100644 index 0000000000..2f2f69a98d --- /dev/null +++ b/plugins/lighthouse/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-lighthouse + title: '@backstage/plugin-lighthouse' + description: A Backstage plugin that integrates towards Lighthouse +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 02b2e67c1a..d59f056ed0 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-lighthouse", "description": "A Backstage plugin that integrates towards Lighthouse", - "version": "0.4.6-next.0", + "version": "0.4.9-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,6 +23,7 @@ "backstage", "lighthouse" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", @@ -43,6 +44,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -60,8 +62,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/linguist-backend/CHANGELOG.md b/plugins/linguist-backend/CHANGELOG.md index 774dc72250..120397a5d9 100644 --- a/plugins/linguist-backend/CHANGELOG.md +++ b/plugins/linguist-backend/CHANGELOG.md @@ -1,5 +1,98 @@ # @backstage/plugin-linguist-backend +## 0.4.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-linguist-common@0.1.1 + +## 0.4.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-linguist-common@0.1.1 + +## 0.4.2-next.0 + +### Patch Changes + +- b2de501bda31: When creating the router using `createRouterFromConfig` or using the plugin for the new backend system the `linguist.useSourceLocation` configuration is now optional. +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-linguist-common@0.1.1 + +## 0.4.0 + +### Minor Changes + +- d440f1dd0e72: Adds a processor to the linguist backend which can automatically add language tags to entities +- 0896d85d5ab5: **BREAKING**: Removed the alpha export plugin options from exported `linguistPlugin()` (used by the new backend system) in favour of static config. + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- ca5e591cb86a: Fixed bug in LinguistBackendClient.ts file where if the linguistJsOptions is specified and sent over to the linguist-js package it would get changed (another attribute would be added) causing future entities of the batch to fail with an error +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.4.0-next.2 + +### Minor Changes + +- d440f1dd0e72: Adds a processor to the linguist backend which can automatically add language tags to entities + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-linguist-common@0.1.1-next.1 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + ## 0.3.2-next.1 ### Patch Changes diff --git a/plugins/linguist-backend/README.md b/plugins/linguist-backend/README.md index 7bca98191b..d2ed7f5f03 100644 --- a/plugins/linguist-backend/README.md +++ b/plugins/linguist-backend/README.md @@ -70,24 +70,32 @@ In your `packages/backend/src/index.ts` make the following changes: // ... other feature additions -+ const linguistSchedule: TaskScheduleDefinition = { -+ frequency: { minutes: 2 }, -+ timeout: { minutes: 15 }, -+ initialDelay: { seconds: 15 }, -+ }; - -+ backend.add( -+ linguistPlugin({ -+ schedule: linguistSchedule, -+ age: { days: 30 }, -+ batchSize: 2, -+ useSourceLocation: false, -+ }), -+ ); ++ backend.add(linguistPlugin()); backend.start(); ``` +The plugin options can be set through the `app-config.yaml`: + +```yaml +// ... + +linguist: + schedule: + frequency: + minutes: 2 + timeout: + minutes: 2 + initialDelay: + seconds: 15 + age: + days: 30 + batchSize: 2 + useSourceLocation: false + +// ... +``` + ## Plugin Option The Linguist backend has various plugin options that you can provide to the `createRouter` function in your `packages/backend/src/plugins/linguist.ts` file that will allow you to configure various aspects of how it works. The following sections go into the details of these options diff --git a/plugins/linguist-backend/api-report.md b/plugins/linguist-backend/api-report.md index aac978e426..95bdc858ee 100644 --- a/plugins/linguist-backend/api-report.md +++ b/plugins/linguist-backend/api-report.md @@ -27,6 +27,11 @@ export function createRouter( routerOptions: RouterOptions, ): Promise; +// @public (undocumented) +export function createRouterFromConfig( + routerOptions: RouterOptions, +): Promise; + // @public (undocumented) export interface LinguistBackendApi { // (undocumented) @@ -36,23 +41,8 @@ export interface LinguistBackendApi { } // @public -export const linguistPlugin: (options: LinguistPluginOptions) => BackendFeature; - -// @public -export interface LinguistPluginOptions { - // (undocumented) - age?: HumanDuration; - // (undocumented) - batchSize?: number; - // (undocumented) - kind?: string[]; - // (undocumented) - linguistJsOptions?: Record; - // (undocumented) - schedule?: TaskScheduleDefinition; - // (undocumented) - useSourceLocation?: boolean; -} +const linguistPlugin: () => BackendFeature; +export default linguistPlugin; // @public export class LinguistTagsProcessor implements CatalogProcessor { @@ -104,6 +94,8 @@ export interface PluginOptions { // @public (undocumented) export interface RouterOptions { + // (undocumented) + config?: Config; // (undocumented) database: PluginDatabaseManager; // (undocumented) diff --git a/plugins/linguist-backend/catalog-info.yaml b/plugins/linguist-backend/catalog-info.yaml new file mode 100644 index 0000000000..de7545d39b --- /dev/null +++ b/plugins/linguist-backend/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-linguist-backend + title: '@backstage/plugin-linguist-backend' +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: awanlin diff --git a/plugins/linguist-backend/config.d.ts b/plugins/linguist-backend/config.d.ts index e7ad487d12..758210feb7 100644 --- a/plugins/linguist-backend/config.d.ts +++ b/plugins/linguist-backend/config.d.ts @@ -14,11 +14,35 @@ * limitations under the License. */ +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { HumanDuration } from '@backstage/types'; +import { Options as LinguistJsOptions } from 'linguist-js/dist/types'; export interface Config { /** Configuration options for the linguist plugin */ linguist?: { + schedule?: TaskScheduleDefinition; + /** + * @default 20 + */ + batchSize?: number; + /** + * @default false + */ + useSourceLocation?: boolean; + /** + * Refresh generated language breakdown + */ + age?: HumanDuration; + /** + * @default ['API', 'Component', 'Template'] + */ + kind?: string[]; + /** + * [linguist-js](https://www.npmjs.com/package/linguist-js) options + */ + linguistJsOptions?: LinguistJsOptions; + /** Options for the tags processor */ tagsProcessor?: { /** diff --git a/plugins/linguist-backend/migrations/20221115_init.js b/plugins/linguist-backend/migrations/20221115_init.js index 3bcf99e08b..359f7c8359 100644 --- a/plugins/linguist-backend/migrations/20221115_init.js +++ b/plugins/linguist-backend/migrations/20221115_init.js @@ -32,7 +32,7 @@ exports.up = async function up(knex) { .comment('An insert counter to ensure ordering'); table.uuid('id').notNullable().comment('The ID of the Linguist result'); table - .text('entity_ref') + .string('entity_ref') .unique() .notNullable() .comment('The entity ref that this Linguist result applies to'); diff --git a/plugins/linguist-backend/package.json b/plugins/linguist-backend/package.json index ba71e3c756..0cbbfae851 100644 --- a/plugins/linguist-backend/package.json +++ b/plugins/linguist-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-linguist-backend", - "version": "0.3.2-next.1", + "version": "0.4.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/linguist-backend/src/index.ts b/plugins/linguist-backend/src/index.ts index 107877551e..df4703bed3 100644 --- a/plugins/linguist-backend/src/index.ts +++ b/plugins/linguist-backend/src/index.ts @@ -23,5 +23,4 @@ export * from './processor'; export * from './service/router'; export type { LinguistBackendApi } from './api'; -export { linguistPlugin } from './plugin'; -export type { LinguistPluginOptions } from './plugin'; +export { linguistPlugin as default } from './plugin'; diff --git a/plugins/linguist-backend/src/plugin.ts b/plugins/linguist-backend/src/plugin.ts index f655d6a1f4..cbf18d9ebe 100644 --- a/plugins/linguist-backend/src/plugin.ts +++ b/plugins/linguist-backend/src/plugin.ts @@ -19,69 +19,50 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { createRouter } from './service/router'; -import { TaskScheduleDefinition } from '@backstage/backend-tasks'; -import { HumanDuration } from '@backstage/types'; -/** - * Options for Linguist backend plugin - * - * @public - */ -export interface LinguistPluginOptions { - schedule?: TaskScheduleDefinition; - age?: HumanDuration; - batchSize?: number; - useSourceLocation?: boolean; - linguistJsOptions?: Record; - kind?: string[]; -} +import { createRouterFromConfig } from './service/router'; /** * Linguist backend plugin * * @public */ -export const linguistPlugin = createBackendPlugin( - (options: LinguistPluginOptions) => ({ - pluginId: 'linguist', - register(env) { - env.registerInit({ - deps: { - logger: coreServices.logger, - reader: coreServices.urlReader, - database: coreServices.database, - discovery: coreServices.discovery, - scheduler: coreServices.scheduler, - tokenManager: coreServices.tokenManager, - httpRouter: coreServices.httpRouter, - }, - async init({ - logger, - reader, - database, - discovery, - scheduler, - tokenManager, - httpRouter, - }) { - httpRouter.use( - await createRouter( - { - ...options, - }, - { - logger: loggerToWinstonLogger(logger), - reader, - database, - discovery, - scheduler, - tokenManager, - }, - ), - ); - }, - }); - }, - }), -); +export const linguistPlugin = createBackendPlugin({ + pluginId: 'linguist', + register(env) { + env.registerInit({ + deps: { + logger: coreServices.logger, + config: coreServices.rootConfig, + reader: coreServices.urlReader, + database: coreServices.database, + discovery: coreServices.discovery, + scheduler: coreServices.scheduler, + tokenManager: coreServices.tokenManager, + httpRouter: coreServices.httpRouter, + }, + async init({ + logger, + config, + reader, + database, + discovery, + scheduler, + tokenManager, + httpRouter, + }) { + httpRouter.use( + await createRouterFromConfig({ + logger: loggerToWinstonLogger(logger), + config, + reader, + database, + discovery, + scheduler, + tokenManager, + }), + ); + }, + }); + }, +}); diff --git a/plugins/linguist-backend/src/service/router.test.ts b/plugins/linguist-backend/src/service/router.test.ts index 7ab8f1c238..4048e1213b 100644 --- a/plugins/linguist-backend/src/service/router.test.ts +++ b/plugins/linguist-backend/src/service/router.test.ts @@ -25,7 +25,7 @@ import { import express from 'express'; import request from 'supertest'; import { ConfigReader } from '@backstage/config'; -import { createRouter } from './router'; +import { createRouter, createRouterFromConfig } from './router'; import { LinguistBackendApi } from '../api'; import { TaskScheduleDefinition } from '@backstage/backend-tasks'; @@ -69,26 +69,57 @@ describe('createRouter', () => { let linguistBackendApi: jest.Mocked; let app: express.Express; - beforeAll(async () => { - const router = await createRouter( - { schedule: schedule, age: { days: 30 }, useSourceLocation: false }, - { - linguistBackendApi: linguistBackendApi, - discovery: testDiscovery, - database: createDatabase(), - reader: mockUrlReader, - logger: getVoidLogger(), - tokenManager: mockedTokenManager, - }, - ); - app = express().use(router); - }); - beforeEach(() => { jest.resetAllMocks(); }); describe('GET /health', () => { + beforeAll(async () => { + const router = await createRouter( + { schedule: schedule, age: { days: 30 }, useSourceLocation: false }, + { + linguistBackendApi: linguistBackendApi, + discovery: testDiscovery, + database: createDatabase(), + reader: mockUrlReader, + logger: getVoidLogger(), + tokenManager: mockedTokenManager, + }, + ); + app = express().use(router); + }); + it('returns ok', async () => { + const response = await request(app).get('/health'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); + + describe('GET /health from config', () => { + beforeAll(async () => { + const config = new ConfigReader({ + linguist: { + schedule: { + frequency: { minutes: 2 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 15 }, + }, + age: { days: 30 }, + useSourceLocation: false, + }, + }); + const router = await createRouterFromConfig({ + linguistBackendApi: linguistBackendApi, + discovery: testDiscovery, + database: createDatabase(), + reader: mockUrlReader, + logger: getVoidLogger(), + config, + tokenManager: mockedTokenManager, + }); + app = express().use(router); + }); it('returns ok', async () => { const response = await request(app).get('/health'); diff --git a/plugins/linguist-backend/src/service/router.ts b/plugins/linguist-backend/src/service/router.ts index 9e45438ec8..d29943c38b 100644 --- a/plugins/linguist-backend/src/service/router.ts +++ b/plugins/linguist-backend/src/service/router.ts @@ -28,11 +28,13 @@ import { LinguistBackendApi } from '../api'; import { LinguistBackendDatabase } from '../db'; import { PluginTaskScheduler, + readTaskScheduleDefinitionFromConfig, TaskScheduleDefinition, } from '@backstage/backend-tasks'; import { HumanDuration } from '@backstage/types'; import { CatalogClient } from '@backstage/catalog-client'; import { LinguistBackendClient } from '../api/LinguistBackendClient'; +import { Config } from '@backstage/config'; /** @public */ export interface PluginOptions { @@ -53,6 +55,7 @@ export interface RouterOptions { database: PluginDatabaseManager; discovery: PluginEndpointDiscovery; scheduler?: PluginTaskScheduler; + config?: Config; } /** @public */ @@ -60,6 +63,9 @@ export async function createRouter( pluginOptions: PluginOptions, routerOptions: RouterOptions, ): Promise { + const { logger, reader, database, discovery, scheduler, tokenManager } = + routerOptions; + const { schedule, age, @@ -69,9 +75,6 @@ export async function createRouter( linguistJsOptions, } = pluginOptions; - const { logger, reader, database, discovery, scheduler, tokenManager } = - routerOptions; - const linguistBackendStore = await LinguistBackendDatabase.create( await database.getClient(), ); @@ -135,3 +138,27 @@ export async function createRouter( router.use(errorHandler()); return router; } + +/** @public */ +export async function createRouterFromConfig(routerOptions: RouterOptions) { + const { config } = routerOptions; + const pluginOptions: PluginOptions = {}; + if (config) { + if (config.has('linguist.schedule')) { + pluginOptions.schedule = readTaskScheduleDefinitionFromConfig( + config.getConfig('linguist.schedule'), + ); + } + pluginOptions.batchSize = config.getOptionalNumber('linguist.batchSize'); + pluginOptions.useSourceLocation = + config.getOptionalBoolean('linguist.useSourceLocation') ?? false; + pluginOptions.age = config.getOptionalConfig('linguist.age') as + | HumanDuration + | undefined; + pluginOptions.kind = config.getOptionalStringArray('linguist.kind'); + pluginOptions.linguistJsOptions = config.getOptionalConfig( + 'linguist.linguistJsOptions', + ); + } + return createRouter(pluginOptions, routerOptions); +} diff --git a/plugins/linguist-common/CHANGELOG.md b/plugins/linguist-common/CHANGELOG.md index 1bfe9fcc84..a4775c1b98 100644 --- a/plugins/linguist-common/CHANGELOG.md +++ b/plugins/linguist-common/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-linguist-common +## 0.1.1 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- d440f1dd0e72: Exported new LanguageType type alias + +## 0.1.1-next.1 + +### Patch Changes + +- d440f1dd0e72: Exported new LanguageType type alias + ## 0.1.1-next.0 ### Patch Changes diff --git a/plugins/linguist-common/catalog-info.yaml b/plugins/linguist-common/catalog-info.yaml new file mode 100644 index 0000000000..266daa4a75 --- /dev/null +++ b/plugins/linguist-common/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-linguist-common + title: '@backstage/plugin-linguist-common' + description: Common functionalities for the linguist plugin +spec: + lifecycle: experimental + type: backstage-common-library + owner: awanlin diff --git a/plugins/linguist-common/package.json b/plugins/linguist-common/package.json index 4ca2b65283..ef19649670 100644 --- a/plugins/linguist-common/package.json +++ b/plugins/linguist-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-linguist-common", "description": "Common functionalities for the linguist plugin", - "version": "0.1.1-next.0", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,6 +20,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/linguist-common" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/plugins/linguist/CHANGELOG.md b/plugins/linguist/CHANGELOG.md index 84af3bdd60..afabc727d2 100644 --- a/plugins/linguist/CHANGELOG.md +++ b/plugins/linguist/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-linguist +## 0.1.9-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-linguist-common@0.1.1 + +## 0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-linguist-common@0.1.1 + +## 0.1.8-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-linguist-common@0.1.1 + +## 0.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-linguist-common@0.1.1 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-linguist-common@0.1.1-next.1 + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.1.6-next.1 ### Patch Changes diff --git a/plugins/linguist/api-report.md b/plugins/linguist/api-report.md index 469b3b93c0..5182ce12f6 100644 --- a/plugins/linguist/api-report.md +++ b/plugins/linguist/api-report.md @@ -7,9 +7,10 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; // @public (undocumented) -export const EntityLinguistCard: () => JSX.Element; +export const EntityLinguistCard: () => JSX_2.Element; // @public (undocumented) export const isLinguistAvailable: (entity: Entity) => boolean; diff --git a/plugins/linguist/catalog-info.yaml b/plugins/linguist/catalog-info.yaml new file mode 100644 index 0000000000..aca70b7570 --- /dev/null +++ b/plugins/linguist/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-linguist + title: '@backstage/plugin-linguist' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: awanlin diff --git a/plugins/linguist/package.json b/plugins/linguist/package.json index 2f5369b404..e9a83ec99c 100644 --- a/plugins/linguist/package.json +++ b/plugins/linguist/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-linguist", - "version": "0.1.6-next.1", + "version": "0.1.9-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,6 +18,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/linguist" }, + "sideEffects": false, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", @@ -38,6 +39,7 @@ "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^2.0.2", "react-use": "^17.2.4", "slugify": "^1.6.4" @@ -56,8 +58,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/microsoft-calendar/CHANGELOG.md b/plugins/microsoft-calendar/CHANGELOG.md index 0cb4271d67..cfaef9de7e 100644 --- a/plugins/microsoft-calendar/CHANGELOG.md +++ b/plugins/microsoft-calendar/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-microsoft-calendar +## 0.1.7-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- 814feeed7343: Update to handle invalid luxon values +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.1.7-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.1.6 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- eea2922e749a: README update - example of apiRef definition and fixed component name +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + ## 0.1.6-next.1 ### Patch Changes diff --git a/plugins/microsoft-calendar/api-report.md b/plugins/microsoft-calendar/api-report.md index d6839f19ea..fad48e5de0 100644 --- a/plugins/microsoft-calendar/api-report.md +++ b/plugins/microsoft-calendar/api-report.md @@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { Calendar } from '@microsoft/microsoft-graph-types'; import { Event as Event_2 } from '@microsoft/microsoft-graph-types'; import { FetchApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { OAuthApi } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -33,7 +34,7 @@ export class MicrosoftCalendarApiClient { export const microsoftCalendarApiRef: ApiRef; // @public (undocumented) -export const MicrosoftCalendarCard: () => JSX.Element; +export const MicrosoftCalendarCard: () => JSX_2.Element; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/microsoft-calendar/catalog-info.yaml b/plugins/microsoft-calendar/catalog-info.yaml new file mode 100644 index 0000000000..82c3c82b49 --- /dev/null +++ b/plugins/microsoft-calendar/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-microsoft-calendar + title: '@backstage/plugin-microsoft-calendar' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/microsoft-calendar/package.json b/plugins/microsoft-calendar/package.json index 84e307ac2b..960b570b32 100644 --- a/plugins/microsoft-calendar/package.json +++ b/plugins/microsoft-calendar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-microsoft-calendar", - "version": "0.1.6-next.1", + "version": "0.1.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,6 +37,7 @@ "name": "abhaysoni" } ], + "sideEffects": false, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", @@ -55,6 +56,7 @@ "@material-ui/icons": "^4.9.1", "@microsoft/microsoft-graph-types": "^2.25.0", "@tanstack/react-query": "^4.1.3", + "@types/react": "^16.13.1 || ^17.0.0", "classnames": "^2.3.1", "dompurify": "^2.3.6", "lodash": "^4.17.21", @@ -76,8 +78,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/microsoft-calendar/src/components/CalendarCard.tsx b/plugins/microsoft-calendar/src/components/CalendarCard.tsx index dd94bad148..57c520ad0f 100644 --- a/plugins/microsoft-calendar/src/components/CalendarCard.tsx +++ b/plugins/microsoft-calendar/src/components/CalendarCard.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { sortBy } from 'lodash'; import { DateTime } from 'luxon'; import React, { useState } from 'react'; @@ -56,9 +57,9 @@ export const CalendarCard = () => { const { data: events, isLoading: isEventLoading } = useEventsQuery({ calendarId: selectedCalendarId || defaultCalendarId || '', enabled: isSignedIn && calendars.length > 0, - timeMin: date.startOf('day').toISO(), - timeMax: date.endOf('day').toISO(), - timeZone: date.zoneName, + timeMin: date.startOf('day').toISO()!, + timeMax: date.endOf('day').toISO()!, + timeZone: date.zoneName ?? undefined, }); const showLoader = diff --git a/plugins/newrelic-dashboard/CHANGELOG.md b/plugins/newrelic-dashboard/CHANGELOG.md index de7a9c6b5b..f0a505ec70 100644 --- a/plugins/newrelic-dashboard/CHANGELOG.md +++ b/plugins/newrelic-dashboard/CHANGELOG.md @@ -1,5 +1,58 @@ # @backstage/plugin-newrelic-dashboard +## 0.2.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + +## 0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + +## 0.2.16-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.2.14 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + +## 0.2.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.2.14-next.1 ### Patch Changes diff --git a/plugins/newrelic-dashboard/api-report.md b/plugins/newrelic-dashboard/api-report.md index 740fc46ef7..2e9b11a88e 100644 --- a/plugins/newrelic-dashboard/api-report.md +++ b/plugins/newrelic-dashboard/api-report.md @@ -7,6 +7,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public @@ -14,13 +15,13 @@ export const DashboardSnapshotComponent: (props: { guid: string; name: string; permalink: string; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) -export const EntityNewRelicDashboardCard: () => JSX.Element; +export const EntityNewRelicDashboardCard: () => JSX_2.Element; // @public (undocumented) -export const EntityNewRelicDashboardContent: () => JSX.Element; +export const EntityNewRelicDashboardContent: () => JSX_2.Element; // @public (undocumented) export const isNewRelicDashboardAvailable: (entity: Entity) => boolean; diff --git a/plugins/newrelic-dashboard/catalog-info.yaml b/plugins/newrelic-dashboard/catalog-info.yaml new file mode 100644 index 0000000000..d944c9cb6b --- /dev/null +++ b/plugins/newrelic-dashboard/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-newrelic-dashboard + title: '@backstage/plugin-newrelic-dashboard' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/newrelic-dashboard/package.json b/plugins/newrelic-dashboard/package.json index b32fd6c1e2..0b8da830ce 100644 --- a/plugins/newrelic-dashboard/package.json +++ b/plugins/newrelic-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-newrelic-dashboard", - "version": "0.2.14-next.1", + "version": "0.2.17-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,6 +18,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/newrelic-dashboard" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -36,6 +37,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -46,8 +48,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/dev-utils": "workspace:^", - "@testing-library/jest-dom": "^5.10.1", - "@types/react": "^16.13.1 || ^17.0.0" + "@testing-library/jest-dom": "^5.10.1" }, "files": [ "dist" diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md index db1fad33e4..009957e7cc 100644 --- a/plugins/newrelic/CHANGELOG.md +++ b/plugins/newrelic/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-newrelic +## 0.3.40-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/theme@0.4.1 + +## 0.3.40-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## 0.3.40-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + +## 0.3.39 + +### Patch Changes + +- 2167b7eab09b: The newrelic plugin now supports pagination when retrieving results from newrelic. It will no longer truncate results. To see all applications, the link header will need to be allowed through the proxy (see the newrelic plugin readme). +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/theme@0.4.1 + ## 0.3.39-next.0 ### Patch Changes diff --git a/plugins/newrelic/README.md b/plugins/newrelic/README.md index 8ac7ed2c72..14049ba39c 100644 --- a/plugins/newrelic/README.md +++ b/plugins/newrelic/README.md @@ -18,6 +18,8 @@ APIs. target: https://api.newrelic.com/v2 headers: X-Api-Key: ${NEW_RELIC_REST_API_KEY} + allowedHeaders: + - link ``` There is some types of api key on new relic, to this use must be `User` type of key, In your production deployment of Backstage, you would also need to ensure that @@ -33,6 +35,8 @@ APIs. '/newrelic/apm/api': headers: X-Api-Key: NRRA-YourActualApiKey + allowedHeaders: + - link ``` Read more about how to find or generate this key in diff --git a/plugins/newrelic/api-report.md b/plugins/newrelic/api-report.md index 302b7c918a..04fd229465 100644 --- a/plugins/newrelic/api-report.md +++ b/plugins/newrelic/api-report.md @@ -6,10 +6,11 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const NewRelicPage: () => JSX.Element; +export const NewRelicPage: () => JSX_2.Element; // @public (undocumented) const newRelicPlugin: BackstagePlugin< diff --git a/plugins/newrelic/catalog-info.yaml b/plugins/newrelic/catalog-info.yaml new file mode 100644 index 0000000000..bdf6aa5757 --- /dev/null +++ b/plugins/newrelic/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-newrelic + title: '@backstage/plugin-newrelic' + description: A Backstage plugin that integrates towards New Relic +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index b09050c599..1f4b3a2e7f 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-newrelic", "description": "A Backstage plugin that integrates towards New Relic", - "version": "0.3.39-next.0", + "version": "0.3.40-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,6 +23,7 @@ "backstage", "newrelic" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -39,6 +40,8 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", + "parse-link-header": "^2.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -47,6 +50,7 @@ "react-router-dom": "6.0.0-beta.0 || ^6.3.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", @@ -55,10 +59,9 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", + "@types/parse-link-header": "^2.0.1", "cross-fetch": "^3.1.5", - "msw": "^1.0.0" + "msw": "^1.2.3" }, "files": [ "dist" diff --git a/plugins/newrelic/src/api/index.test.ts b/plugins/newrelic/src/api/index.test.ts new file mode 100644 index 0000000000..d4ed4a1691 --- /dev/null +++ b/plugins/newrelic/src/api/index.test.ts @@ -0,0 +1,384 @@ +/* + * 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 { NewRelicApplication, NewRelicClient } from '.'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; + +const mockedDiscoveryApi: DiscoveryApi = { + getBaseUrl: async () => 'https://test.test', +}; + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('NewRelicClient', () => { + const server = setupServer(); + setupRequestMockHandlers(server); + + beforeEach(() => { + server.resetHandlers(); + }); + + test.each([ + ['https://test.test/BASEPATH/apm/api/applications.json', '/BASEPATH'], + ['https://test.test/BASEPATH2/apm/api/applications.json', '/BASEPATH2'], + ['https://test.testBASEPATH3/apm/api/applications.json', 'BASEPATH3'], + ['https://test.test/newrelic/apm/api/applications.json', undefined], + ])( + 'It correctly forms the request url (%p) when proxyPathBase is %p', + async (expectedUrl, basePathOverride) => { + server.use( + rest.get(expectedUrl, (_, res, ctx) => + res(ctx.status(200), ctx.json({ applications: [] })), + ), + ); + + const mockedFetchApi = new MockFetchApi(); + const fetchSpy = jest.spyOn(mockedFetchApi, 'fetch'); + + const client = new NewRelicClient({ + discoveryApi: mockedDiscoveryApi, + fetchApi: mockedFetchApi, + proxyPathBase: basePathOverride, + }); + await client.getApplications(); + + expect(fetchSpy).toHaveBeenCalledWith(expectedUrl); + }, + ); + + it('Correctly reads all pages of results and returns the expected results', async () => { + const mockedApplicationOne: NewRelicApplication = { + id: 1, + application_summary: { + apdex_score: 0, + error_rate: 100, + host_count: 500, + instance_count: 5000, + response_time: 20, + throughput: 500000, + }, + name: 'Testing Application #1', + language: 'en-us', + health_status: 'Failing', + reporting: true, + settings: { + app_apdex_threshold: 0, + end_user_apdex_threshold: 0, + enable_real_user_monitoring: true, + use_server_side_config: true, + }, + }; + + const mockedApplicationTwo: NewRelicApplication = { + id: 2, + application_summary: { + apdex_score: -900, + error_rate: 0, + host_count: 0, + instance_count: 0, + response_time: 0, + throughput: 0, + }, + name: 'Testing Application #2', + language: 'en-us', + health_status: 'Working', + reporting: true, + settings: { + app_apdex_threshold: 0, + end_user_apdex_threshold: 0, + enable_real_user_monitoring: true, + use_server_side_config: true, + }, + }; + + const mockedApplicationThree: NewRelicApplication = { + id: 3, + application_summary: { + apdex_score: -900, + error_rate: 0, + host_count: 0, + instance_count: 0, + response_time: 0, + throughput: 0, + }, + name: 'Testing Application #3', + language: 'en-us', + health_status: 'Waiting', + reporting: false, + settings: { + app_apdex_threshold: 1000, + end_user_apdex_threshold: 500, + enable_real_user_monitoring: false, + use_server_side_config: false, + }, + }; + + const queryToRequestData = new Map< + string | null, + { link?: string | string[]; apps: NewRelicApplication[] } + >([ + [ + null, + { + link: [ + '; rel="next"', + '; rel="bad"', + ], + apps: [mockedApplicationOne], + }, + ], + [ + '2', + { + link: '; rel="next",', + apps: [], + }, + ], + [ + '3', + { + apps: [mockedApplicationTwo, mockedApplicationThree], + }, + ], + ]); + + const mockedFetchApi = new MockFetchApi(); + const fetchSpy = jest.spyOn(mockedFetchApi, 'fetch'); + + server.use( + rest.get( + 'https://test.test/newrelic/apm/api/applications.json', + (req, res, ctx) => { + const nextPageNumber = req.url.searchParams.get('page'); + const requestData = queryToRequestData.get(nextPageNumber) ?? { + apps: [], + }; + + const { link, apps: applications } = requestData; + const statusTransform = ctx.status(200); + const responseBody = ctx.json({ applications }); + + if (!!link) { + return res(statusTransform, ctx.set({ link }), responseBody); + } + + return res(statusTransform, responseBody); + }, + ), + ); + + const client = new NewRelicClient({ + discoveryApi: mockedDiscoveryApi, + fetchApi: mockedFetchApi, + }); + + const actual = await client.getApplications(); + const expected = { + applications: [ + mockedApplicationOne, + mockedApplicationTwo, + mockedApplicationThree, + ], + }; + + expect(fetchSpy).toHaveBeenCalledTimes(3); + expect(fetchSpy).toHaveBeenCalledWith( + 'https://test.test/newrelic/apm/api/applications.json', + ); + expect(fetchSpy).toHaveBeenCalledWith( + 'https://test.test/newrelic/apm/api/applications.json?page=2', + ); + expect(fetchSpy).toHaveBeenCalledWith( + 'https://test.test/newrelic/apm/api/applications.json?page=3', + ); + expect(actual).toStrictEqual(expected); + }); + + test.each([['Link'], ['LINK'], ['lINK']])( + 'It does not attempt pagination when the link header name is invalid (%p)', + async linkHeaderName => { + const mockedFetchApi = new MockFetchApi(); + const fetchSpy = jest.spyOn(mockedFetchApi, 'fetch'); + + server.use( + rest.get( + 'https://test.test/newrelic/apm/api/applications.json', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set( + linkHeaderName, + '; rel="next"', + ), + ctx.json({ applications: [] }), + ), + ), + rest.get('https://test.test/badroute', () => { + throw new Error( + 'NewRelicClient attempted to paginate when it should not have', + ); + }), + ); + + const client = new NewRelicClient({ + discoveryApi: mockedDiscoveryApi, + fetchApi: mockedFetchApi, + }); + await client.getApplications(); + + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(fetchSpy).toHaveBeenCalledWith( + 'https://test.test/newrelic/apm/api/applications.json', + ); + }, + ); + + test.each([ + [''], + ['<> rel=""'], + ['<>; rel=""'], + ['; rel=""'], + ['<>; rel:"value"'], + ['; rel: "next"'], + ['ABCDE'], + ['; rel="next",'], + ])( + 'It does not attempt pagination when the link header value is invalid (%p)', + async linkHeaderValue => { + const mockedFetchApi = new MockFetchApi(); + const fetchSpy = jest.spyOn(mockedFetchApi, 'fetch'); + + server.use( + rest.get( + 'https://test.test/newrelic/apm/api/applications.json', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('link', linkHeaderValue), + ctx.json({ applications: [] }), + ), + ), + rest.get('https://test.test/badroute', () => { + throw new Error( + 'NewRelicClient attempted to paginate when it should not have', + ); + }), + ); + + const client = new NewRelicClient({ + discoveryApi: mockedDiscoveryApi, + fetchApi: mockedFetchApi, + }); + await client.getApplications(); + + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(fetchSpy).toHaveBeenCalledWith( + 'https://test.test/newrelic/apm/api/applications.json', + ); + }, + ); + + test.each([ + ['Error communicating with New Relic: Not Found', 404, JSON.stringify({})], + [ + 'Error communicating with New Relic: ERROR TITLE', + 404, + JSON.stringify({ + error: { + title: 'ERROR TITLE', + }, + }), + ], + [ + 'Error communicating with New Relic: Internal Server Error', + 500, + JSON.stringify(undefined), + ], + [ + 'Error communicating with New Relic: Internal Server Error', + 500, + JSON.stringify(null), + ], + [ + 'Error communicating with New Relic: Internal Server Error', + 500, + ' { + server.use( + rest.get( + 'https://test.test/newrelic/apm/api/applications.json', + (_, res, ctx) => res(ctx.status(statusCode), ctx.body(body)), + ), + ); + + const client = new NewRelicClient({ + discoveryApi: mockedDiscoveryApi, + fetchApi: new MockFetchApi(), + }); + + await expect(client.getApplications()).rejects.toThrow( + expectedErrorMessage, + ); + }, + ); + + it('Throws an error when the body is invalid json but the status code is 200', async () => { + server.use( + rest.get( + 'https://test.test/newrelic/apm/api/applications.json', + (_, res, ctx) => res(ctx.status(200), ctx.body(' { + const getBaseUrlSpy = jest.spyOn(mockedDiscoveryApi, 'getBaseUrl'); + + server.use( + rest.get( + 'https://test.test/newrelic/apm/api/applications.json', + (_, res, ctx) => res(ctx.status(200), ctx.json({ applications: [] })), + ), + ); + + const client = new NewRelicClient({ + discoveryApi: mockedDiscoveryApi, + fetchApi: new MockFetchApi(), + }); + + await client.getApplications(); + await client.getApplications(); + await client.getApplications(); + await client.getApplications(); + + expect(getBaseUrlSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/plugins/newrelic/src/api/index.ts b/plugins/newrelic/src/api/index.ts index 2734dd2b4f..b37575b163 100644 --- a/plugins/newrelic/src/api/index.ts +++ b/plugins/newrelic/src/api/index.ts @@ -14,7 +14,13 @@ * limitations under the License. */ -import { createApiRef, DiscoveryApi } from '@backstage/core-plugin-api'; +import { + createApiRef, + DiscoveryApi, + FetchApi, +} from '@backstage/core-plugin-api'; + +import parseLinkHeader from 'parse-link-header'; export type NewRelicApplication = { id: number; @@ -61,6 +67,7 @@ const DEFAULT_PROXY_PATH_BASE = '/newrelic'; type Options = { discoveryApi: DiscoveryApi; + fetchApi: FetchApi; /** * Path to use for requests via the proxy, defaults to /newrelic */ @@ -71,39 +78,72 @@ export interface NewRelicApi { getApplications(): Promise; } +interface NewRelicPageReadResult { + nextPageUrl: string | undefined; + applicationsFromReadPage: NewRelicApplication[]; +} + export class NewRelicClient implements NewRelicApi { private readonly discoveryApi: DiscoveryApi; + private readonly fetchApi: FetchApi; private readonly proxyPathBase: string; + private baseUrl: string; constructor(options: Options) { this.discoveryApi = options.discoveryApi; + this.fetchApi = options.fetchApi; this.proxyPathBase = options.proxyPathBase ?? DEFAULT_PROXY_PATH_BASE; + this.baseUrl = ''; } async getApplications(): Promise { - const url = await this.getApiUrl('apm', 'applications.json'); - const response = await fetch(url); - let responseJson; - - try { - responseJson = await response.json(); - } catch (e) { - responseJson = { applications: [] }; + if (!this.baseUrl) { + const proxyUrl = await this.discoveryApi.getBaseUrl('proxy'); + this.baseUrl = `${proxyUrl}${this.proxyPathBase}/apm/api/applications.json`; } - if (response.status !== 200) { + const applications: NewRelicApplication[] = []; + let targetUrl = this.baseUrl; + + do { + const { nextPageUrl, applicationsFromReadPage } = + await this.fetchNewRelic(targetUrl); + + targetUrl = nextPageUrl ?? ''; + applications.push(...applicationsFromReadPage); + } while (!!targetUrl); + + return { applications }; + } + + private async fetchNewRelic( + targetUrl: string, + ): Promise { + const response = await this.fetchApi.fetch(targetUrl); + + if (!response.ok) { + let specificErrorTitle = undefined; + try { + specificErrorTitle = (await response.json())?.error?.title; + } catch (e) { + /* empty */ + } + throw new Error( `Error communicating with New Relic: ${ - responseJson?.error?.title || response.statusText + specificErrorTitle || response.statusText }`, ); } - return responseJson; - } + const readResponse = (await response.json()) as NewRelicApplications; + const linkHeader = response.headers.get('link'); + const parseResult = parseLinkHeader(linkHeader); + const nextPageNumber = parseResult?.next?.page; - private async getApiUrl(product: string, path: string) { - const proxyUrl = await this.discoveryApi.getBaseUrl('proxy'); - return `${proxyUrl}${this.proxyPathBase}/${product}/api/${path}`; + return { + nextPageUrl: nextPageNumber && `${this.baseUrl}?page=${nextPageNumber}`, + applicationsFromReadPage: readResponse.applications, + }; } } diff --git a/plugins/newrelic/src/plugin.ts b/plugins/newrelic/src/plugin.ts index 0387c50aa9..da7cae9b93 100644 --- a/plugins/newrelic/src/plugin.ts +++ b/plugins/newrelic/src/plugin.ts @@ -20,6 +20,7 @@ import { createPlugin, createRouteRef, discoveryApiRef, + fetchApiRef, createRoutableExtension, } from '@backstage/core-plugin-api'; @@ -33,8 +34,12 @@ export const newRelicPlugin = createPlugin({ apis: [ createApiFactory({ api: newRelicApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new NewRelicClient({ discoveryApi }), + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ discoveryApi, fetchApi }) => + new NewRelicClient({ discoveryApi, fetchApi }), }), ], routes: { diff --git a/plugins/nomad-backend/CHANGELOG.md b/plugins/nomad-backend/CHANGELOG.md index 37a5ba36cf..f114e98227 100644 --- a/plugins/nomad-backend/CHANGELOG.md +++ b/plugins/nomad-backend/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/plugin-nomad-backend +## 0.1.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/errors@1.2.1 + +## 0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/errors@1.2.1 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## 0.1.2 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## 0.1.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + ## 0.1.2-next.1 ### Patch Changes diff --git a/plugins/nomad-backend/catalog-info.yaml b/plugins/nomad-backend/catalog-info.yaml new file mode 100644 index 0000000000..7364d1920e --- /dev/null +++ b/plugins/nomad-backend/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-nomad-backend + title: '@backstage/plugin-nomad-backend' +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: maintainers diff --git a/plugins/nomad-backend/package.json b/plugins/nomad-backend/package.json index 78a3e3a386..d55bb9cacf 100644 --- a/plugins/nomad-backend/package.json +++ b/plugins/nomad-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-nomad-backend", - "version": "0.1.2-next.1", + "version": "0.1.5-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/nomad/CHANGELOG.md b/plugins/nomad/CHANGELOG.md index 151657c12b..52d0134701 100644 --- a/plugins/nomad/CHANGELOG.md +++ b/plugins/nomad/CHANGELOG.md @@ -1,5 +1,63 @@ # @backstage/plugin-nomad +## 0.1.5-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## 0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## 0.1.4-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.1.2 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/theme@0.4.1 + +## 0.1.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.1.2-next.1 ### Patch Changes diff --git a/plugins/nomad/api-report.md b/plugins/nomad/api-report.md index f000854374..c2d06b9892 100644 --- a/plugins/nomad/api-report.md +++ b/plugins/nomad/api-report.md @@ -7,13 +7,14 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public -export const EntityNomadAllocationListTable: () => JSX.Element; +export const EntityNomadAllocationListTable: () => JSX_2.Element; // @public -export const EntityNomadJobVersionListCard: () => JSX.Element | null; +export const EntityNomadJobVersionListCard: () => JSX_2.Element | null; // @public (undocumented) export const isNomadAllocationsAvailable: (entity: Entity) => boolean; diff --git a/plugins/nomad/catalog-info.yaml b/plugins/nomad/catalog-info.yaml new file mode 100644 index 0000000000..d704846059 --- /dev/null +++ b/plugins/nomad/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-nomad + title: '@backstage/plugin-nomad' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/nomad/package.json b/plugins/nomad/package.json index 283b6cdb9c..ddfb7d49da 100644 --- a/plugins/nomad/package.json +++ b/plugins/nomad/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-nomad", - "version": "0.1.2-next.1", + "version": "0.1.5-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,6 +18,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/nomad" }, + "sideEffects": false, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", @@ -37,6 +38,7 @@ "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.60", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.3.0", "react-use": "^17.2.4" }, @@ -53,7 +55,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "*", "cross-fetch": "^3.1.5", "msw": "^1.0.0" }, diff --git a/plugins/octopus-deploy/CHANGELOG.md b/plugins/octopus-deploy/CHANGELOG.md index b6a652e24d..e9b6f1b814 100644 --- a/plugins/octopus-deploy/CHANGELOG.md +++ b/plugins/octopus-deploy/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-octopus-deploy +## 0.2.6-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## 0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## 0.2.5-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.2.3 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## 0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.2.3-next.1 ### Patch Changes diff --git a/plugins/octopus-deploy/api-report.md b/plugins/octopus-deploy/api-report.md index 53c92403bd..713d99aa8f 100644 --- a/plugins/octopus-deploy/api-report.md +++ b/plugins/octopus-deploy/api-report.md @@ -10,11 +10,12 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { FetchApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; // @public (undocumented) export const EntityOctopusDeployContent: (props: { defaultLimit?: number | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const isOctopusDeployAvailable: (entity: Entity) => boolean; diff --git a/plugins/octopus-deploy/catalog-info.yaml b/plugins/octopus-deploy/catalog-info.yaml new file mode 100644 index 0000000000..390b91f6e3 --- /dev/null +++ b/plugins/octopus-deploy/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-octopus-deploy + title: '@backstage/plugin-octopus-deploy' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/octopus-deploy/package.json b/plugins/octopus-deploy/package.json index 65010ae66f..d6b02b221b 100644 --- a/plugins/octopus-deploy/package.json +++ b/plugins/octopus-deploy/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-octopus-deploy", - "version": "0.2.3-next.1", + "version": "0.2.6-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,6 +18,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/octopus-deploy" }, + "sideEffects": false, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", @@ -36,6 +37,7 @@ "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -52,8 +54,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/opencost/.eslintrc.js b/plugins/opencost/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/opencost/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/opencost/CHANGELOG.md b/plugins/opencost/CHANGELOG.md new file mode 100644 index 0000000000..48cfe54ddc --- /dev/null +++ b/plugins/opencost/CHANGELOG.md @@ -0,0 +1,33 @@ +# @backstage/plugin-opencost + +## 0.2.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/theme@0.4.1 + +## 0.2.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## 0.2.0-next.0 + +### Minor Changes + +- 1380a689ab56: New OpenCost plugin provides an port of the latest OpenCost UI to Backstage with updated dependencies. The plugin's README covers installation and configuration + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 diff --git a/plugins/opencost/README.md b/plugins/opencost/README.md new file mode 100644 index 0000000000..39c5e7ee2e --- /dev/null +++ b/plugins/opencost/README.md @@ -0,0 +1,79 @@ +# OpenCost + +Welcome to the [OpenCost](https://opencost.io) plugin! + +Currently this is a port of the [OpenCost UI](https://github.com/opencost/opencost/tree/develop/ui), but we will continue to expand it to expose any relevant data or pre-configured views that may be preferred. + +All of the code was originally ported from https://github.com/opencost/opencost/blob/develop/ui/ which is under by the Apache v2 License and also managed by the CNCF. + +## Installation + +1. Add the OpenCost dependency to the `packages/app/package.json`: + ```sh + # From your Backstage root directory + yarn add --cwd packages/app @backstage/plugin-opencost + ``` +2. Add the `OpenCostPage` to your `packages/app/src/App.tsx`: + + ```tsx + import { OpenCostPage } from '@backstage/plugin-opencost'; + ``` + +and + + ```tsx + + … + } /> + + ``` + +3. Add link to OpenCost to your sidebar + + ```typescript + // packages/app/src/components/Root/Root.tsx + import MoneyIcon from '@material-ui/icons/MonetizationOn'; + + ... + + export const Root = ({ children }: PropsWithChildren<{}>) => ( + + + ... + + ... + + + ); + + ``` + +## Plugin Configuration + +Since OpenCost doesn't have any authentication at this point, you just need to give API access to the plugin to access your data. + +If you haven't set up an ingress rule, you can port-forward the API with + +``` +kubectl -n opencost port-forward deployment/opencost 9003 +``` + +Add the following to your `app-config.yaml`: + +```yaml +opencost: + baseUrl: http://localhost:9003 +``` + +## Ideas/Next Steps + +- More testing +- Use the OpenCost mascot for the sidebar logo +- Use the Backstage proxy to communicate with the OpenCost API if necessary for authentication +- Convert AllocationReport.js to use the [Backstage Table](https://backstage.io/storybook/?path=/story/data-display-table--default-table) +- Allow for user-provided default reports and/or disabling controls +- Support multiple hard-coded reports +- clean up deprecation warnings and upgrade to all the latest React components +- Fork(?) to support `Kubecost`, which could provide Alerts and Recommendations, similar to the Cost Explorer plugin + +![Screenshot](screenshot.png) diff --git a/plugins/opencost/api-report.md b/plugins/opencost/api-report.md new file mode 100644 index 0000000000..079f394618 --- /dev/null +++ b/plugins/opencost/api-report.md @@ -0,0 +1,23 @@ +## API Report File for "@backstage/plugin-opencost" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// @public (undocumented) +export const OpenCostPage: () => JSX_2.Element; + +// @public (undocumented) +export const openCostPlugin: BackstagePlugin< + { + root: RouteRef; + }, + {}, + {} +>; +``` diff --git a/plugins/opencost/catalog-info.yaml b/plugins/opencost/catalog-info.yaml new file mode 100644 index 0000000000..277562bb32 --- /dev/null +++ b/plugins/opencost/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-opencost + title: '@backstage/plugin-opencost' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/opencost/config.d.ts b/plugins/opencost/config.d.ts new file mode 100644 index 0000000000..0070bec7b7 --- /dev/null +++ b/plugins/opencost/config.d.ts @@ -0,0 +1,30 @@ +/* + * 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 interface Config { + /** + * Configurations for the OpenCost plugin + * + * @visibility frontend + */ + opencost?: { + /** + * The base URL for the OpenCost API + * @visibility frontend + */ + baseUrl: string; + }; +} diff --git a/plugins/opencost/dev/index.tsx b/plugins/opencost/dev/index.tsx new file mode 100644 index 0000000000..41b2bd5346 --- /dev/null +++ b/plugins/opencost/dev/index.tsx @@ -0,0 +1,27 @@ +/* + * 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 React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { openCostPlugin, OpenCostPage } from '../src/plugin'; + +createDevApp() + .registerPlugin(openCostPlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/opencost', + }) + .render(); diff --git a/plugins/opencost/package.json b/plugins/opencost/package.json new file mode 100644 index 0000000000..6a6db959dc --- /dev/null +++ b/plugins/opencost/package.json @@ -0,0 +1,62 @@ +{ + "name": "@backstage/plugin-opencost", + "version": "0.2.0-next.2", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin" + }, + "sideEffects": false, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/theme": "workspace:^", + "@date-io/luxon": "1.x", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.60", + "@material-ui/pickers": "^3.3.10", + "@material-ui/styles": "^4.11.5", + "@types/react": "^16.13.1 || ^17.0.0", + "axios": "^1.4.0", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "react-use": "^17.2.4", + "recharts": "^2.5.0" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0", + "react-router-dom": "*" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "cross-fetch": "^3.1.5", + "msw": "^1.0.0" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/opencost/screenshot.png b/plugins/opencost/screenshot.png new file mode 100644 index 0000000000..432b525cb1 Binary files /dev/null and b/plugins/opencost/screenshot.png differ diff --git a/plugins/opencost/src/components/AllocationChart/RangeChart.jsx b/plugins/opencost/src/components/AllocationChart/RangeChart.jsx new file mode 100644 index 0000000000..b1ce5a34ee --- /dev/null +++ b/plugins/opencost/src/components/AllocationChart/RangeChart.jsx @@ -0,0 +1,201 @@ +/* + * 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 React from 'react'; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from 'recharts'; +import { makeStyles } from '@material-ui/styles'; +import { reverse } from 'lodash'; +import { primary, greyscale, browns } from '../../constants/colors'; +import { toCurrency } from '../../util'; + +const useStyles = makeStyles({ + tooltip: { + borderRadius: 2, + background: 'rgba(255, 255, 255, 0.95)', + padding: 12, + }, + tooltipLineItem: { + fontSize: '1rem', + margin: 0, + marginBottom: 4, + padding: 0, + }, +}); + +function toBarLabels(allocationRange) { + const keyToFill = {}; + let p = 0; + let g = 0; + let b = 0; + + for (const { idle } of allocationRange) { + for (const allocation of idle) { + const key = allocation.name; + if (keyToFill[key] === undefined) { + // idle allocations are assigned grey + keyToFill[key] = greyscale[g]; + g = (g + 1) % greyscale.length; + } + } + } + + for (const { top } of allocationRange) { + for (const allocation of top) { + const key = allocation.name; + if (keyToFill[key] === undefined) { + if (key === '__unallocated__') { + // unallocated gets black (clean up) + keyToFill[key] = '#212121'; + } else { + // non-idle allocations get the next available color + keyToFill[key] = primary[p]; + p = (p + 1) % primary.length; + } + } + } + } + + for (const { other } of allocationRange) { + for (const allocation of other) { + const key = allocation.name; + if (keyToFill[key] === undefined) { + // idle allocations are assigned grey + keyToFill[key] = browns[b]; + b = (b + 1) % browns.length; + } + } + } + + const labels = []; + for (const key in keyToFill) { + if (Object.hasOwn(keyToFill, key)) { + labels.push({ + dataKey: key, + fill: keyToFill[key], + }); + } + } + + return reverse(labels); +} + +function toBar(datum) { + const { top, other, idle } = datum; + const bar = {}; + + for (const key in top) { + if (Object.hasOwn(top, key)) { + const allocation = top[key]; + const start = new Date(allocation.start); + bar.start = `${start.getUTCFullYear()}-${ + start.getUTCMonth() + 1 + }-${start.getUTCDate()}`; + bar[allocation.name] = allocation.totalCost; + } + } + + for (const key in other) { + if (Object.hasOwn(other, key)) { + const allocation = other[key]; + const start = new Date(allocation.start); + bar.start = `${start.getUTCFullYear()}-${ + start.getUTCMonth() + 1 + }-${start.getUTCDate()}`; + bar[allocation.name] = allocation.totalCost; + } + } + + for (const key in idle) { + if (Object.hasOwn(idle, key)) { + const allocation = idle[key]; + const start = new Date(allocation.start); + bar.start = `${start.getUTCFullYear()}-${ + start.getUTCMonth() + 1 + }-${start.getUTCDate()}`; + bar[allocation.name] = allocation.totalCost; + } + } + + return bar; +} + +const RangeChart = ({ data, currency, height }) => { + const classes = useStyles(); + + const barData = data.map(toBar); + const barLabels = toBarLabels(data); + + const CustomTooltip = params => { + const { active, payload } = params; + + if (!payload || payload.length === 0) { + return null; + } + + const total = payload.reduce((sum, item) => sum + item.value, 0.0); + if (active) { + /* eslint react/forbid-elements: [0, { allow: ["warning"] }] */ + return ( +
+

{`Total: ${toCurrency(total, currency)}`}

+ {reverse(payload).map((item, i) => ( +

{`${item.name}: ${toCurrency(item.value, currency)}`}

+ ))} +
+ ); + } + + return null; + }; + + return ( + + + + + + } /> + {barLabels.map((barLabel, i) => ( + + ))} + + + ); +}; + +export default RangeChart; diff --git a/plugins/opencost/src/components/AllocationChart/SummaryChart.jsx b/plugins/opencost/src/components/AllocationChart/SummaryChart.jsx new file mode 100644 index 0000000000..53c0842845 --- /dev/null +++ b/plugins/opencost/src/components/AllocationChart/SummaryChart.jsx @@ -0,0 +1,127 @@ +/* + * 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 React from 'react'; +import { ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'; +import { primary, greyscale, browns } from '../../constants/colors'; +import { toCurrency } from '../../util'; + +function toPieData(top, other, idle) { + const slices = []; + + for (const i in top) { + if (Object.hasOwn(top, i)) { + const allocation = top[i]; + const fill = + allocation.name === '__unallocated__' + ? '#212121' + : primary[i % primary.length]; + + slices.push({ + name: allocation.name, + value: allocation.totalCost, + fill: fill, + }); + } + } + + for (const i in other) { + if (Object.hasOwn(other, i)) { + const allocation = other[i]; + const fill = browns[i % browns.length]; + slices.push({ + name: allocation.name, + value: allocation.totalCost, + fill: fill, + }); + } + } + + for (const i in idle) { + if (Object.hasOwn(idle, i)) { + const allocation = idle[i]; + const fill = greyscale[i % greyscale.length]; + slices.push({ + name: allocation.name, + value: allocation.totalCost, + fill: fill, + }); + } + } + + return slices; +} + +const SummaryChart = ({ top, other, idle, currency, height }) => { + const pieData = toPieData(top, other, idle); + + const renderLabel = params => { + const { cx, cy, midAngle, outerRadius, percent, name, fill, value } = + params; + + const RADIAN = Math.PI / 180; + const radius = outerRadius * 1.1; + let x = cx + radius * Math.cos(-midAngle * RADIAN); + x += x > cx ? 2 : -2; + const y = cy + radius * Math.sin(-midAngle * RADIAN); + // y -= Math.min(Math.abs(2 / Math.cos(-midAngle * RADIAN)), 8) + + if (percent < 0.02) { + return undefined; + } + + return ( + cx ? 'start' : 'end'} + dominantBaseline="central" + > + {`${name}: ${toCurrency(value, currency)} (${(percent * 100).toFixed( + 1, + )}%)`} + + ); + }; + + return ( + + + + {pieData.map((datum, i) => ( + + ))} + + + + ); +}; + +export default SummaryChart; diff --git a/plugins/opencost/src/components/AllocationChart/index.jsx b/plugins/opencost/src/components/AllocationChart/index.jsx new file mode 100644 index 0000000000..9fec3e296f --- /dev/null +++ b/plugins/opencost/src/components/AllocationChart/index.jsx @@ -0,0 +1,108 @@ +/* + * 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 React from 'react'; +import { isArray, filter, map, reduce, reverse, sortBy } from 'lodash'; + +import Typography from '@material-ui/core/Typography'; + +import RangeChart from './RangeChart'; +import SummaryChart from './SummaryChart'; + +// TODO niko/etl +/* eslint @typescript-eslint/no-shadow: ["error", { "allow": ["agg"] }]*/ +function agg(allocationSet, name) { + if (allocationSet.length === 0) { + return null; + } + + return reduce( + allocationSet, + (agg, cur) => ({ + name: agg.name, + aggregatedBy: cur.aggregatedBy, + properties: agg.properties, + start: cur.start, + end: cur.end, + cpuCost: agg.cpuCost + cur.cpuCost, + gpuCost: agg.gpuCost + cur.gpuCost, + ramCost: agg.ramCost + cur.ramCost, + pvCost: agg.pvCost + cur.pvCost, + totalCost: agg.totalCost + cur.totalCost, + count: agg.count + 1, + }), + { + name: name, + properties: null, + cpuCost: 0.0, + gpuCost: 0.0, + ramCost: 0.0, + pvCost: 0.0, + totalCost: 0.0, + count: 0, + }, + ); +} + +function isIdle(allocation) { + return allocation.name.indexOf('__idle__') >= 0; +} + +function top(n, by) { + return allocations => { + if (isArray(allocations[0])) { + return map(allocations, top(n, by)); + } + + const sorted = reverse(sortBy(allocations, by)); + const active = filter(sorted, a => !isIdle(a)); + const idle = filter(sorted, a => isIdle(a)); + const topn = active.slice(0, n); + const other = []; + if (active.length > n) { + other.push(agg(active.slice(n), 'other')); + } + + return { + top: topn, + other: other, + idle: idle, + }; + }; +} + +const AllocationChart = ({ allocationRange, currency, n, height }) => { + if (allocationRange.length === 0) { + return No data; + } + + if (allocationRange.length === 1) { + const datum = top(n, alloc => alloc.totalCost)(allocationRange[0]); + return ( + + ); + } + + const data = top(n, alloc => alloc.totalCost)(allocationRange); + return ; +}; + +export default React.memo(AllocationChart); diff --git a/plugins/opencost/src/components/AllocationReport.jsx b/plugins/opencost/src/components/AllocationReport.jsx new file mode 100644 index 0000000000..52686a741c --- /dev/null +++ b/plugins/opencost/src/components/AllocationReport.jsx @@ -0,0 +1,261 @@ +/* not changing the working code at this point */ +/* eslint no-nested-ternary: 0 */ +/* eslint react-hooks/rules-of-hooks: 0 */ + +/* + * 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 React, { useEffect, useState } from 'react'; +import { get, round } from 'lodash'; +import { makeStyles } from '@material-ui/styles'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TablePagination from '@material-ui/core/TablePagination'; +import TableRow from '@material-ui/core/TableRow'; +import TableSortLabel from '@material-ui/core/TableSortLabel'; +import Typography from '@material-ui/core/Typography'; +import AllocationChart from './AllocationChart'; +import { toCurrency } from '../util'; + +const useStyles = makeStyles({ + noResults: { + padding: 24, + }, +}); + +function descendingComparator(a, b, orderBy) { + if (get(b, orderBy) < get(a, orderBy)) { + return -1; + } + if (get(b, orderBy) > get(a, orderBy)) { + return 1; + } + return 0; +} + +function getComparator(order, orderBy) { + return order === 'desc' + ? (a, b) => descendingComparator(a, b, orderBy) + : (a, b) => -descendingComparator(a, b, orderBy); +} + +function stableSort(array, comparator) { + const stabilizedThis = array.map((el, index) => [el, index]); + stabilizedThis.sort((a, b) => { + const order = comparator(a[0], b[0]); + if (order !== 0) return order; + return a[1] - b[1]; + }); + return stabilizedThis.map(el => el[0]); +} + +const headCells = [ + { id: 'name', numeric: false, label: 'Name', width: 'auto' }, + { id: 'cpuCost', numeric: true, label: 'CPU', width: 100 }, + { id: 'ramCost', numeric: true, label: 'RAM', width: 100 }, + { id: 'pvCost', numeric: true, label: 'PV', width: 100 }, + { id: 'totalEfficiency', numeric: true, label: 'Efficiency', width: 130 }, + { id: 'totalCost', numeric: true, label: 'Total cost', width: 130 }, +]; + +const AllocationReport = ({ + allocationData, + cumulativeData, + totalData, + currency, +}) => { + const classes = useStyles(); + + if (allocationData.length === 0) { + return ( + + No results + + ); + } + + const [order, setOrder] = React.useState('desc'); + const [orderBy, setOrderBy] = React.useState('totalCost'); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(25); + const numData = cumulativeData.length; + + useEffect(() => { + setPage(0); + }, [numData]); + + const lastPage = Math.floor(numData / rowsPerPage); + + const handleChangePage = (event, newPage) => setPage(newPage); + + const handleChangeRowsPerPage = event => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const handleRequestSort = (event, property) => { + const isDesc = orderBy === property && order === 'desc'; + setOrder(isDesc ? 'asc' : 'desc'); + setOrderBy(property); + }; + + const createSortHandler = property => event => + handleRequestSort(event, property); + + const orderedRows = stableSort(cumulativeData, getComparator(order, orderBy)); + const pageRows = orderedRows.slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage, + ); + + return ( +
+ + +
+ + + +
+ + + {headCells.map(cell => ( + + + {cell.label} + + + ))} + + + + + {headCells.map(cell => { + return ( + + {cell.numeric + ? cell.label === 'Efficiency' + ? totalData.totalEfficiency === 1.0 && + totalData.cpuReqCoreHrs === 0 && + totalData.ramReqByteHrs === 0 + ? 'Inf%' + : `${round(totalData.totalEfficiency * 100, 1)}%` + : toCurrency(totalData[cell.id], currency) + : totalData[cell.id]} + + ); + })} + + {pageRows.map((row, key) => { + if (row.name === '__unmounted__') { + row.name = 'Unmounted PVs'; + } + + const isIdle = row.name.indexOf('__idle__') >= 0; + const isUnallocated = row.name.indexOf('__unallocated__') >= 0; + const isUnmounted = row.name.indexOf('Unmounted PVs') >= 0; + + // Replace "efficiency" with Inf if there is usage w/o request + let efficiency = round(row.totalEfficiency * 100, 1); + if ( + row.totalEfficiency === 1.0 && + row.cpuReqCoreHrs === 0 && + row.ramReqByteHrs === 0 + ) { + efficiency = 'Inf'; + } + + // Do not allow drill-down for idle and unallocated rows + if (isIdle || isUnallocated || isUnmounted) { + return ( + + {row.name} + + {toCurrency(row.cpuCost, currency)} + + + {toCurrency(row.ramCost, currency)} + + + {toCurrency(row.pvCost, currency)} + + {isIdle ? ( + + ) : ( + {efficiency}% + )} + + {toCurrency(row.totalCost, currency)} + + + ); + } + + return ( + + {row.name} + + {toCurrency(row.cpuCost, currency)} + + + {toCurrency(row.ramCost, currency)} + + + {toCurrency(row.pvCost, currency)} + + {efficiency}% + + {toCurrency(row.totalCost, currency)} + + + ); + })} + +
+ + +
+ ); +}; + +export default React.memo(AllocationReport); diff --git a/plugins/opencost/src/components/Controls/Download.jsx b/plugins/opencost/src/components/Controls/Download.jsx new file mode 100644 index 0000000000..727188e5c1 --- /dev/null +++ b/plugins/opencost/src/components/Controls/Download.jsx @@ -0,0 +1,110 @@ +/* + * 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 React from 'react'; +import { get, forEach, reverse, round, sortBy } from 'lodash'; +import ExportIcon from '@material-ui/icons/GetApp'; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; + +const columns = [ + { + head: 'Name', + prop: 'name', + currency: false, + }, + { + head: 'CPU', + prop: 'cpuCost', + currency: true, + }, + { + head: 'GPU', + prop: 'gpuCost', + currency: true, + }, + { + head: 'RAM', + prop: 'ramCost', + currency: true, + }, + { + head: 'PV', + prop: 'pvCost', + currency: true, + }, + { + head: 'Network', + prop: 'networkCost', + currency: true, + }, + { + head: 'Shared', + prop: 'sharedCost', + currency: true, + }, + { + head: 'Total', + prop: 'totalCost', + currency: true, + }, +]; + +const toCSVLine = datum => { + const cols = []; + + forEach(columns, c => { + if (c.currency) { + cols.push(round(get(datum, c.prop, 0.0), 2)); + } else { + cols.push(`"${get(datum, c.prop, '')}"`); + } + }); + + return cols.join(','); +}; + +const DownloadControl = ({ cumulativeData, title }) => { + // downloadReport downloads a CSV of the cumulative allocation data + function downloadReport() { + // Build CSV + const head = columns.map(c => c.head).join(','); + const body = reverse(sortBy(cumulativeData, 'totalCost')) + .map(toCSVLine) + .join('\r\n'); + const csv = `${head}\r\n${body}`; + + // Create download link + const a = document.createElement('a'); + a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' })); + const filename = title.toLocaleLowerCase('en-US').replace(/\s/gi, '-'); + a.setAttribute('download', `${filename}-${Date.now()}.csv`); + + // Click the link + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } + + return ( + + + + + + ); +}; + +export default React.memo(DownloadControl); diff --git a/plugins/opencost/src/components/Controls/Edit.jsx b/plugins/opencost/src/components/Controls/Edit.jsx new file mode 100644 index 0000000000..f02f8cc111 --- /dev/null +++ b/plugins/opencost/src/components/Controls/Edit.jsx @@ -0,0 +1,106 @@ +/* + * 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 { makeStyles } from '@material-ui/styles'; +import FormControl from '@material-ui/core/FormControl'; +import InputLabel from '@material-ui/core/InputLabel'; +import MenuItem from '@material-ui/core/MenuItem'; +import Select from '@material-ui/core/Select'; + +import React from 'react'; + +import SelectWindow from '../SelectWindow'; + +const useStyles = makeStyles({ + wrapper: { + display: 'inline-flex', + }, + formControl: { + margin: 8, + minWidth: 120, + }, +}); + +function EditControl({ + windowOptions, + window, + setWindow, + aggregationOptions, + aggregateBy, + setAggregateBy, + accumulateOptions, + accumulate, + setAccumulate, + currencyOptions, + currency, + setCurrency, +}) { + const classes = useStyles(); + return ( +
+ + + Breakdown + + + + Resolution + + + + Currency + + +
+ ); +} + +export default React.memo(EditControl); diff --git a/plugins/opencost/src/components/Controls/index.jsx b/plugins/opencost/src/components/Controls/index.jsx new file mode 100644 index 0000000000..c63833a6ab --- /dev/null +++ b/plugins/opencost/src/components/Controls/index.jsx @@ -0,0 +1,57 @@ +/* + * 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 React from 'react'; +import DownloadControl from './Download'; +import EditControl from './Edit'; + +const Controls = ({ + windowOptions, + window, + setWindow, + aggregationOptions, + aggregateBy, + setAggregateBy, + accumulateOptions, + accumulate, + setAccumulate, + title, + cumulativeData, + currency, + currencyOptions, + setCurrency, +}) => { + return ( +
+ + +
+ ); +}; + +export default React.memo(Controls); diff --git a/plugins/opencost/src/components/Details.js b/plugins/opencost/src/components/Details.js new file mode 100644 index 0000000000..5245c9e9fa --- /dev/null +++ b/plugins/opencost/src/components/Details.js @@ -0,0 +1,287 @@ +/* + * 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 React, { memo, useEffect, useState } from 'react'; +import { forEach, get, reverse, round, sortBy } from 'lodash'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import ClusterIcon from '@material-ui/icons/GroupWork'; +import NodeIcon from '@material-ui/icons/Memory'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Warnings from './Warnings'; +import AllocationService from '../services/allocation'; +import { bytesToString, toCurrency } from '../util'; + +const Details = ({ + window, + namespace, + controllerKind, + controller, + pod, + currency, +}) => { + const [cluster, setCluster] = useState(''); + const [node, setNode] = useState(''); + + const [fetch, setFetch] = useState(true); + const [loading, setLoading] = useState(false); + const [errors, setErrors] = useState([]); + const [rows, setRows] = useState([]); + + useEffect(() => { + if (fetch) { + setCluster(''); + setNode(''); + fetchData(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fetch]); + + async function fetchData() { + setLoading(true); + setErrors([]); + + try { + const filters = []; + + if (cluster) { + filters.push({ + property: 'cluster', + value: cluster, + }); + } + + if (node) { + filters.push({ + property: 'node', + value: node, + }); + } + + if (namespace) { + filters.push({ + property: 'namespace', + value: namespace, + }); + } + + if (controllerKind) { + filters.push({ + property: 'controllerKind', + value: controllerKind, + }); + } + + if (controller) { + filters.push({ + property: 'controller', + value: controller, + }); + } + + if (pod) { + filters.push({ + property: 'pod', + value: pod, + }); + } + + const resp = await AllocationService.fetchAllocation(window, '', { + accumulate: true, + }); + + let data = []; + forEach(resp.data[0], datum => { + if (datum.name === '__idle__') { + return; + } + + if (!cluster) { + setCluster(get(datum, 'properties.cluster', '')); + } + + if (!node) { + setNode(get(datum, 'properties.node', '')); + } + + // TODO can we get pod, container back in properties? + const names = datum.name.split('/'); + datum.pod = names[names.length - 2]; + datum.container = names[names.length - 1]; + + datum.hours = round(get(datum, 'minutes', 0.0) / 60.0, 2); + + if (datum.hours > 0) { + datum.cpu = round(get(datum, 'cpuCoreHours', 0.0) / datum.hours, 2); + datum.cpuCostPerCoreHr = datum.cpuCost / (datum.cpu * datum.hours); + if (datum.cpu === 0) { + datum.cpuCostPerCoreHr = 0.0; + } + + datum.ram = round(get(datum, 'ramByteHours', 0.0) / datum.hours, 2); + const ramGiB = datum.ram / 1024 / 1024 / 1024; + datum.ramCostPerGiBHr = datum.ramCost / (ramGiB * datum.hours); + if (ramGiB === 0) { + datum.ramCostPerGiBHr = 0.0; + } + } else { + datum.cpu = 0.0; + datum.cpuCostPerCoreHr = 0.0; + datum.ram = 0.0; + datum.ramCostPerGiBHr = 0.0; + } + + data.push(datum); + }); + + data = reverse(sortBy(data, 'totalCost')); + + setRows(data); + } catch (e) { + /* eslint no-console: ["error", { allow: ["warn"] }] */ + console.warn( + `Error fetching details for (${controllerKind}, ${controller}):`, + e, + ); + setErrors([ + { + primary: 'Error fetching details', + secondary: `Tried fetching details for: ${namespace}, ${controllerKind}, ${controller}, ${pod}`, + }, + ]); + } + + setLoading(false); + setFetch(false); + } + + if (loading) { + return ( +
+
+ +
+
+ ); + } + + return ( +
+ {!loading && errors.length > 0 && ( +
+ +
+ )} + + + {cluster && ( + + + + + + + )} + {node && ( + + + + + + + )} + + + + + + + Container + + + Hours + + + CPU + + + $/(CPU*Hr) + + + CPU cost + + + RAM + + + $/(GiB*Hr) + + + RAM cost + + + Total cost + + + + + {rows.map((row, i) => ( + + + {row.container} + + + {row.hours} + + + {row.cpu} + + + {toCurrency(row.cpuCostPerCoreHr, currency, 5)} + + + {toCurrency(row.cpuCost, currency, 3)} + + + {bytesToString(row.ram)} + + + {toCurrency(row.ramCostPerGiBHr, currency, 5)} + + + {toCurrency(row.ramCost, currency, 3)} + + + {toCurrency(row.totalCost, currency, 3)} + + + ))} + +
+
+
+ ); +}; + +export default memo(Details); diff --git a/plugins/opencost/src/components/OpenCostPage/OpenCostPage.tsx b/plugins/opencost/src/components/OpenCostPage/OpenCostPage.tsx new file mode 100644 index 0000000000..bd60fa0c35 --- /dev/null +++ b/plugins/opencost/src/components/OpenCostPage/OpenCostPage.tsx @@ -0,0 +1,44 @@ +/* + * 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 React from 'react'; +import { Grid } from '@material-ui/core'; +import { Header, Page, Content } from '@backstage/core-components'; +import { OpenCostReport } from '../OpenCostReport'; + +export const OpenCostPage = () => ( + +
+ + OpenCost + +
+ + + + + + + +
+); diff --git a/plugins/opencost/src/components/OpenCostPage/index.ts b/plugins/opencost/src/components/OpenCostPage/index.ts new file mode 100644 index 0000000000..f966c83a30 --- /dev/null +++ b/plugins/opencost/src/components/OpenCostPage/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { OpenCostPage } from './OpenCostPage'; diff --git a/plugins/opencost/src/components/OpenCostReport/OpenCostReport.tsx b/plugins/opencost/src/components/OpenCostReport/OpenCostReport.tsx new file mode 100644 index 0000000000..12f64f3606 --- /dev/null +++ b/plugins/opencost/src/components/OpenCostReport/OpenCostReport.tsx @@ -0,0 +1,319 @@ +/* + * 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. + */ +// code ported from https://github.com/opencost/opencost/blob/develop/ui/src/Reports.js + +import React, { useEffect, useState } from 'react'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import IconButton from '@material-ui/core/IconButton'; +import Paper from '@material-ui/core/Paper'; +import RefreshIcon from '@material-ui/icons/Refresh'; +import Typography from '@material-ui/core/Typography'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { find, get, sortBy, toArray } from 'lodash'; +import { makeStyles } from '@material-ui/styles'; +import AllocationReport from '../AllocationReport'; +import AllocationService from '../../services/allocation'; +import Controls from '../Controls'; +import Page from '../Page'; +import Subtitle from '../Subtitle'; +import Warnings from '../Warnings'; +import { + checkCustomWindow, + cumulativeToTotals, + rangeToCumulative, + toVerboseTimeRange, +} from '../../util'; +import { currencyCodes } from '../../constants/currencyCodes'; +import { useApi, configApiRef } from '@backstage/core-plugin-api'; + +const windowOptions = [ + { name: 'Today', value: 'today' }, + { name: 'Yesterday', value: 'yesterday' }, + { name: 'Week-to-date', value: 'week' }, + { name: 'Month-to-date', value: 'month' }, + { name: 'Last week', value: 'lastweek' }, + { name: 'Last month', value: 'lastmonth' }, + { name: 'Last 7 days', value: '6d' }, + { name: 'Last 30 days', value: '29d' }, + { name: 'Last 60 days', value: '59d' }, + { name: 'Last 90 days', value: '89d' }, +]; + +const aggregationOptions = [ + { name: 'Cluster', value: 'cluster' }, + { name: 'Node', value: 'node' }, + { name: 'Namespace', value: 'namespace' }, + { name: 'Controller kind', value: 'controllerKind' }, + { name: 'Controller', value: 'controller' }, + { name: 'Service', value: 'service' }, + { name: 'Pod', value: 'pod' }, + { name: 'Container', value: 'container' }, +]; + +const accumulateOptions = [ + { name: 'Entire window', value: true }, + { name: 'Daily', value: false }, +]; + +const useStyles = makeStyles({ + reportHeader: { + display: 'flex', + flexFlow: 'row', + padding: 24, + }, + titles: { + flexGrow: 1, + }, +}); + +// generateTitle generates a string title from a report object +// @ts-ignore: implicitly has an 'any' type +function generateTitle({ window, aggregateBy, accumulate }) { + let windowName = get(find(windowOptions, { value: window }), 'name', ''); + if (windowName === '') { + if (checkCustomWindow(window)) { + windowName = toVerboseTimeRange(window); + } else { + /* eslint no-console: ["error", { allow: ["warn"] }] */ + console.warn(`unknown window: ${window}`); + } + } + + const aggregationName = get( + find(aggregationOptions, { value: aggregateBy }), + 'name', + '', + ).toLocaleLowerCase('en-US'); + if (aggregationName === '') { + /* eslint no-console: ["error", { allow: ["warn"] }] */ + console.warn(`unknown aggregation: ${aggregateBy}`); + } + + let str = `${windowName} by ${aggregationName}`; + + if (!accumulate) { + str = `${str} daily`; + } + + return str; +} + +export const OpenCostReport = () => { + const classes = useStyles(); + // Allocation data state + const [allocationData, setAllocationData] = useState([]); + const [cumulativeData, setCumulativeData] = useState({}); + const [totalData, setTotalData] = useState({}); + + // When allocation data changes, create a cumulative version of it + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + const cumulative = rangeToCumulative(allocationData, aggregateBy); + setCumulativeData(toArray(cumulative)); + setTotalData(cumulativeToTotals(cumulative)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [allocationData]); + + // Form state, which controls form elements, but not the report itself. On + // certain actions, the form state may flow into the report state. + const [window, setWindow] = useState(windowOptions[0].value); + const [aggregateBy, setAggregateBy] = useState(aggregationOptions[0].value); + const [accumulate, setAccumulate] = useState(accumulateOptions[0].value); + const [currency, setCurrency] = useState('USD'); + + // Report state, including current report and saved options + const [title, setTitle] = useState('Last 7 days by namespace daily'); + + // When parameters changes, fetch data. This should be the + // only mechanism used to fetch data. Also generate a sensible title from the paramters. + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + setFetch(true); + setTitle(generateTitle({ window, aggregateBy, accumulate })); + }, [window, aggregateBy, accumulate]); + + // page and settings state + const [init, setInit] = useState(false); + const [fetch, setFetch] = useState(false); + const [loading, setLoading] = useState(true); + const [errors, setErrors] = useState([]); + + // Initialize once, then fetch report each time setFetch(true) is called + useEffect(() => { + if (!init) { + initialize(); + } + if (init && fetch) { + fetchData(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [init, fetch]); + + // parse any context information from the URL + const routerLocation = useLocation(); + const searchParams = new URLSearchParams(routerLocation.search); + const routerNavigate = useNavigate(); + useEffect(() => { + setWindow(searchParams.get('window') || '6d'); + setAggregateBy(searchParams.get('agg') || 'namespace'); + setAccumulate(searchParams.get('acc') === 'true' || false); + setCurrency(searchParams.get('currency') || 'USD'); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [routerLocation]); + + async function initialize() { + setInit(true); + } + + const configApi = useApi(configApiRef); + const baseUrl = configApi.getConfig('opencost').getString('baseUrl'); + /* eslint no-console: 0 */ + console.log(`baseUrl:${baseUrl}`); + + async function fetchData() { + setLoading(true); + setErrors([]); + + try { + const resp = await AllocationService.fetchAllocation( + baseUrl, + window, + aggregateBy, + { accumulate }, + ); + if (resp.data && resp.data.length > 0) { + const allocationRange = resp.data; + for (const i in allocationRange) { + if (Object.hasOwn(allocationRange, i)) { + // update cluster aggregations to use clusterName/clusterId names + allocationRange[i] = sortBy(allocationRange[i], a => a.totalCost); + } + } + setAllocationData(allocationRange); + } else { + setAllocationData([]); + } + } catch (err) { + if (err.message.indexOf('404') === 0) { + setErrors([ + // @ts-ignore: not assignable to type 'never' + { + primary: 'Failed to load report data', + secondary: + 'Please update OpenCost to the latest version, then open an Issue on GitHub if problems persist.', + }, + ]); + } else { + let secondary = + 'Please open an OpenCost issue with a bug report if problems persist.'; + if (err.message.length > 0) { + secondary = err.message; + } + setErrors([ + // @ts-ignore: not assignable to type 'never' + { + primary: 'Failed to load report data', + secondary: secondary, + }, + ]); + } + setAllocationData([]); + } + + setLoading(false); + setFetch(false); + } + return ( + + {!loading && errors.length > 0 && ( +
+ +
+ )} + {init && ( + +
+
+ {title} + +
+ + setFetch(true)}> + + + + { + searchParams.set('window', win); + routerNavigate({ + search: `?${searchParams.toString()}`, + }); + }} + aggregationOptions={aggregationOptions} + aggregateBy={aggregateBy} + // @ts-ignore: implicitly has an 'any' type + setAggregateBy={agg => { + searchParams.set('agg', agg); + routerNavigate({ + search: `?${searchParams.toString()}`, + }); + }} + accumulateOptions={accumulateOptions} + accumulate={accumulate} + // @ts-ignore: implicitly has an 'any' type + setAccumulate={acc => { + searchParams.set('acc', acc); + routerNavigate({ + search: `?${searchParams.toString()}`, + }); + }} + title={title} + cumulativeData={cumulativeData} + currency={currency} + currencyOptions={currencyCodes} + // @ts-ignore: implicitly has an 'any' type + setCurrency={curr => { + searchParams.set('currency', curr); + routerNavigate({ + search: `?${searchParams.toString()}`, + }); + }} + /> +
+ + {loading && ( +
+
+ +
+
+ )} + {!loading && ( + + )} +
+ )} +
+ ); +}; diff --git a/plugins/opencost/src/components/OpenCostReport/index.ts b/plugins/opencost/src/components/OpenCostReport/index.ts new file mode 100644 index 0000000000..aee85ea8db --- /dev/null +++ b/plugins/opencost/src/components/OpenCostReport/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { OpenCostReport } from './OpenCostReport'; diff --git a/plugins/opencost/src/components/Page.jsx b/plugins/opencost/src/components/Page.jsx new file mode 100644 index 0000000000..6346b6287b --- /dev/null +++ b/plugins/opencost/src/components/Page.jsx @@ -0,0 +1,47 @@ +/* + * 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 { makeStyles } from '@material-ui/styles'; +import React from 'react'; + +const useStyles = makeStyles({ + wrapper: { + display: 'flex', + flexFlow: 'column', + flexGrow: 1, + margin: '20px 30px 0 30px', + minWidth: 800, + }, + flexGrow: { + display: 'flex', + flexFlow: 'column', + flexGrow: 1, + }, +}); + +const Page = props => { + const classes = useStyles(); + + return ( +
+
+
{props.children}
+
+
+ ); +}; + +export default Page; diff --git a/plugins/opencost/src/components/SelectWindow.jsx b/plugins/opencost/src/components/SelectWindow.jsx new file mode 100644 index 0000000000..470770f35c --- /dev/null +++ b/plugins/opencost/src/components/SelectWindow.jsx @@ -0,0 +1,213 @@ +/* + * 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 React, { useEffect, useState } from 'react'; +import { makeStyles } from '@material-ui/styles'; +import { + MuiPickersUtilsProvider, + KeyboardDatePicker, +} from '@material-ui/pickers'; +import Button from '@material-ui/core/Button'; +import LuxonUtils from '@date-io/luxon'; +import FormControl from '@material-ui/core/FormControl'; +import Link from '@material-ui/core/Link'; +import Popover from '@material-ui/core/Popover'; +import TextField from '@material-ui/core/TextField'; +import Typography from '@material-ui/core/Typography'; +import { isValid } from 'date-fns'; +import { find, get } from 'lodash'; + +const useStyles = makeStyles({ + dateContainer: { + paddingLeft: 18, + paddingRight: 18, + paddingTop: 6, + paddingBottom: 18, + display: 'flex', + flexFlow: 'row', + }, + dateContainerColumn: { + display: 'flex', + flexFlow: 'column', + }, + formControl: { + margin: 8, + width: 120, + }, +}); + +const SelectWindow = ({ windowOptions, window, setWindow }) => { + const classes = useStyles(); + const [anchorEl, setAnchorEl] = useState(null); + + const [startDate, setStartDate] = useState(null); + const [endDate, setEndDate] = useState(null); + const [intervalString, setIntervalString] = useState(null); + + const handleClick = event => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleStartDateChange = date => { + if (isValid(date.toJSDate())) { + setStartDate(date.startOf('day').toJSDate()); + } + }; + + const handleEndDateChange = date => { + if (isValid(date.toJSDate())) { + setEndDate(date.endOf('day').toJSDate()); + } + }; + + const handleSubmitPresetDates = dateString => { + setWindow(dateString); + setStartDate(null); + setEndDate(null); + handleClose(); + }; + + const handleSubmitCustomDates = () => { + if (intervalString !== null) { + setWindow(intervalString); + handleClose(); + } + }; + + useEffect(() => { + if (startDate !== null && endDate !== null) { + // Note: getTimezoneOffset() is calculated based on current system locale, NOT date object + const adjustedStartDate = new Date( + startDate - startDate.getTimezoneOffset() * 60000, + ); + const adjustedEndDate = new Date( + endDate - endDate.getTimezoneOffset() * 60000, + ); + setIntervalString( + `${adjustedStartDate.toISOString().split('.')[0]}Z` + + `,${adjustedEndDate.toISOString().split('.')[0]}Z`, + ); + } + }, [startDate, endDate]); + + const open = Boolean(anchorEl); + const id = open ? 'date-range-popover' : undefined; + + return ( + <> + + handleClick(e)} + inputProps={{ + readOnly: true, + style: { cursor: 'pointer' }, + }} + /> + + +
+
+ + + + +
+ +
+
+
+ {windowOptions.map(opt => ( + + handleSubmitPresetDates(opt.value)} + > + {opt.name} + + + ))} +
+
+
+ + ); +}; + +export default React.memo(SelectWindow); diff --git a/plugins/opencost/src/components/Subtitle.jsx b/plugins/opencost/src/components/Subtitle.jsx new file mode 100644 index 0000000000..63b3df61ff --- /dev/null +++ b/plugins/opencost/src/components/Subtitle.jsx @@ -0,0 +1,59 @@ +/* + * 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 React from 'react'; +import { makeStyles } from '@material-ui/styles'; +import { upperFirst } from 'lodash'; +import Breadcrumbs from '@material-ui/core/Breadcrumbs'; +import NavigateNextIcon from '@material-ui/icons/NavigateNext'; +import Typography from '@material-ui/core/Typography'; +import { toVerboseTimeRange } from '../util'; + +const useStyles = makeStyles({ + root: { + '& > * + *': { + marginTop: 2, + }, + }, + link: { + cursor: 'pointer', + }, +}); + +const Subtitle = ({ report }) => { + const classes = useStyles(); + + const { aggregateBy, window } = report; + + return ( +
+ } + aria-label="breadcrumb" + > + {aggregateBy && aggregateBy.length > 0 ? ( + + {toVerboseTimeRange(window)} by {upperFirst(aggregateBy)} + + ) : ( + {toVerboseTimeRange(window)} + )} + +
+ ); +}; + +export default React.memo(Subtitle); diff --git a/plugins/opencost/src/components/Warnings.jsx b/plugins/opencost/src/components/Warnings.jsx new file mode 100644 index 0000000000..ae1047ebc2 --- /dev/null +++ b/plugins/opencost/src/components/Warnings.jsx @@ -0,0 +1,53 @@ +/* + * 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 React from 'react'; +import { makeStyles } from '@material-ui/styles'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import Paper from '@material-ui/core/Paper'; +import WarningIcon from '@material-ui/icons/Warning'; + +const useStyles = makeStyles({ + root: {}, +}); + +const Warnings = ({ warnings }) => { + const classes = useStyles(); + + if (!warnings || warnings.length === 0) { + return null; + } + + return ( + + + {warnings.map((warn, i) => ( + + + + + + + ))} + + + ); +}; + +export default Warnings; diff --git a/plugins/opencost/src/constants/colors.js b/plugins/opencost/src/constants/colors.js new file mode 100644 index 0000000000..8e0289541b --- /dev/null +++ b/plugins/opencost/src/constants/colors.js @@ -0,0 +1,52 @@ +/* + * 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 blue from '@material-ui/core/colors/blue'; +import brown from '@material-ui/core/colors/brown'; +import cyan from '@material-ui/core/colors/cyan'; +import deepOrange from '@material-ui/core/colors/deepOrange'; +import deepPurple from '@material-ui/core/colors/deepPurple'; +import green from '@material-ui/core/colors/green'; +import grey from '@material-ui/core/colors/grey'; +import indigo from '@material-ui/core/colors/indigo'; +import orange from '@material-ui/core/colors/orange'; +import red from '@material-ui/core/colors/red'; +import teal from '@material-ui/core/colors/teal'; +import yellow from '@material-ui/core/colors/yellow'; + +export const primary = [ + blue[500], + red[500], + green[500], + yellow[500], + cyan[500], + orange[500], + teal[500], + indigo[500], + deepOrange[500], + deepPurple[500], +]; + +export const greyscale = [ + grey[300], + grey[400], + grey[200], + grey[500], + grey[100], + grey[600], +]; + +export const browns = [brown[500]]; diff --git a/plugins/opencost/src/constants/currencyCodes.js b/plugins/opencost/src/constants/currencyCodes.js new file mode 100644 index 0000000000..a325df54d0 --- /dev/null +++ b/plugins/opencost/src/constants/currencyCodes.js @@ -0,0 +1,196 @@ +/* + * 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 const currencyCodes = [ + 'AED', + 'AFN', + 'ALL', + 'AMD', + 'ANG', + 'AOA', + 'ARS', + 'AUD', + 'AWG', + 'AZN', + 'BAM', + 'BBD', + 'BDT', + 'BGN', + 'BHD', + 'BIF', + 'BMD', + 'BND', + 'BOB', + 'BOV', + 'BRL', + 'BSD', + 'BTN', + 'BWP', + 'BYR', + 'BZD', + 'CAD', + 'CDF', + 'CHE', + 'CHF', + 'CHW', + 'CLF', + 'CLP', + 'CNY', + 'COP', + 'COU', + 'CRC', + 'CUC', + 'CUP', + 'CVE', + 'CZK', + 'DJF', + 'DKK', + 'DOP', + 'DZD', + 'EGP', + 'ERN', + 'ETB', + 'EUR', + 'FJD', + 'FKP', + 'GBP', + 'GEL', + 'GHS', + 'GIP', + 'GMD', + 'GNF', + 'GTQ', + 'GYD', + 'HKD', + 'HNL', + 'HRK', + 'HTG', + 'HUF', + 'IDR', + 'ILS', + 'INR', + 'IQD', + 'IRR', + 'ISK', + 'JMD', + 'JOD', + 'JPY', + 'KES', + 'KGS', + 'KHR', + 'KMF', + 'KPW', + 'KRW', + 'KWD', + 'KYD', + 'KZT', + 'LAK', + 'LBP', + 'LKR', + 'LRD', + 'LSL', + 'LTL', + 'LVL', + 'LYD', + 'MAD', + 'MDL', + 'MGA', + 'MKD', + 'MMK', + 'MNT', + 'MOP', + 'MRO', + 'MUR', + 'MVR', + 'MWK', + 'MXN', + 'MXV', + 'MYR', + 'MZN', + 'NAD', + 'NGN', + 'NIO', + 'NOK', + 'NPR', + 'NZD', + 'OMR', + 'PAB', + 'PEN', + 'PGK', + 'PHP', + 'PKR', + 'PLN', + 'PYG', + 'QAR', + 'RON', + 'RSD', + 'RUB', + 'RWF', + 'SAR', + 'SBD', + 'SCR', + 'SDG', + 'SEK', + 'SGD', + 'SHP', + 'SLL', + 'SOS', + 'SRD', + 'SSP', + 'STD', + 'SYP', + 'SZL', + 'THB', + 'TJS', + 'TMT', + 'TND', + 'TOP', + 'TRY', + 'TTD', + 'TWD', + 'TZS', + 'UAH', + 'UGX', + 'USD', + 'USN', + 'USS', + 'UYI', + 'UYU', + 'UZS', + 'VEF', + 'VND', + 'VUV', + 'WST', + 'XAF', + 'XAG', + 'XAU', + 'XBA', + 'XBB', + 'XBC', + 'XBD', + 'XCD', + 'XDR', + 'XFU', + 'XOF', + 'XPD', + 'XPF', + 'XPT', + 'XTS', + 'XXX', + 'YER', + 'ZAR', + 'ZMW', +]; diff --git a/plugins/opencost/src/images/pig.png b/plugins/opencost/src/images/pig.png new file mode 100644 index 0000000000..5de5409e87 Binary files /dev/null and b/plugins/opencost/src/images/pig.png differ diff --git a/plugins/opencost/src/index.ts b/plugins/opencost/src/index.ts new file mode 100644 index 0000000000..30153e0c32 --- /dev/null +++ b/plugins/opencost/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 Backstage plugin that integrates with OpenCost + * + * @packageDocumentation + */ + +export { openCostPlugin, OpenCostPage } from './plugin'; diff --git a/plugins/opencost/src/plugin.test.ts b/plugins/opencost/src/plugin.test.ts new file mode 100644 index 0000000000..6d6d20c603 --- /dev/null +++ b/plugins/opencost/src/plugin.test.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. + */ + +import { openCostPlugin } from './plugin'; + +describe('opencost', () => { + it('should export plugin', () => { + expect(openCostPlugin).toBeDefined(); + }); +}); diff --git a/plugins/opencost/src/plugin.ts b/plugins/opencost/src/plugin.ts new file mode 100644 index 0000000000..2725cf3dde --- /dev/null +++ b/plugins/opencost/src/plugin.ts @@ -0,0 +1,40 @@ +/* + * 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 { + createPlugin, + createRoutableExtension, +} from '@backstage/core-plugin-api'; + +import { rootRouteRef } from './routes'; + +/** @public */ +export const openCostPlugin = createPlugin({ + id: 'opencost', + routes: { + root: rootRouteRef, + }, +}); + +/** @public */ +export const OpenCostPage = openCostPlugin.provide( + createRoutableExtension({ + name: 'OpenCostPage', + component: () => + import('./components/OpenCostPage').then(m => m.OpenCostPage), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/opencost/src/routes.ts b/plugins/opencost/src/routes.ts new file mode 100644 index 0000000000..5f1f9e4527 --- /dev/null +++ b/plugins/opencost/src/routes.ts @@ -0,0 +1,21 @@ +/* + * 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 { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'opencost', +}); diff --git a/plugins/opencost/src/services/allocation.js b/plugins/opencost/src/services/allocation.js new file mode 100644 index 0000000000..e7fba8feae --- /dev/null +++ b/plugins/opencost/src/services/allocation.js @@ -0,0 +1,36 @@ +/* + * 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 axios from 'axios'; + +class AllocationService { + async fetchAllocation(baseUrl, win, aggregate, options) { + const { accumulate } = options; + const params = { + window: win, + aggregate: aggregate, + step: '1d', + }; + if (typeof accumulate === 'boolean') { + params.accumulate = accumulate; + } + + const result = await axios.get(`${baseUrl}/allocation/compute`, { params }); + + return result.data; + } +} + +export default new AllocationService(); diff --git a/plugins/opencost/src/util.js b/plugins/opencost/src/util.js new file mode 100644 index 0000000000..5854718a55 --- /dev/null +++ b/plugins/opencost/src/util.js @@ -0,0 +1,371 @@ +/* + * 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 { forEach, get, round } from 'lodash'; + +// rangeToCumulative takes an AllocationSetRange (type: array[AllocationSet]) +// and accumulates the values into a single AllocationSet (type: object) +export function rangeToCumulative(allocationSetRange, aggregateBy) { + if (allocationSetRange.length === 0) { + return null; + } + + const result = {}; + + forEach(allocationSetRange, allocSet => { + forEach(allocSet, alloc => { + if (result[alloc.name] === undefined) { + const hrs = get(alloc, 'minutes', 0) / 60.0; + + result[alloc.name] = { + name: alloc.name, + [aggregateBy]: alloc.name, + cpuCost: get(alloc, 'cpuCost', 0), + gpuCost: get(alloc, 'gpuCost', 0), + ramCost: get(alloc, 'ramCost', 0), + pvCost: get(alloc, 'pvCost', 0), + networkCost: get(alloc, 'networkCost', 0), + sharedCost: get(alloc, 'sharedCost', 0), + externalCost: get(alloc, 'externalCost', 0), + totalCost: get(alloc, 'totalCost', 0), + cpuUseCoreHrs: get(alloc, 'cpuCoreUsageAverage', 0) * hrs, + cpuReqCoreHrs: get(alloc, 'cpuCoreRequestAverage', 0) * hrs, + ramUseByteHrs: get(alloc, 'ramByteUsageAverage', 0) * hrs, + ramReqByteHrs: get(alloc, 'ramByteRequestAverage', 0) * hrs, + cpuEfficiency: get(alloc, 'cpuEfficiency', 0), + ramEfficiency: get(alloc, 'ramEfficiency', 0), + totalEfficiency: get(alloc, 'totalEfficiency', 0), + }; + } else { + const hrs = get(alloc, 'minutes', 0) / 60.0; + + result[alloc.name].cpuCost += get(alloc, 'cpuCost', 0); + result[alloc.name].gpuCost += get(alloc, 'gpuCost', 0); + result[alloc.name].ramCost += get(alloc, 'ramCost', 0); + result[alloc.name].pvCost += get(alloc, 'pvCost', 0); + result[alloc.name].networkCost += get(alloc, 'networkCost', 0); + result[alloc.name].sharedCost += get(alloc, 'sharedCost', 0); + result[alloc.name].externalCost += get(alloc, 'externalCost', 0); + result[alloc.name].totalCost += get(alloc, 'totalCost', 0); + result[alloc.name].cpuUseCoreHrs += + get(alloc, 'cpuCoreUsageAverage', 0) * hrs; + result[alloc.name].cpuReqCoreHrs += + get(alloc, 'cpuCoreRequestAverage', 0) * hrs; + result[alloc.name].ramUseByteHrs += + get(alloc, 'ramByteUsageAverage', 0) * hrs; + result[alloc.name].ramReqByteHrs += + get(alloc, 'ramByteRequestAverage', 0) * hrs; + } + }); + }); + + // If the range is of length > 1 (i.e. it is not just a single set) then + // compute efficiency for each result after accumulating. + if (allocationSetRange.length > 1) { + forEach(result, (alloc, name) => { + // If we can't compute total efficiency, it defaults to 0.0 + let totalEfficiency = 0.0; + + // CPU efficiency is defined as (usage/request). If request == 0.0 but + // usage > 0, then efficiency gets set to 1.0. + let cpuEfficiency = 0.0; + if (alloc.cpuReqCoreHrs > 0) { + cpuEfficiency = alloc.cpuUseCoreHrs / alloc.cpuReqCoreHrs; + } else if (alloc.cpuUseCoreHrs > 0) { + cpuEfficiency = 1.0; + } + + // RAM efficiency is defined as (usage/request). If request == 0.0 but + // usage > 0, then efficiency gets set to 1.0. + let ramEfficiency = 0.0; + if (alloc.ramReqByteHrs > 0) { + ramEfficiency = alloc.ramUseByteHrs / alloc.ramReqByteHrs; + } else if (alloc.ramUseByteHrs > 0) { + ramEfficiency = 1.0; + } + + // Compute efficiency as the cost-weighted average of CPU and RAM + // efficiency + if (alloc.cpuCost + alloc.ramCost > 0.0) { + totalEfficiency = + (alloc.cpuCost * cpuEfficiency + alloc.ramCost * ramEfficiency) / + (alloc.cpuCost + alloc.ramCost); + } + + result[name].cpuEfficiency = cpuEfficiency; + result[name].ramEfficiency = ramEfficiency; + result[name].totalEfficiency = totalEfficiency; + }); + } + + return result; +} + +// cumulativeToTotals adds each entry in the given AllocationSet (type: object) +// and returns a single Allocation (type: object) representing the totals +export function cumulativeToTotals(allocationSet) { + const totals = { + name: 'Totals', + cpuCost: 0, + gpuCost: 0, + ramCost: 0, + pvCost: 0, + networkCost: 0, + sharedCost: 0, + externalCost: 0, + totalCost: 0, + cpuEfficiency: 0, + ramEfficiency: 0, + totalEfficiency: 0, + }; + + // Use these for computing efficiency. As such, idle will not factor into + // these numbers, including CPU and RAM cost. + let cpuReqCoreHrs = 0; + let cpuUseCoreHrs = 0; + let ramReqByteHrs = 0; + let ramUseByteHrs = 0; + let cpuCost = 0; + let ramCost = 0; + + forEach(allocationSet, (alloc, name) => { + // Accumulate efficiency-related fields + if (name !== '__idle__') { + cpuReqCoreHrs += get(alloc, 'cpuReqCoreHrs', 0.0); + cpuUseCoreHrs += get(alloc, 'cpuUseCoreHrs', 0.0); + ramReqByteHrs += get(alloc, 'ramReqByteHrs', 0.0); + ramUseByteHrs += get(alloc, 'ramUseByteHrs', 0.0); + cpuCost += get(alloc, 'cpuCost', 0.0); + ramCost += get(alloc, 'ramCost', 0.0); + } + + // Sum cumulative fields + totals.cpuCost += get(alloc, 'cpuCost', 0); + totals.gpuCost += get(alloc, 'gpuCost', 0); + totals.ramCost += get(alloc, 'ramCost', 0); + totals.pvCost += get(alloc, 'pvCost', 0); + totals.networkCost += get(alloc, 'networkCost', 0); + totals.sharedCost += get(alloc, 'sharedCost', 0); + totals.externalCost += get(alloc, 'externalCost', 0); + totals.totalCost += get(alloc, 'totalCost', 0); + }); + + // Compute efficiency + if (cpuReqCoreHrs > 0) { + totals.cpuEfficiency = cpuUseCoreHrs / cpuReqCoreHrs; + } else if (cpuUseCoreHrs > 0) { + totals.cpuEfficiency = 1.0; + } + + if (ramReqByteHrs > 0) { + totals.ramEfficiency = ramUseByteHrs / ramReqByteHrs; + } else if (ramUseByteHrs > 0) { + totals.ramEfficiency = 1.0; + } + + if (cpuCost + ramCost > 0) { + totals.totalEfficiency = + (cpuCost * totals.cpuEfficiency + ramCost * totals.ramEfficiency) / + (cpuCost + ramCost); + } + + totals.cpuReqCoreHrs = cpuReqCoreHrs; + totals.cpuUseCoreHrs = cpuUseCoreHrs; + totals.ramReqByteHrs = ramReqByteHrs; + totals.ramUseByteHrs = ramUseByteHrs; + + return totals; +} + +export function toVerboseTimeRange(window) { + const months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; + + const start = new Date(); + start.setUTCHours(0, 0, 0, 0); + + const end = new Date(); + end.setUTCHours(0, 0, 0, 0); + + switch (window) { + case 'today': + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()}`; + case 'yesterday': + start.setUTCDate(start.getUTCDate() - 1); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()}`; + case 'week': + start.setUTCDate(start.getUTCDate() - start.getUTCDay()); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} until now`; + case 'month': + start.setUTCDate(1); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} until now`; + case 'lastweek': + start.setUTCDate(start.getUTCDate() - (start.getUTCDay() + 7)); + end.setUTCDate(end.getUTCDate() - (end.getUTCDay() + 1)); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} through ${end.getUTCDate()} ${ + months[end.getUTCMonth()] + } ${end.getUTCFullYear()}`; + case 'lastmonth': + end.setUTCDate(1); + end.setUTCDate(end.getUTCDate() - 1); + start.setUTCDate(1); + start.setUTCDate(start.getUTCDate() - 1); + start.setUTCDate(1); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} through ${end.getUTCDate()} ${ + months[end.getUTCMonth()] + } ${end.getUTCFullYear()}`; + case '6d': + start.setUTCDate(start.getUTCDate() - 6); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} through now`; + case '29d': + start.setUTCDate(start.getUTCDate() - 29); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} through now`; + case '59d': + start.setUTCDate(start.getUTCDate() - 59); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} through now`; + case '89d': + start.setUTCDate(start.getUTCDate() - 89); + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} through now`; + // no default + } + + const splitDates = window.split(','); + if (checkCustomWindow(window) && splitDates.length > 1) { + const s = splitDates[0].split(/\D+/).slice(0, 3); + const e = splitDates[1].split(/\D+/).slice(0, 3); + if (s.length === 3 && e.length === 3) { + start.setUTCFullYear(s[0], s[1] - 1, s[2]); + end.setUTCFullYear(e[0], e[1] - 1, e[2]); + if (start === end) { + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()}`; + } + return `${start.getUTCDate()} ${ + months[start.getUTCMonth()] + } ${start.getUTCFullYear()} through ${end.getUTCDate()} ${ + months[end.getUTCMonth()] + } ${end.getUTCFullYear()}`; + } + } + return ''; +} + +export function bytesToString(bytes) { + const ei = Math.pow(1024, 6); + if (bytes >= ei) { + return `${round(bytes / ei, 1)} EiB`; + } + const pi = Math.pow(1024, 5); + if (bytes >= pi) { + return `${round(bytes / pi, 1)} PiB`; + } + const ti = Math.pow(1024, 4); + if (bytes >= ti) { + return `${round(bytes / ti, 1)} TiB`; + } + const gi = Math.pow(1024, 3); + if (bytes >= gi) { + return `${round(bytes / gi, 1)} GiB`; + } + const mi = Math.pow(1024, 2); + if (bytes >= mi) { + return `${round(bytes / mi, 1)} MiB`; + } + const ki = Math.pow(1024, 1); + if (bytes >= ki) { + return `${round(bytes / ki, 1)} KiB`; + } + + return `${round(bytes, 1)} B`; +} + +const currencyLocale = 'en-US'; + +export function toCurrency(amount, currency, precision) { + if (typeof amount !== 'number') { + Console.warn( + `Tried to convert "${amount}" to currency, but it is not a number`, + ); + return ''; + } + + let ctype = currency; + + if (currency === undefined || currency === '') { + ctype = 'USD'; + } + + const opts = { + style: 'currency', + currency: ctype, + }; + + if (typeof precision === 'number') { + opts.minimumFractionDigits = precision; + opts.maximumFractionDigits = precision; + } + + return amount.toLocaleString(currencyLocale, opts); +} + +export function checkCustomWindow(window) { + // Example ISO interval string: 2020-12-02T00:00:00Z,2020-12-03T23:59:59Z + const customDateRegex = + /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z,\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/; + return customDateRegex.test(window); +} + +export default { + rangeToCumulative, + cumulativeToTotals, + toVerboseTimeRange, + bytesToString, + toCurrency, + checkCustomWindow, +}; diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md index eeac469210..578601033d 100644 --- a/plugins/org-react/CHANGELOG.md +++ b/plugins/org-react/CHANGELOG.md @@ -1,5 +1,63 @@ # @backstage/plugin-org-react +## 0.1.13-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/theme@0.4.1 + +## 0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/theme@0.4.1 + +## 0.1.12-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.1.10 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## 0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.1.10-next.1 ### Patch Changes diff --git a/plugins/org-react/api-report.md b/plugins/org-react/api-report.md index 48bd090f64..5cfe5ca90a 100644 --- a/plugins/org-react/api-report.md +++ b/plugins/org-react/api-report.md @@ -3,12 +3,13 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { GroupEntity } from '@backstage/catalog-model'; +import { default as React_2 } from 'react'; // @public (undocumented) -export const GroupListPicker: (props: GroupListPickerProps) => JSX.Element; +export const GroupListPicker: ( + props: GroupListPickerProps, +) => React_2.JSX.Element; // @public export type GroupListPickerProps = { diff --git a/plugins/org-react/catalog-info.yaml b/plugins/org-react/catalog-info.yaml new file mode 100644 index 0000000000..e020b5103e --- /dev/null +++ b/plugins/org-react/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-org-react + title: '@backstage/plugin-org-react' +spec: + lifecycle: experimental + type: backstage-web-library + owner: maintainers diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json index 176722ad03..d1644698ce 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.10-next.1", + "version": "0.1.13-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,6 +21,7 @@ "keywords": [ "backstage" ], + "sideEffects": false, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", @@ -40,6 +41,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -56,8 +58,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 4e98307d04..4f9654b88c 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/plugin-org +## 0.6.14-next.2 + +### Patch Changes + +- dd9329caea1c: Entity relations toggle should by default be aggregated for User entities +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## 0.6.14-next.1 + +### Patch Changes + +- 43a2137bb829: Ensure direct relations are shown for User entities while keeping support for aggregating closest parent group ownership +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## 0.6.13-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.6.11 + +### Patch Changes + +- 50331203e2a2: Fixed bug in MembersListCard component where the url for the members was static which was breaking the component when catalog route was changed +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## 0.6.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.6.11-next.1 ### Patch Changes diff --git a/plugins/org/api-report.md b/plugins/org/api-report.md index 7f21bfbb6b..d85df7125c 100644 --- a/plugins/org/api-report.md +++ b/plugins/org/api-report.md @@ -9,47 +9,52 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; // @public (undocumented) export const EntityGroupProfileCard: (props: { variant?: InfoCardVariants | undefined; showLinks?: boolean | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityMembersListCard: (props: { memberDisplayTitle?: string | undefined; pageSize?: number | undefined; showAggregateMembersToggle?: boolean | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityOwnershipCard: (props: { variant?: InfoCardVariants | undefined; entityFilterKind?: string[] | undefined; hideRelationsToggle?: boolean | undefined; - relationsType?: string | undefined; + relationsType?: EntityRelationAggregation | undefined; entityLimit?: number | undefined; -}) => JSX.Element; +}) => JSX_2.Element; + +// @public (undocumented) +export type EntityRelationAggregation = 'direct' | 'aggregated'; // @public (undocumented) export const EntityUserProfileCard: (props: { variant?: InfoCardVariants | undefined; showLinks?: boolean | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const GroupProfileCard: (props: { variant?: InfoCardVariants; showLinks?: boolean; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const MembersListCard: (props: { memberDisplayTitle?: string; pageSize?: number; showAggregateMembersToggle?: boolean; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public export const MyGroupsSidebarItem: (props: { @@ -57,7 +62,7 @@ export const MyGroupsSidebarItem: (props: { pluralTitle: string; icon: IconComponent; filter?: Record; -}) => JSX.Element | null; +}) => React_2.JSX.Element | null; // @public (undocumented) const orgPlugin: BackstagePlugin< @@ -75,13 +80,13 @@ export const OwnershipCard: (props: { variant?: InfoCardVariants; entityFilterKind?: string[]; hideRelationsToggle?: boolean; - relationsType?: string; + relationsType?: EntityRelationAggregation; entityLimit?: number; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const UserProfileCard: (props: { variant?: InfoCardVariants; showLinks?: boolean; -}) => JSX.Element; +}) => React_2.JSX.Element; ``` diff --git a/plugins/org/catalog-info.yaml b/plugins/org/catalog-info.yaml new file mode 100644 index 0000000000..defa276143 --- /dev/null +++ b/plugins/org/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-org + title: '@backstage/plugin-org' + description: A Backstage plugin that helps you create entity pages for your organization +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/org/package.json b/plugins/org/package.json index 81d6495d08..6d340c9147 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-org", "description": "A Backstage plugin that helps you create entity pages for your organization", - "version": "0.6.11-next.1", + "version": "0.6.14-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,6 +19,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/org" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -37,6 +38,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "p-limit": "^3.1.0", "pluralize": "^8.0.0", "qs": "^6.10.1", @@ -59,9 +61,8 @@ "@testing-library/dom": "^8.0.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", + "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ 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 47e954eb17..f5a2d50cb5 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx @@ -19,6 +19,7 @@ import { CatalogApi, catalogApiRef, EntityProvider, + entityRouteRef, StarredEntitiesApi, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; @@ -124,6 +125,11 @@ describe('MemberTab Test', () => { , , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, ), ); @@ -152,6 +158,11 @@ describe('MemberTab Test', () => { , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, ), ); @@ -176,12 +187,15 @@ describe('MemberTab Test', () => { , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, ); - const toggleSwitch = screen.queryByRole('checkbox'); expect(toggleSwitch).toBeNull(); }); - it('Shows the aggregate members toggle if the showAggregateMembersToggle prop is true', async () => { await renderInTestApp( { , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, ); - expect(screen.queryByRole('checkbox')).toBeInTheDocument(); }); - it('Shows only direct members if the showAggregateMembersToggle prop is undefined', async () => { await renderInTestApp( { , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, ); - const displayedMemberNames = screen.queryAllByTestId('user-link'); const duplicatedUserText = screen.getByText('Duplicated User'); const groupAUserOneText = screen.getByText('Group A User One'); - expect(displayedMemberNames).toHaveLength(2); expect(duplicatedUserText).toBeInTheDocument(); expect(groupAUserOneText).toBeInTheDocument(); @@ -234,7 +254,6 @@ describe('MemberTab Test', () => { duplicatedUserText.compareDocumentPosition(groupAUserOneText), ).toBe(Node.DOCUMENT_POSITION_FOLLOWING); }); - it('Shows only direct members if the aggregate members switch is turned off', async () => { await renderInTestApp( { , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, ); - const displayedMemberNames = screen.queryAllByTestId('user-link'); const duplicatedUserText = screen.getByText('Duplicated User'); const groupAUserOneText = screen.getByText('Group A User One'); - expect(displayedMemberNames).toHaveLength(2); expect(duplicatedUserText).toBeInTheDocument(); expect(groupAUserOneText).toBeInTheDocument(); @@ -265,7 +287,6 @@ describe('MemberTab Test', () => { duplicatedUserText.compareDocumentPosition(groupAUserOneText), ).toBe(Node.DOCUMENT_POSITION_FOLLOWING); }); - it('Shows all descendant members of the group when the aggregate users switch is turned on, showing duplicated members only once', async () => { await renderInTestApp( { , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, ); - // Click the toggle switch await userEvent.click(screen.getByRole('checkbox')); - const displayedMemberNames = screen.queryAllByTestId('user-link'); const duplicatedUserText = screen.getByText('Duplicated User'); const groupAUserOneText = screen.getByText('Group A User One'); const groupBUserOneText = screen.getByText('Group B User One'); const groupDUserOneText = screen.getByText('Group D User One'); const groupEUserOneText = screen.getByText('Group E User One'); - expect(displayedMemberNames).toHaveLength(5); - expect(duplicatedUserText).toBeInTheDocument(); expect(groupAUserOneText).toBeInTheDocument(); expect(groupBUserOneText).toBeInTheDocument(); expect(groupDUserOneText).toBeInTheDocument(); expect(groupEUserOneText).toBeInTheDocument(); - expect( duplicatedUserText.compareDocumentPosition(groupAUserOneText), ).toBe(Node.DOCUMENT_POSITION_FOLLOWING); diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx index 0a5386ea20..3716af5522 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -20,11 +20,7 @@ import { UserEntity, stringifyEntityRef, } from '@backstage/catalog-model'; -import { - catalogApiRef, - entityRouteParams, - useEntity, -} from '@backstage/plugin-catalog-react'; +import { catalogApiRef, useEntity } from '@backstage/plugin-catalog-react'; import { Box, createStyles, @@ -36,7 +32,6 @@ import { } from '@material-ui/core'; import Pagination from '@material-ui/lab/Pagination'; import React, { useState } from 'react'; -import { generatePath } from 'react-router-dom'; import useAsync from 'react-use/lib/useAsync'; import { @@ -52,6 +47,7 @@ import { getAllDesendantMembersForGroupEntity, removeDuplicateEntitiesFrom, } from '../../../../helpers/helpers'; +import { EntityRefLink } from '@backstage/plugin-catalog-react'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -102,15 +98,11 @@ const MemberComponent = (props: { member: UserEntity }) => { textAlign="center" > - - - + entityRef={props.member} + title={displayName} + /> {profile?.email && ( diff --git a/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx b/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx index 8dab5c4290..2725991d0d 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx @@ -34,6 +34,7 @@ import React from 'react'; import pluralize from 'pluralize'; import { catalogIndexRouteRef } from '../../../routes'; import { useGetEntities } from './useGetEntities'; +import { EntityRelationAggregation } from './types'; const useStyles = makeStyles((theme: BackstageTheme) => createStyles({ @@ -109,13 +110,11 @@ const EntityCountTile = ({ export const ComponentsGrid = ({ entity, relationsType, - isGroup, entityFilterKind, entityLimit = 6, }: { entity: Entity; - relationsType: string; - isGroup: boolean; + relationsType: EntityRelationAggregation; entityFilterKind?: string[]; entityLimit?: number; }) => { @@ -123,7 +122,6 @@ export const ComponentsGrid = ({ const { componentsWithCounters, loading, error } = useGetEntities( entity, relationsType, - isGroup, 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 30b7dee2de..72a912e16e 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx @@ -152,6 +152,27 @@ describe('OwnershipCard', () => { ], }; + const userEntity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'the-user', + }, + spec: { + memberOf: ['my-team'], + }, + relations: [ + { + type: 'memberOf', + targetRef: 'group:default/my-team', + }, + { + type: 'memberOf', + targetRef: 'group:custom/some-team', + }, + ], + }; + it('displays entity counts', async () => { const catalogApi: jest.Mocked = { getEntities: jest.fn(), @@ -263,33 +284,14 @@ describe('OwnershipCard', () => { }, ); - expect(getByText('OPENAPI').closest('a')).toHaveAttribute( - 'href', - '/create/?filters%5Bkind%5D=api&filters%5Btype%5D=openapi&filters%5Bowners%5D=my-team&filters%5Buser%5D=all', - ); + const href = getByText('OPENAPI').closest('a')?.href ?? ''; + // This env does not support URLSearchParams + const queryParams = decodeURIComponent(href); + + expect(queryParams).toContain('filters[owners]=my-team'); }); it('links to the catalog with the user and groups filters from an user profile', async () => { - const userEntity: UserEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - name: 'the-user', - }, - spec: { - memberOf: ['my-team'], - }, - relations: [ - { - type: 'memberOf', - targetRef: 'group:default/my-team', - }, - { - type: 'memberOf', - targetRef: 'group:custom/some-team', - }, - ], - }; const catalogApi: jest.Mocked = { getEntities: jest.fn(), } as any; @@ -299,7 +301,7 @@ describe('OwnershipCard', () => { const { getByText } = await renderInTestApp( - + , { @@ -309,9 +311,11 @@ describe('OwnershipCard', () => { }, ); - expect(getByText('OPENAPI').closest('a')).toHaveAttribute( - 'href', - '/create/?filters%5Bkind%5D=api&filters%5Btype%5D=openapi&filters%5Bowners%5D=user%3Athe-user&filters%5Bowners%5D=my-team&filters%5Bowners%5D=custom%2Fsome-team&filters%5Buser%5D=all', + const href = getByText('OPENAPI').closest('a')?.href ?? ''; + // This env does not support URLSearchParams + const queryParams = decodeURIComponent(href); + expect(queryParams).toMatch( + /filters\[owners\]=custom\/some\-team.*filters\[owners\]=user:the-user/, ); }); @@ -383,5 +387,74 @@ describe('OwnershipCard', () => { expect(getByTitle('Aggregated Relations')).toBeInTheDocument(); }); + + it('defaults to aggregated for User entity kind', async () => { + const catalogApi: jest.Mocked = { + getEntities: jest.fn(), + } as any; + + catalogApi.getEntities.mockImplementation(getEntitiesMock); + + const { getByLabelText } = await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/create': catalogIndexRouteRef, + }, + }, + ); + + expect(getByLabelText('Ownership Type Switch')).toBeChecked(); + }); + + it('defaults to direct for all entity kinds except User', async () => { + const catalogApi: jest.Mocked = { + getEntities: jest.fn(), + } as any; + + catalogApi.getEntities.mockImplementation(getEntitiesMock); + + const { getByLabelText } = await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/create': catalogIndexRouteRef, + }, + }, + ); + + expect(getByLabelText('Ownership Type Switch')).not.toBeChecked(); + }); + + it('defaults to provided relationsType', async () => { + const catalogApi: jest.Mocked = { + getEntities: jest.fn(), + } as any; + + catalogApi.getEntities.mockImplementation(getEntitiesMock); + + const { getByLabelText } = await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/create': catalogIndexRouteRef, + }, + }, + ); + + expect(getByLabelText('Ownership Type Switch')).not.toBeChecked(); + }); }); }); diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx index d104eb032d..791ce5ca64 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx @@ -25,8 +25,9 @@ import { Switch, Tooltip, } from '@material-ui/core'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { ComponentsGrid } from './ComponentsGrid'; +import { EntityRelationAggregation } from './types'; const useStyles = makeStyles(theme => ({ list: { @@ -56,7 +57,7 @@ export const OwnershipCard = (props: { variant?: InfoCardVariants; entityFilterKind?: string[]; hideRelationsToggle?: boolean; - relationsType?: string; + relationsType?: EntityRelationAggregation; entityLimit?: number; }) => { const { @@ -70,11 +71,18 @@ export const OwnershipCard = (props: { hideRelationsToggle === undefined ? false : hideRelationsToggle; const classes = useStyles(); const { entity } = useEntity(); - const isGroup = entity.kind === 'Group'; + + const defaultRelationsType = entity.kind === 'User' ? 'aggregated' : 'direct'; const [getRelationsType, setRelationsType] = useState( - relationsType || 'direct', + relationsType ?? defaultRelationsType, ); + useEffect(() => { + if (!relationsType) { + setRelationsType(defaultRelationsType); + } + }, [setRelationsType, defaultRelationsType, relationsType]); + return ( {!relationsToggle && ( @@ -95,14 +103,13 @@ export const OwnershipCard = (props: { - getRelationsType === 'direct' - ? setRelationsType('aggregated') - : setRelationsType('direct') - } + onChange={() => { + const updatedRelationsType = + getRelationsType === 'direct' ? 'aggregated' : 'direct'; + setRelationsType(updatedRelationsType); + }} name="pin" inputProps={{ 'aria-label': 'Ownership Type Switch' }} - disabled={!isGroup} /> Aggregated Relations @@ -114,7 +121,6 @@ export const OwnershipCard = (props: { entity={entity} entityLimit={entityLimit} relationsType={getRelationsType} - isGroup={isGroup} entityFilterKind={entityFilterKind} /> diff --git a/plugins/org/src/components/Cards/OwnershipCard/index.ts b/plugins/org/src/components/Cards/OwnershipCard/index.ts index dc0cef0226..a4798cc4bf 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/index.ts +++ b/plugins/org/src/components/Cards/OwnershipCard/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export * from './OwnershipCard'; +export * from './types'; diff --git a/plugins/org/src/components/Cards/OwnershipCard/types.ts b/plugins/org/src/components/Cards/OwnershipCard/types.ts new file mode 100644 index 0000000000..c1e95ee94e --- /dev/null +++ b/plugins/org/src/components/Cards/OwnershipCard/types.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. + */ + +/** @public */ +export type EntityRelationAggregation = 'direct' | 'aggregated'; diff --git a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts new file mode 100644 index 0000000000..191bd10284 --- /dev/null +++ b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts @@ -0,0 +1,150 @@ +/* + * 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 { CompoundEntityRef, Entity } from '@backstage/catalog-model'; +import { useGetEntities } from './useGetEntities'; +import { CatalogApi } from '@backstage/catalog-client'; +import { renderHook } from '@testing-library/react-hooks'; +import { getEntityRelations } from '@backstage/plugin-catalog-react'; + +const givenParentGroup = 'team.squad1'; +const givenLeafGroup = 'team.squad2'; +const givenUser = 'user.john'; +const givenParentGroupEntity = { + kind: 'Group', + metadata: { + name: givenParentGroup, + }, +} as Partial as Entity; +const givenLeafGroupEntity = { + kind: 'Group', + metadata: { + name: givenLeafGroup, + }, +} as Partial as Entity; +const givenUserEntity = { + kind: 'User', + metadata: { + name: givenUser, + }, +} as Partial as Entity; + +const catalogApiMock: Pick = { + getEntities: jest.fn(async () => Promise.resolve({ items: [] })), + getEntitiesByRefs: jest.fn(async ({ entityRefs: [ref] }) => + ref.includes(givenParentGroup) + ? { items: [givenParentGroupEntity] } + : { items: [givenLeafGroupEntity] }, + ), +}; + +jest.mock('@backstage/core-plugin-api', () => ({ + useApi: jest.fn(() => catalogApiMock), +})); +jest.mock('@backstage/plugin-catalog-react', () => ({ + catalogApiRef: {}, + getEntityRelations: jest.fn(entity => { + if (entity?.metadata.name === givenParentGroup) { + return [ + { + kind: 'Group', + namespace: 'default', + name: givenLeafGroup, + } as CompoundEntityRef, + ]; + } else if (entity?.kind === 'User') { + return [ + { + kind: 'Group', + namespace: 'default', + name: givenLeafGroup, + } as CompoundEntityRef, + ]; + } + + return []; + }) as typeof getEntityRelations, +})); + +describe('useGetEntities', () => { + const ownersFilter = (...owners: string[]) => + expect.objectContaining({ + filter: expect.arrayContaining([ + expect.objectContaining({ + 'relations.ownedBy': owners, + }), + ]), + }); + + describe('given aggregated relationsType', () => { + const whenHookIsCalledWith = async (_entity: Entity) => { + const hook = renderHook( + ({ entity }) => useGetEntities(entity, 'aggregated'), + { + initialProps: { entity: _entity }, + }, + ); + + await hook.waitForNextUpdate(); + }; + + it('given group entity should aggregate child ownership', async () => { + await whenHookIsCalledWith(givenParentGroupEntity); + expect(catalogApiMock.getEntities).toHaveBeenCalledWith( + ownersFilter( + `group:default/${givenParentGroup}`, + `group:default/${givenLeafGroup}`, + ), + ); + }); + + it('given user entity should aggregate parent ownership and direct', async () => { + await whenHookIsCalledWith(givenUserEntity); + expect(catalogApiMock.getEntities).toHaveBeenCalledWith( + ownersFilter( + `group:default/${givenLeafGroup}`, + `user:default/${givenUser}`, + ), + ); + }); + }); + + describe('given direct relationsType', () => { + const whenHookIsCalledWith = async (_entity: Entity) => { + const hook = renderHook( + ({ entity }) => useGetEntities(entity, 'direct'), + { + initialProps: { entity: _entity }, + }, + ); + + await hook.waitForNextUpdate(); + }; + + it('given group entity should return directly owned entities', async () => { + await whenHookIsCalledWith(givenLeafGroupEntity); + expect(catalogApiMock.getEntities).toHaveBeenCalledWith( + ownersFilter(`group:default/${givenLeafGroup}`), + ); + }); + + it('given user entity should return directly owned entities', async () => { + await whenHookIsCalledWith(givenUserEntity); + expect(catalogApiMock.getEntities).toHaveBeenCalledWith( + ownersFilter(`user:default/${givenUser}`), + ); + }); + }); +}); diff --git a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts index a59f534300..1ff01cd8e1 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts +++ b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts @@ -31,6 +31,7 @@ import limiterFactory from 'p-limit'; import { useApi } from '@backstage/core-plugin-api'; import useAsync from 'react-use/lib/useAsync'; import qs from 'qs'; +import { EntityRelationAggregation as EntityRelationsAggregation } from './types'; const limiter = limiterFactory(10); @@ -57,78 +58,109 @@ const getQueryParams = ( return qs.stringify({ filters }, { arrayFormat: 'repeat' }); }; -const getOwnersEntityRef = (owner: Entity): string[] => { - let owners = [stringifyEntityRef(owner)]; - if (owner.kind === 'User') { - const ownerGroups = getEntityRelations(owner, RELATION_MEMBER_OF, { - kind: 'Group', +const getMemberOfEntityRefs = (owner: Entity): string[] => { + const parentGroups = getEntityRelations(owner, RELATION_MEMBER_OF, { + kind: 'Group', + }); + + const ownerGroupsNames = parentGroups.map(({ kind, namespace, name }) => + stringifyEntityRef({ + kind, + namespace, + name, + }), + ); + + return [...ownerGroupsNames, stringifyEntityRef(owner)]; +}; + +const isEntity = (entity: Entity | undefined): entity is Entity => + entity !== undefined; + +const getChildOwnershipEntityRefs = async ( + entity: Entity, + catalogApi: CatalogApi, +): Promise => { + const childGroups = getEntityRelations(entity, RELATION_PARENT_OF, { + kind: 'Group', + }); + + const hasChildGroups = childGroups.length > 0; + + if (hasChildGroups) { + const entityRefs = childGroups.map(r => stringifyEntityRef(r)); + const childGroupResponse = await catalogApi.getEntitiesByRefs({ + fields: ['kind', 'metadata.namespace', 'metadata.name'], + entityRefs, }); - const ownerGroupsName = ownerGroups.map(ownerGroup => - stringifyEntityRef({ - kind: ownerGroup.kind, - namespace: ownerGroup.namespace, - name: ownerGroup.name, - }), - ); - owners = [...owners, ...ownerGroupsName]; + const childGroupEntities = childGroupResponse.items.filter(isEntity); + + return ( + await Promise.all( + childGroupEntities.map(childGroupEntity => + limiter(() => + getChildOwnershipEntityRefs(childGroupEntity, catalogApi), + ), + ), + ) + ).flatMap(aggregated => aggregated); } + + return [stringifyEntityRef(entity)]; +}; + +const getOwners = async ( + entity: Entity, + relations: EntityRelationsAggregation, + catalogApi: CatalogApi, +): Promise => { + const isGroup = entity.kind === 'Group'; + const isAggregated = relations === 'aggregated'; + const isUserEntity = entity.kind === 'User'; + + const owners: string[] = []; + + if (isAggregated && isGroup) { + const childEntityRefs = await getChildOwnershipEntityRefs( + entity, + catalogApi, + ); + owners.push(stringifyEntityRef(entity)); + owners.push.apply(owners, childEntityRefs); + } else if (isAggregated && isUserEntity) { + const parentEntityRefs = getMemberOfEntityRefs(entity); + owners.push.apply(owners, parentEntityRefs); + } else { + owners.push(stringifyEntityRef(entity)); + } + return owners; }; -const getAggregatedOwnersEntityRef = async ( - parentGroup: Entity, +const getOwnedEntitiesByOwners = ( + owners: string[], + kinds: string[], catalogApi: CatalogApi, -): Promise => { - const requestedEntities: Entity[] = []; - const outstandingEntities = new Map>(); - const processedEntities = new Set(); - requestedEntities.push(parentGroup); - let currentEntity = parentGroup; - - while (requestedEntities.length > 0) { - const childRelations = getEntityRelations( - currentEntity, - RELATION_PARENT_OF, +) => + catalogApi.getEntities({ + filter: [ { - kind: 'Group', + kind: kinds, + 'relations.ownedBy': owners, }, - ); - - await Promise.all( - childRelations.map(childGroup => - limiter(async () => { - const promise = catalogApi.getEntityByRef(childGroup); - outstandingEntities.set(childGroup.name, promise); - try { - const processedEntity = await promise; - if (processedEntity) { - requestedEntities.push(processedEntity); - } - } finally { - outstandingEntities.delete(childGroup.name); - } - }), - ), - ); - requestedEntities.shift(); - processedEntities.add( - stringifyEntityRef({ - kind: currentEntity.kind, - namespace: currentEntity.metadata.namespace, - name: currentEntity.metadata.name, - }), - ); - // always set currentEntity to the first element of array requestedEntities - currentEntity = requestedEntities[0]; - } - - return Array.from(processedEntities); -}; + ], + fields: [ + 'kind', + 'metadata.name', + 'metadata.namespace', + 'spec.type', + 'relations', + ], + }); export function useGetEntities( entity: Entity, - relationsType: string, - isGroup: boolean, + relations: EntityRelationsAggregation, entityFilterKind?: string[], entityLimit = 6, ): { @@ -151,25 +183,13 @@ export function useGetEntities( error, value: componentsWithCounters, } = useAsync(async () => { - const owners = - relationsType === 'aggregated' && isGroup - ? await getAggregatedOwnersEntityRef(entity, catalogApi) - : getOwnersEntityRef(entity); - const ownedEntitiesList = await catalogApi.getEntities({ - filter: [ - { - kind: kinds, - 'relations.ownedBy': owners, - }, - ], - fields: [ - 'kind', - 'metadata.name', - 'metadata.namespace', - 'spec.type', - 'relations', - ], - }); + const owners = await getOwners(entity, relations, catalogApi); + + const ownedEntitiesList = await getOwnedEntitiesByOwners( + owners, + kinds, + catalogApi, + ); const counts = ownedEntitiesList.items.reduce( (acc: EntityTypeProps[], ownedEntity) => { @@ -204,7 +224,7 @@ export function useGetEntities( kind: string; queryParams: string; }>; - }, [catalogApi, entity, relationsType]); + }, [catalogApi, entity, relations]); return { componentsWithCounters, diff --git a/plugins/org/src/setupTests.ts b/plugins/org/src/setupTests.ts index 28a35d2b06..09cb508b22 100644 --- a/plugins/org/src/setupTests.ts +++ b/plugins/org/src/setupTests.ts @@ -14,3 +14,7 @@ * limitations under the License. */ import '@testing-library/jest-dom'; + +beforeEach(() => { + jest.clearAllMocks(); +}); diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index 4ff94ae479..3919147d0a 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-pagerduty +## 0.6.5-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-home-react@0.1.3-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.6.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/plugin-home-react@0.1.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.6.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-home-react@0.1.3-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.6.2 + +### Patch Changes + +- 5003fc966741: Add new `disableChangeEvents` prop to `EntityPagerDutyCard` to hide the Change Events tab and disable fetching of change events for the PagerDuty service. +- Updated dependencies + - @backstage/plugin-home-react@0.1.2 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.6.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/plugin-home-react@0.1.2-next.0 + ## 0.6.2-next.0 ### Patch Changes diff --git a/plugins/pagerduty/api-report.md b/plugins/pagerduty/api-report.md index dd24aa9bec..24c2e103cf 100644 --- a/plugins/pagerduty/api-report.md +++ b/plugins/pagerduty/api-report.md @@ -12,22 +12,25 @@ import { ConfigApi } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { FetchApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { ReactNode } from 'react'; // @public (undocumented) export const EntityPagerDutyCard: ( props: EntityPagerDutyCardProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) export type EntityPagerDutyCardProps = { readOnly?: boolean; + disableChangeEvents?: boolean; }; // @public (undocumented) export const HomePagePagerDutyCard: ( props: CardExtensionProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) export type HomePagePagerDutyCardProps = PagerDutyEntity & { @@ -66,7 +69,7 @@ export type PagerDutyAssignee = { }; // @public @deprecated (undocumented) -export const PagerDutyCard: (props: EntityPagerDutyCardProps) => JSX.Element; +export const PagerDutyCard: (props: EntityPagerDutyCardProps) => JSX_2.Element; // @public @deprecated (undocumented) export type PagerDutyCardProps = EntityPagerDutyCardProps; @@ -214,7 +217,9 @@ export type PagerDutyUser = { }; // @public (undocumented) -export function TriggerButton(props: { children?: ReactNode }): JSX.Element; +export function TriggerButton(props: { + children?: ReactNode; +}): React_2.JSX.Element; // @public (undocumented) export class UnauthorizedError extends Error {} diff --git a/plugins/pagerduty/catalog-info.yaml b/plugins/pagerduty/catalog-info.yaml new file mode 100644 index 0000000000..c8585f0e55 --- /dev/null +++ b/plugins/pagerduty/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-pagerduty + title: '@backstage/plugin-pagerduty' + description: A Backstage plugin that integrates towards PagerDuty +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 13d56fd3f2..5420961a39 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-pagerduty", "description": "A Backstage plugin that integrates towards PagerDuty", - "version": "0.6.2-next.0", + "version": "0.6.5-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,6 +23,7 @@ "backstage", "pagerduty" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -62,7 +63,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/pagerduty/src/components/EntityPagerDutyCard/index.tsx b/plugins/pagerduty/src/components/EntityPagerDutyCard/index.tsx index 50e6f3a096..b252069b90 100644 --- a/plugins/pagerduty/src/components/EntityPagerDutyCard/index.tsx +++ b/plugins/pagerduty/src/components/EntityPagerDutyCard/index.tsx @@ -30,12 +30,19 @@ export const isPluginApplicableToEntity = (entity: Entity) => /** @public */ export type EntityPagerDutyCardProps = { readOnly?: boolean; + disableChangeEvents?: boolean; }; /** @public */ export const EntityPagerDutyCard = (props: EntityPagerDutyCardProps) => { - const { readOnly } = props; + const { readOnly, disableChangeEvents } = props; const { entity } = useEntity(); const pagerDutyEntity = getPagerDutyEntity(entity); - return ; + return ( + + ); }; diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx index bb89865c5f..332d823f1f 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx @@ -46,11 +46,12 @@ const BasicCard = ({ children }: { children: ReactNode }) => ( /** @public */ export type PagerDutyCardProps = PagerDutyEntity & { readOnly?: boolean; + disableChangeEvents?: boolean; }; /** @public */ export const PagerDutyCard = (props: PagerDutyCardProps) => { - const { readOnly, integrationKey, name } = props; + const { readOnly, disableChangeEvents, integrationKey, name } = props; const api = useApi(pagerDutyApiRef); const [refreshIncidents, setRefreshIncidents] = useState(false); const [refreshChangeEvents, setRefreshChangeEvents] = @@ -169,12 +170,16 @@ export const PagerDutyCard = (props: PagerDutyCardProps) => { refreshIncidents={refreshIncidents} /> - - - + <> + {disableChangeEvents !== true && ( + + + + )} + diff --git a/plugins/periskop-backend/CHANGELOG.md b/plugins/periskop-backend/CHANGELOG.md index 2bf8f6c147..af061f074a 100644 --- a/plugins/periskop-backend/CHANGELOG.md +++ b/plugins/periskop-backend/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/plugin-periskop-backend +## 0.1.22-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + +## 0.1.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + +## 0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + +## 0.1.19 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/config@1.0.8 + +## 0.1.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + ## 0.1.19-next.1 ### Patch Changes diff --git a/plugins/periskop-backend/api-report.md b/plugins/periskop-backend/api-report.md index 7906bc4785..46d5de8d93 100644 --- a/plugins/periskop-backend/api-report.md +++ b/plugins/periskop-backend/api-report.md @@ -12,7 +12,8 @@ import { Logger } from 'winston'; export function createRouter(options: RouterOptions): Promise; // @alpha -export const periskopPlugin: () => BackendFeature; +const periskopPlugin: () => BackendFeature; +export default periskopPlugin; // @public (undocumented) export interface RouterOptions { diff --git a/plugins/periskop-backend/catalog-info.yaml b/plugins/periskop-backend/catalog-info.yaml new file mode 100644 index 0000000000..8ad2a7f261 --- /dev/null +++ b/plugins/periskop-backend/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-periskop-backend + title: '@backstage/plugin-periskop-backend' +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: maintainers diff --git a/plugins/periskop-backend/package.json b/plugins/periskop-backend/package.json index 52c7d127b0..f1327810bf 100644 --- a/plugins/periskop-backend/package.json +++ b/plugins/periskop-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-periskop-backend", - "version": "0.1.19-next.1", + "version": "0.1.22-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/periskop-backend/src/index.ts b/plugins/periskop-backend/src/index.ts index a053633745..17a6d0d054 100644 --- a/plugins/periskop-backend/src/index.ts +++ b/plugins/periskop-backend/src/index.ts @@ -15,4 +15,4 @@ */ export * from './service/router'; -export { periskopPlugin } from './plugin'; +export { periskopPlugin as default } from './plugin'; diff --git a/plugins/periskop/CHANGELOG.md b/plugins/periskop/CHANGELOG.md index a12e00971f..b5d4598d24 100644 --- a/plugins/periskop/CHANGELOG.md +++ b/plugins/periskop/CHANGELOG.md @@ -1,5 +1,63 @@ # @backstage/plugin-periskop +## 0.1.22-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.1.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.1.21-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.1.19 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.1.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.1.19-next.1 ### Patch Changes diff --git a/plugins/periskop/api-report.md b/plugins/periskop/api-report.md index 3332dd9edb..578fb1dc49 100644 --- a/plugins/periskop/api-report.md +++ b/plugins/periskop/api-report.md @@ -10,6 +10,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ConfigApi } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; // @public (undocumented) export interface AggregatedError { @@ -24,7 +25,7 @@ export interface AggregatedError { } // @public (undocumented) -export const EntityPeriskopErrorsCard: () => JSX.Element; +export const EntityPeriskopErrorsCard: () => JSX_2.Element; // @public (undocumented) interface Error_2 { diff --git a/plugins/periskop/catalog-info.yaml b/plugins/periskop/catalog-info.yaml new file mode 100644 index 0000000000..b4ca9bdd34 --- /dev/null +++ b/plugins/periskop/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-periskop + title: '@backstage/plugin-periskop' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/periskop/package.json b/plugins/periskop/package.json index 3ac124e85c..5ba607e94b 100644 --- a/plugins/periskop/package.json +++ b/plugins/periskop/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-periskop", - "version": "0.1.19-next.1", + "version": "0.1.22-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,6 +18,7 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -37,6 +38,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "react-use": "^17.2.4" }, @@ -55,8 +57,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/luxon": "^3.0.0", - "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/permission-backend-module-policy-allow-all/.eslintrc.js b/plugins/permission-backend-module-policy-allow-all/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/permission-backend-module-policy-allow-all/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md new file mode 100644 index 0000000000..8ed771d68a --- /dev/null +++ b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md @@ -0,0 +1,16 @@ +# @backstage/plugin-permission-backend-module-allow-all-policy + +## 0.1.0-next.0 + +### Minor Changes + +- 5f7b2153526b: Created package with policy `permissionModuleAllowAllPolicy` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 diff --git a/plugins/permission-backend-module-policy-allow-all/README.md b/plugins/permission-backend-module-policy-allow-all/README.md new file mode 100644 index 0000000000..1cdeadcf95 --- /dev/null +++ b/plugins/permission-backend-module-policy-allow-all/README.md @@ -0,0 +1,5 @@ +# @backstage/plugin-permission-backend-module-allow-all-policy + +The allow all policy backend module for the permission plugin. + +_This plugin was created through the Backstage CLI_ diff --git a/plugins/permission-backend-module-policy-allow-all/api-report.md b/plugins/permission-backend-module-policy-allow-all/api-report.md new file mode 100644 index 0000000000..081655578d --- /dev/null +++ b/plugins/permission-backend-module-policy-allow-all/api-report.md @@ -0,0 +1,11 @@ +## API Report File for "@backstage/plugin-permission-backend-module-allow-all-policy" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @public +const permissionModuleAllowAllPolicy: () => BackendFeature; +export default permissionModuleAllowAllPolicy; +``` diff --git a/plugins/permission-backend-module-policy-allow-all/package.json b/plugins/permission-backend-module-policy-allow-all/package.json new file mode 100644 index 0000000000..76caea26be --- /dev/null +++ b/plugins/permission-backend-module-policy-allow-all/package.json @@ -0,0 +1,39 @@ +{ + "name": "@backstage/plugin-permission-backend-module-allow-all-policy", + "description": "Allow all policy backend module for the permission plugin.", + "version": "0.1.0-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "@backstage/plugin-permission-common": "workspace:^", + "@backstage/plugin-permission-node": "workspace:^" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/permission-backend-module-policy-allow-all/src/index.ts b/plugins/permission-backend-module-policy-allow-all/src/index.ts new file mode 100644 index 0000000000..49fefc99db --- /dev/null +++ b/plugins/permission-backend-module-policy-allow-all/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. + */ + +/** + * The allow all policy backend module for the permission plugin. + * + * @packageDocumentation + */ + +export { permissionModuleAllowAllPolicy as default } from './module'; diff --git a/plugins/permission-backend-module-policy-allow-all/src/module.ts b/plugins/permission-backend-module-policy-allow-all/src/module.ts new file mode 100644 index 0000000000..47ab202a00 --- /dev/null +++ b/plugins/permission-backend-module-policy-allow-all/src/module.ts @@ -0,0 +1,36 @@ +/* + * 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 { createBackendModule } from '@backstage/backend-plugin-api'; +import { policyExtensionPoint } from '@backstage/plugin-permission-node/alpha'; +import { AllowAllPermissionPolicy } from './policy'; + +/** + * A permission policy module that allows all requests. + * + * @public + */ +export const permissionModuleAllowAllPolicy = createBackendModule({ + moduleId: 'allowAllPolicy', + pluginId: 'permission', + register(reg) { + reg.registerInit({ + deps: { policy: policyExtensionPoint }, + async init({ policy }) { + policy.setPolicy(new AllowAllPermissionPolicy()); + }, + }); + }, +}); diff --git a/plugins/permission-backend-module-policy-allow-all/src/policy.ts b/plugins/permission-backend-module-policy-allow-all/src/policy.ts new file mode 100644 index 0000000000..da853e47db --- /dev/null +++ b/plugins/permission-backend-module-policy-allow-all/src/policy.ts @@ -0,0 +1,36 @@ +/* + * 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 { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; +import { + AuthorizeResult, + PolicyDecision, +} from '@backstage/plugin-permission-common'; +import { + PermissionPolicy, + PolicyQuery, +} from '@backstage/plugin-permission-node'; + +export class AllowAllPermissionPolicy implements PermissionPolicy { + async handle( + _request: PolicyQuery, + _user?: BackstageIdentityResponse, + ): Promise { + return { + result: AuthorizeResult.ALLOW, + }; + } +} diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index 2d641baae8..ab2ef342f6 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,71 @@ # @backstage/plugin-permission-backend +## 0.5.26-next.2 + +### Patch Changes + +- 84ad6fccd4d5: Moved `permissionModuleAllowAllPolicy` to `@backstage/plugin-permission-backend-module-allow-all-policy` +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/errors@1.2.1 + +## 0.5.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/errors@1.2.1 + +## 0.5.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + +## 0.5.23 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- 0d6ddc62d8da: Refactor backend plugin creation parameter from callback to object. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + +## 0.5.23-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + ## 0.5.23-next.1 ### Patch Changes diff --git a/plugins/permission-backend/alpha-api-report.md b/plugins/permission-backend/alpha-api-report.md index 926691930b..acface76a4 100644 --- a/plugins/permission-backend/alpha-api-report.md +++ b/plugins/permission-backend/alpha-api-report.md @@ -6,10 +6,8 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; // @alpha -export const permissionModuleAllowAllPolicy: () => BackendFeature; - -// @alpha -export const permissionPlugin: () => BackendFeature; +const permissionPlugin: () => BackendFeature; +export default permissionPlugin; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/permission-backend/catalog-info.yaml b/plugins/permission-backend/catalog-info.yaml new file mode 100644 index 0000000000..13baa09802 --- /dev/null +++ b/plugins/permission-backend/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-permission-backend + title: '@backstage/plugin-permission-backend' +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: permission-maintainers diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index 269cb12061..460bc32b10 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -1,13 +1,11 @@ { "name": "@backstage/plugin-permission-backend", - "version": "0.5.23-next.1", + "version": "0.5.26-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" }, "exports": { ".": "./src/index.ts", diff --git a/plugins/permission-backend/src/alpha.ts b/plugins/permission-backend/src/alpha.ts index f6a2bb4f09..c0283c6fd0 100644 --- a/plugins/permission-backend/src/alpha.ts +++ b/plugins/permission-backend/src/alpha.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { permissionPlugin, permissionModuleAllowAllPolicy } from './plugin'; +export { permissionPlugin as default } from './plugin'; diff --git a/plugins/permission-backend/src/plugin.ts b/plugins/permission-backend/src/plugin.ts index a6b731b120..9cf0024e47 100644 --- a/plugins/permission-backend/src/plugin.ts +++ b/plugins/permission-backend/src/plugin.ts @@ -17,18 +17,9 @@ import { loggerToWinstonLogger } from '@backstage/backend-common'; import { coreServices, - createBackendModule, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; -import { - AuthorizeResult, - PolicyDecision, -} from '@backstage/plugin-permission-common'; -import { - PermissionPolicy, - PolicyQuery, -} from '@backstage/plugin-permission-node'; +import { PermissionPolicy } from '@backstage/plugin-permission-node'; import { policyExtensionPoint, PolicyExtensionPoint, @@ -46,41 +37,12 @@ class PolicyExtensionPointImpl implements PolicyExtensionPoint { } } -/** - * A permission policy module that allows all requests. - * - * @alpha - */ -export const permissionModuleAllowAllPolicy = createBackendModule({ - moduleId: 'allowAllPolicy', - pluginId: 'permission', - register(reg) { - class AllowAllPermissionPolicy implements PermissionPolicy { - async handle( - _request: PolicyQuery, - _user?: BackstageIdentityResponse, - ): Promise { - return { - result: AuthorizeResult.ALLOW, - }; - } - } - - reg.registerInit({ - deps: { policy: policyExtensionPoint }, - async init({ policy }) { - policy.setPolicy(new AllowAllPermissionPolicy()); - }, - }); - }, -}); - /** * Permission plugin * * @alpha */ -export const permissionPlugin = createBackendPlugin(() => ({ +export const permissionPlugin = createBackendPlugin({ pluginId: 'permission', register(env) { const policies = new PolicyExtensionPointImpl(); @@ -99,7 +61,7 @@ export const permissionPlugin = createBackendPlugin(() => ({ const winstonLogger = loggerToWinstonLogger(logger); if (!policies.policy) { throw new Error( - 'No policy module installed! Please install a policy module. If you want to allow all requests, use permissionModuleAllowAllPolicy', + 'No policy module installed! Please install a policy module. If you want to allow all requests, use @backstage/plugin-permission-backend-module-allow-all-policy permissionModuleAllowAllPolicy', ); } @@ -115,4 +77,4 @@ export const permissionPlugin = createBackendPlugin(() => ({ }, }); }, -})); +}); diff --git a/plugins/permission-common/CHANGELOG.md b/plugins/permission-common/CHANGELOG.md index b0a2cb2b42..caf6bacba6 100644 --- a/plugins/permission-common/CHANGELOG.md +++ b/plugins/permission-common/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-permission-common +## 0.7.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.7.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + ## 0.7.7 ### Patch Changes @@ -433,7 +451,7 @@ - b768259244: **BREAKING**: Authorize API request and response types have been updated. The existing `AuthorizeRequest` and `AuthorizeResponse` types now match the entire request and response objects for the /authorize endpoint, and new types `AuthorizeQuery` and `AuthorizeDecision` have been introduced for individual items in the request and response batches respectively. - **BREAKING**: PermissionClient has been updated to use the new request and response format in the latest version of @backstage/permission-backend. + **BREAKING**: PermissionClient has been updated to use the new request and response format in the latest version of @backstage/plugin-permission-backend. ### Patch Changes @@ -446,7 +464,7 @@ - b768259244: **BREAKING**: Authorize API request and response types have been updated. The existing `AuthorizeRequest` and `AuthorizeResponse` types now match the entire request and response objects for the /authorize endpoint, and new types `AuthorizeQuery` and `AuthorizeDecision` have been introduced for individual items in the request and response batches respectively. - **BREAKING**: PermissionClient has been updated to use the new request and response format in the latest version of @backstage/permission-backend. + **BREAKING**: PermissionClient has been updated to use the new request and response format in the latest version of @backstage/plugin-permission-backend. ### Patch Changes diff --git a/plugins/permission-common/catalog-info.yaml b/plugins/permission-common/catalog-info.yaml new file mode 100644 index 0000000000..d8d8918530 --- /dev/null +++ b/plugins/permission-common/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-permission-common + title: '@backstage/plugin-permission-common' + description: Isomorphic types and client for Backstage permissions and authorization +spec: + lifecycle: experimental + type: backstage-common-library + owner: permission-maintainers diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index 32f137f560..a8b6b30169 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-common", "description": "Isomorphic types and client for Backstage permissions and authorization", - "version": "0.7.7", + "version": "0.7.8-next.1", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -29,6 +29,7 @@ "config.d.ts" ], "configSchema": "config.d.ts", + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index 41e5f3ca5b..d44cc37877 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/plugin-permission-node +## 0.7.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/errors@1.2.1 + +## 0.7.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/errors@1.2.1 + +## 0.7.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + +## 0.7.11 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + +## 0.7.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + ## 0.7.11-next.1 ### Patch Changes diff --git a/plugins/permission-node/catalog-info.yaml b/plugins/permission-node/catalog-info.yaml new file mode 100644 index 0000000000..a85f952af8 --- /dev/null +++ b/plugins/permission-node/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-permission-node + title: '@backstage/plugin-permission-node' + description: Common permission and authorization utilities for backend plugins +spec: + lifecycle: experimental + type: backstage-node-library + owner: permission-maintainers diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index c5f30ff4a0..d88a691ba0 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,14 +1,12 @@ { "name": "@backstage/plugin-permission-node", "description": "Common permission and authorization utilities for backend plugins", - "version": "0.7.11-next.1", + "version": "0.7.14-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" }, "exports": { ".": "./src/index.ts", diff --git a/plugins/permission-react/CHANGELOG.md b/plugins/permission-react/CHANGELOG.md index 47aa6a70a3..2fcfc4b3a5 100644 --- a/plugins/permission-react/CHANGELOG.md +++ b/plugins/permission-react/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-permission-react +## 0.4.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + +## 0.4.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + +## 0.4.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-permission-common@0.7.7 + ## 0.4.14 ### Patch Changes diff --git a/plugins/permission-react/api-report.md b/plugins/permission-react/api-report.md index 293343b7ac..3c1961c92c 100644 --- a/plugins/permission-react/api-report.md +++ b/plugins/permission-react/api-report.md @@ -12,6 +12,7 @@ import { EvaluatePermissionRequest } from '@backstage/plugin-permission-common'; import { EvaluatePermissionResponse } from '@backstage/plugin-permission-common'; import { IdentityApi } from '@backstage/core-plugin-api'; import { Permission } from '@backstage/plugin-permission-common'; +import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { ResourcePermission } from '@backstage/plugin-permission-common'; @@ -65,7 +66,7 @@ export const PermissionedRoute: ( resourceRef: string | undefined; } ), -) => JSX.Element; +) => React_2.JSX.Element; // @public export function RequirePermission( diff --git a/plugins/permission-react/catalog-info.yaml b/plugins/permission-react/catalog-info.yaml new file mode 100644 index 0000000000..119f7320ab --- /dev/null +++ b/plugins/permission-react/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-permission-react + title: '@backstage/plugin-permission-react' +spec: + lifecycle: experimental + type: backstage-web-library + owner: permission-maintainers diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index a8308b5a75..8c7c410909 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.14", + "version": "0.4.15-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,6 +21,7 @@ "keywords": [ "backstage" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/plugins/playlist-backend/CHANGELOG.md b/plugins/playlist-backend/CHANGELOG.md index f55ad9991e..cd800dabd1 100644 --- a/plugins/playlist-backend/CHANGELOG.md +++ b/plugins/playlist-backend/CHANGELOG.md @@ -1,5 +1,75 @@ # @backstage/plugin-playlist-backend +## 0.3.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-playlist-common@0.1.10-next.1 + +## 0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/errors@1.2.1 + - @backstage/plugin-playlist-common@0.1.10-next.0 + +## 0.3.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-playlist-common@0.1.9 + +## 0.3.4 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-playlist-common@0.1.9 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + +## 0.3.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + ## 0.3.4-next.1 ### Patch Changes diff --git a/plugins/playlist-backend/catalog-info.yaml b/plugins/playlist-backend/catalog-info.yaml new file mode 100644 index 0000000000..79bdfd41da --- /dev/null +++ b/plugins/playlist-backend/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-playlist-backend + title: '@backstage/plugin-playlist-backend' +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: kuangp diff --git a/plugins/playlist-backend/package.json b/plugins/playlist-backend/package.json index 5285406e58..39670326ce 100644 --- a/plugins/playlist-backend/package.json +++ b/plugins/playlist-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-playlist-backend", - "version": "0.3.4-next.1", + "version": "0.3.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/playlist-common/CHANGELOG.md b/plugins/playlist-common/CHANGELOG.md index ed624302c5..9dcc158cc7 100644 --- a/plugins/playlist-common/CHANGELOG.md +++ b/plugins/playlist-common/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-playlist-common +## 0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.8-next.1 + +## 0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.8-next.0 + +## 0.1.9 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/plugin-permission-common@0.7.7 + ## 0.1.9-next.0 ### Patch Changes diff --git a/plugins/playlist-common/catalog-info.yaml b/plugins/playlist-common/catalog-info.yaml new file mode 100644 index 0000000000..9367544c2c --- /dev/null +++ b/plugins/playlist-common/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-playlist-common + title: '@backstage/plugin-playlist-common' + description: Common functionalities for the playlist plugin +spec: + lifecycle: experimental + type: backstage-common-library + owner: kuangp diff --git a/plugins/playlist-common/package.json b/plugins/playlist-common/package.json index 2ac82b4852..31ca2492a6 100644 --- a/plugins/playlist-common/package.json +++ b/plugins/playlist-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-playlist-common", "description": "Common functionalities for the playlist plugin", - "version": "0.1.9-next.0", + "version": "0.1.10-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,6 +20,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/playlist-common" }, + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/plugins/playlist/CHANGELOG.md b/plugins/playlist/CHANGELOG.md index 3850cc3e5d..5590049552 100644 --- a/plugins/playlist/CHANGELOG.md +++ b/plugins/playlist/CHANGELOG.md @@ -1,5 +1,88 @@ # @backstage/plugin-playlist +## 0.1.16-next.2 + +### Patch Changes + +- 79847cb73ed7: Updated latest playlist screen in readme document +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/plugin-search-react@1.7.0-next.2 + - @backstage/plugin-permission-react@0.4.15-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-playlist-common@0.1.10-next.1 + +## 0.1.16-next.1 + +### Patch Changes + +- ec4b0f8cd957: change color of delete icon to secondary in playlist table +- b5ba33a92750: Limit the use of the same playlist name when adding a playlist +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/plugin-search-react@1.7.0-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-react@0.4.15-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-playlist-common@0.1.10-next.0 + +## 0.1.15-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.15-next.0 + - @backstage/plugin-playlist-common@0.1.9 + - @backstage/plugin-search-react@1.6.5-next.0 + +## 0.1.13 + +### Patch Changes + +- d1e0588324d1: Displaying an alert popup each time the Playlist is created or deleted +- 12a8c94eda8d: Add package repository and homepage metadata +- 8a997547f02a: Adding alert popup whenever user delete the entities from playlist entity table +- Updated dependencies + - @backstage/plugin-playlist-common@0.1.9 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/plugin-search-react@1.6.4 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-react@0.4.14 + +## 0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.1.13-next.1 ### Patch Changes diff --git a/plugins/playlist/README.md b/plugins/playlist/README.md index e35f94b9e5..25caa02742 100644 --- a/plugins/playlist/README.md +++ b/plugins/playlist/README.md @@ -154,6 +154,10 @@ playlist: ![Create New Playlist example](./docs/playlist-create-new.png) +### Edit Existing Playlist + +![Edit Existing Playlist example](./docs/playlist-edit-existing.png) + ### Add Entities to Playlist ![Add Entities to Playlist example](./docs/playlist-add-entities.png) diff --git a/plugins/playlist/api-report.md b/plugins/playlist/api-report.md index 7a732a4a53..cf0ebe8a37 100644 --- a/plugins/playlist/api-report.md +++ b/plugins/playlist/api-report.md @@ -10,6 +10,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { FetchApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { Playlist } from '@backstage/plugin-playlist-common'; import { PlaylistMetadata } from '@backstage/plugin-playlist-common'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -17,7 +18,7 @@ import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) export const EntityPlaylistDialog: ( props: EntityPlaylistDialogProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) export type EntityPlaylistDialogProps = { @@ -93,7 +94,7 @@ export class PlaylistClient implements PlaylistApi { } // @public (undocumented) -export const PlaylistIndexPage: () => JSX.Element; +export const PlaylistIndexPage: () => JSX_2.Element; // @public (undocumented) export const playlistPlugin: BackstagePlugin< diff --git a/plugins/playlist/catalog-info.yaml b/plugins/playlist/catalog-info.yaml new file mode 100644 index 0000000000..1b8eaaabb0 --- /dev/null +++ b/plugins/playlist/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-playlist + title: '@backstage/plugin-playlist' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: kuangp diff --git a/plugins/playlist/docs/playlist-edit-existing.png b/plugins/playlist/docs/playlist-edit-existing.png new file mode 100644 index 0000000000..64d3bc1381 Binary files /dev/null and b/plugins/playlist/docs/playlist-edit-existing.png differ diff --git a/plugins/playlist/docs/playlist-view-single.png b/plugins/playlist/docs/playlist-view-single.png index 0890c5013b..04f62bdee1 100644 Binary files a/plugins/playlist/docs/playlist-view-single.png and b/plugins/playlist/docs/playlist-view-single.png differ diff --git a/plugins/playlist/package.json b/plugins/playlist/package.json index 69e02df8dd..90b206285e 100644 --- a/plugins/playlist/package.json +++ b/plugins/playlist/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-playlist", - "version": "0.1.13-next.1", + "version": "0.1.16-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,6 +18,7 @@ "url": "https://github.com/backstage/backstage", "directory": "plugins/playlist" }, + "sideEffects": false, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", @@ -42,6 +43,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "lodash": "^4.17.21", "pluralize": "^8.0.0", "qs": "^6.9.4", @@ -63,8 +65,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", - "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0", "swr": "^2.0.0" }, diff --git a/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.test.tsx b/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.test.tsx index 9f30d7608c..b01b5acbf2 100644 --- a/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.test.tsx +++ b/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.test.tsx @@ -18,11 +18,32 @@ import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { fireEvent, getByRole, waitFor } from '@testing-library/react'; import { act } from '@testing-library/react-hooks'; +import { PlaylistApi, playlistApiRef } from '../../api'; import React from 'react'; import { PlaylistEditDialog } from './PlaylistEditDialog'; describe('', () => { + const samplePlaylists = [ + { + id: 'id1', + name: 'playlist-1', + owner: 'group:default/some-owner', + public: true, + entities: 1, + followers: 2, + isFollowing: false, + }, + { + id: 'id2', + name: 'playlist-2', + owner: 'group:default/another-owner', + public: true, + entities: 2, + followers: 1, + isFollowing: true, + }, + ]; it('handle saving with an edited playlist', async () => { const identityApi: Partial = { getBackstageIdentity: async () => ({ @@ -33,8 +54,18 @@ describe('', () => { }; const mockOnSave = jest.fn().mockImplementation(async () => {}); + const playlistApi: Partial = { + getAllPlaylists: jest + .fn() + .mockImplementation(async () => samplePlaylists), + }; const rendered = await renderInTestApp( - + , ); @@ -83,4 +114,53 @@ describe('', () => { }); }); }); + + it('displays duplicate validation message for playlist name', async () => { + const identityApi: Partial = { + getBackstageIdentity: async () => ({ + type: 'user', + userEntityRef: 'user:default/me', + ownershipEntityRefs: ['group:default/test-owner', 'user:default/me'], + }), + }; + + const mockOnSave = jest.fn().mockImplementation(async () => {}); + const playlistApi: Partial = { + getAllPlaylists: jest + .fn() + .mockImplementation(async () => [...samplePlaylists]), + }; + + const rendered = await renderInTestApp( + + + , + ); + + act(() => { + fireEvent.input( + getByRole(rendered.getByTestId('edit-dialog-name-input'), 'textbox'), + { + target: { + value: 'playlist-1', + }, + }, + ); + + fireEvent.click(rendered.getByTestId('edit-dialog-save-button')); + }); + + await waitFor(() => { + expect( + rendered.getByText('A playlist with this name already exists'), + ).toBeInTheDocument(); + }); + + expect(mockOnSave).not.toHaveBeenCalled(); + }); }); diff --git a/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.tsx b/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.tsx index a213c7c51c..97bd4728b9 100644 --- a/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.tsx +++ b/plugins/playlist/src/components/PlaylistEditDialog/PlaylistEditDialog.tsx @@ -18,6 +18,7 @@ import { parseEntityRef } from '@backstage/catalog-model'; import { identityApiRef, useApi } from '@backstage/core-plugin-api'; import { humanizeEntityRef } from '@backstage/plugin-catalog-react'; import { PlaylistMetadata } from '@backstage/plugin-playlist-common'; +import { playlistApiRef } from '../../api'; import { Button, CircularProgress, @@ -35,7 +36,7 @@ import { Select, TextField, } from '@material-ui/core'; -import React from 'react'; +import React, { useState } from 'react'; import { useForm, Controller } from 'react-hook-form'; import useAsync from 'react-use/lib/useAsync'; import useAsyncFn from 'react-use/lib/useAsyncFn'; @@ -74,6 +75,13 @@ export const PlaylistEditDialog = ({ }: PlaylistEditDialogProps) => { const classes = useStyles(); const identityApi = useApi(identityApiRef); + const playlistApi = useApi(playlistApiRef); + const [playlistPromise] = useState(() => + playlistApi.getAllPlaylists({}).catch(() => { + // We ensure that this promise never can throw, to make its usage simpler in the code below + return []; + }), + ); const { loading: loadingOwnership, value: ownershipRefs } = useAsync(async () => { @@ -81,6 +89,15 @@ export const PlaylistEditDialog = ({ return ownershipEntityRefs; }, []); + const nameIsUnique = async (name: string) => { + const playlists = await playlistPromise; + if (name !== playlist.name && playlists.some(p => p.name === name)) { + return 'A playlist with this name already exists'; + } + + return true; + }; + const defaultValues = { ...playlist, public: playlist.public.toString(), @@ -117,13 +134,17 @@ export const PlaylistEditDialog = ({ nameIsUnique(value), + }} render={({ field }) => ( searchBarContainer: { borderRadius: 30, display: 'flex', - height: '2.4em', + height: '100%', + boxShadow: 'none', }, gridContainer: { height: '100%', diff --git a/plugins/playlist/src/components/PlaylistPage/PlaylistEntitiesTable.tsx b/plugins/playlist/src/components/PlaylistPage/PlaylistEntitiesTable.tsx index 3d8efe3bc5..c74859ce20 100644 --- a/plugins/playlist/src/components/PlaylistPage/PlaylistEntitiesTable.tsx +++ b/plugins/playlist/src/components/PlaylistPage/PlaylistEntitiesTable.tsx @@ -21,7 +21,7 @@ import { Table, TableFilter, } from '@backstage/core-components'; -import { errorApiRef, useApi } from '@backstage/core-plugin-api'; +import { errorApiRef, useApi, alertApiRef } from '@backstage/core-plugin-api'; import { EntityRefLink } from '@backstage/plugin-catalog-react'; import { usePermission } from '@backstage/plugin-permission-react'; import { permissions } from '@backstage/plugin-playlist-common'; @@ -42,6 +42,7 @@ export const PlaylistEntitiesTable = ({ }) => { const errorApi = useApi(errorApiRef); const playlistApi = useApi(playlistApiRef); + const alertApi = useApi(alertApiRef); const [openAddEntitiesDrawer, setOpenAddEntitiesDrawer] = useState(false); const { allowed: editAllowed } = usePermission({ @@ -73,16 +74,30 @@ export const PlaylistEntitiesTable = ({ const removeEntity = useCallback( async (_, entity: Entity | Entity[]) => { try { + const entityArray = [entity].flat(); + const entityNames = entityArray.map( + item => item.metadata.title ?? item.metadata.name, + ); await playlistApi.removePlaylistEntities( playlistId, [entity].flat().map(stringifyEntityRef), ); loadEntities(); + const message = + entityNames.length === 1 + ? `Removed entity '${entityNames[0]}'` + : `Removed entities: '${entityNames.join("', '")}'`; + + alertApi.post({ + message, + severity: 'success', + display: 'transient', + }); } catch (e) { errorApi.post(e); } }, - [errorApi, loadEntities, playlistApi, playlistId], + [errorApi, loadEntities, playlistApi, playlistId, alertApi], ); const singularTitleLowerCase = useTitle({ @@ -93,7 +108,7 @@ export const PlaylistEntitiesTable = ({ const actions = editAllowed ? [ { - icon: DeleteIcon, + icon: () => , tooltip: `Remove from ${singularTitleLowerCase}`, onClick: removeEntity, }, diff --git a/plugins/playlist/src/components/PlaylistPage/PlaylistHeader.tsx b/plugins/playlist/src/components/PlaylistPage/PlaylistHeader.tsx index f892e8de7f..23a41b734b 100644 --- a/plugins/playlist/src/components/PlaylistPage/PlaylistHeader.tsx +++ b/plugins/playlist/src/components/PlaylistPage/PlaylistHeader.tsx @@ -81,6 +81,7 @@ export const PlaylistHeader = ({ playlist, onUpdate }: PlaylistHeaderProps) => { const playlistApi = useApi(playlistApiRef); const navigate = useNavigate(); const rootRoute = useRouteRef(rootRouteRef); + const [openEditDialog, setOpenEditDialog] = useState(false); const [openDeleteDialog, setOpenDeleteDialog] = useState(false); @@ -99,12 +100,17 @@ export const PlaylistHeader = ({ playlist, onUpdate }: PlaylistHeaderProps) => { try { await playlistApi.updatePlaylist({ ...update, id: playlist.id }); setOpenEditDialog(false); - onUpdate(); + let message = `Updated playlist '${playlist.name}'`; + if (update.name !== playlist.name) { + message = `Updated playlist name '${playlist.name}' to '${update.name}'`; + } + alertApi.post({ - message: `Updated playlist '${playlist.name}'`, + message, severity: 'success', display: 'transient', }); + onUpdate(); } catch (e) { errorApi.post(e); } @@ -116,15 +122,16 @@ export const PlaylistHeader = ({ playlist, onUpdate }: PlaylistHeaderProps) => { try { await playlistApi.deletePlaylist(playlist.id); navigate(rootRoute()); + const message = `Deleted playlist '${playlist.name}'`; alertApi.post({ - message: `Deleted playlist '${playlist.name}'`, + message, severity: 'success', display: 'transient', }); } catch (e) { errorApi.post(e); } - }, [playlistApi]); + }, [playlistApi, alertApi]); const singularTitle = useTitle({ pluralize: false, @@ -166,7 +173,7 @@ export const PlaylistHeader = ({ playlist, onUpdate }: PlaylistHeaderProps) => { }, { label: `Delete ${singularTitle}`, - icon: , + icon: , disabled: !deleteAllowed, onClick: () => setOpenDeleteDialog(true), }, diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index 4a9e2bdb70..92211efac1 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-proxy-backend +## 0.3.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + +## 0.3.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + +## 0.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + +## 0.3.0 + +### Minor Changes + +- 7daf65bfcfa1: Defining proxy endpoints directly under the root `proxy` configuration key is deprecated. Endpoints should now be declared under `proxy.endpoints` instead. The `skipInvalidProxies` and `reviveConsumedRequestBodies` can now also be configured through static configuration. + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 4b82382ed8c2: Fixed invalid configuration schema. The configuration schema may be more strict as a result. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/config@1.0.8 + +## 0.3.0-next.2 + +### Minor Changes + +- 7daf65bfcfa1: Defining proxy endpoints directly under the root `proxy` configuration key is deprecated. Endpoints should now be declared under `proxy.endpoints` instead. The `skipInvalidProxies` and `reviveConsumedRequestBodies` can now also be configured through static configuration. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + ## 0.2.42-next.1 ### Patch Changes diff --git a/plugins/proxy-backend/api-report.md b/plugins/proxy-backend/api-report.md index 9c5cf5f9ef..ae327d485a 100644 --- a/plugins/proxy-backend/api-report.md +++ b/plugins/proxy-backend/api-report.md @@ -13,7 +13,8 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; export function createRouter(options: RouterOptions): Promise; // @alpha -export const proxyPlugin: () => BackendFeature; +const proxyPlugin: () => BackendFeature; +export default proxyPlugin; // @public (undocumented) export interface RouterOptions { diff --git a/plugins/proxy-backend/catalog-info.yaml b/plugins/proxy-backend/catalog-info.yaml new file mode 100644 index 0000000000..a60112d9f3 --- /dev/null +++ b/plugins/proxy-backend/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-proxy-backend + title: '@backstage/plugin-proxy-backend' + description: >- + A Backstage backend plugin that helps you set up proxy endpoints in the + backend +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: maintainers diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 959c946d55..62a95cb5a7 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-proxy-backend", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", - "version": "0.2.42-next.1", + "version": "0.3.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/proxy-backend/src/index.ts b/plugins/proxy-backend/src/index.ts index 2658a557b7..3163abd381 100644 --- a/plugins/proxy-backend/src/index.ts +++ b/plugins/proxy-backend/src/index.ts @@ -21,4 +21,4 @@ */ export * from './service'; -export { proxyPlugin } from './plugin'; +export { proxyPlugin as default } from './plugin'; diff --git a/plugins/puppetdb/CHANGELOG.md b/plugins/puppetdb/CHANGELOG.md index dbd5fde6df..22f96ad52c 100644 --- a/plugins/puppetdb/CHANGELOG.md +++ b/plugins/puppetdb/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/plugin-puppetdb +## 0.1.7-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.1.6-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.1.4-next.0 ### Patch Changes diff --git a/plugins/puppetdb/api-report.md b/plugins/puppetdb/api-report.md index 48a3276460..c7d1d0ff0e 100644 --- a/plugins/puppetdb/api-report.md +++ b/plugins/puppetdb/api-report.md @@ -7,7 +7,9 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; import { PathParams } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { SubRouteRef } from '@backstage/core-plugin-api'; @@ -15,7 +17,7 @@ import { SubRouteRef } from '@backstage/core-plugin-api'; export const isPuppetDbAvailable: (entity: Entity) => boolean; // @public -export const PuppetDbPage: () => JSX.Element; +export const PuppetDbPage: () => JSX_2.Element; // @public const puppetdbPlugin: BackstagePlugin<{}, {}, {}>; @@ -29,7 +31,7 @@ export const puppetDbReportRouteRef: SubRouteRef>; export const puppetDbRouteRef: RouteRef; // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/puppetdb/catalog-info.yaml b/plugins/puppetdb/catalog-info.yaml new file mode 100644 index 0000000000..98c8d34d81 --- /dev/null +++ b/plugins/puppetdb/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-puppetdb + title: '@backstage/plugin-puppetdb' + description: >- + Backstage plugin to visualize resource information and Puppet facts from + PuppetDB. +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/puppetdb/package.json b/plugins/puppetdb/package.json index ef9370200e..8596213746 100644 --- a/plugins/puppetdb/package.json +++ b/plugins/puppetdb/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-puppetdb", "description": "Backstage plugin to visualize resource information and Puppet facts from PuppetDB.", - "version": "0.1.4-next.0", + "version": "0.1.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,6 +24,7 @@ "puppetdb", "puppet" ], + "sideEffects": false, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", @@ -43,6 +44,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.57", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -58,8 +60,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", - "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.1" }, "files": [ diff --git a/plugins/rollbar-backend/CHANGELOG.md b/plugins/rollbar-backend/CHANGELOG.md index 9789127cc4..a09b37a21e 100644 --- a/plugins/rollbar-backend/CHANGELOG.md +++ b/plugins/rollbar-backend/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-rollbar-backend +## 0.1.48-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + +## 0.1.48-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + +## 0.1.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/config@1.0.8 + +## 0.1.45 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/config@1.0.8 + +## 0.1.45-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.2-next.2 + ## 0.1.45-next.1 ### Patch Changes diff --git a/plugins/rollbar-backend/catalog-info.yaml b/plugins/rollbar-backend/catalog-info.yaml new file mode 100644 index 0000000000..132695d53b --- /dev/null +++ b/plugins/rollbar-backend/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-rollbar-backend + title: '@backstage/plugin-rollbar-backend' + description: A Backstage backend plugin that integrates towards Rollbar +spec: + lifecycle: experimental + type: backstage-backend-plugin + owner: andrewthauer diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index 3def2395f1..b9bae6c5eb 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar-backend", "description": "A Backstage backend plugin that integrates towards Rollbar", - "version": "0.1.45-next.1", + "version": "0.1.48-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index 6bacf806bc..f6da79d81b 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,58 @@ # @backstage/plugin-rollbar +## 0.4.24-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/theme@0.4.1 + +## 0.4.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + +## 0.4.23-next.0 + +### Patch Changes + +- 482bb5c0bbf8: Moved `@types/react` to be a regular dependency +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + - @backstage/plugin-catalog-react@1.8.3-next.0 + +## 0.4.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-model@1.4.1 + - @backstage/theme@0.4.1 + +## 0.4.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 0.4.21-next.0 ### Patch Changes diff --git a/plugins/rollbar/api-report.md b/plugins/rollbar/api-report.md index 5ee8a50ce3..45c672e7bb 100644 --- a/plugins/rollbar/api-report.md +++ b/plugins/rollbar/api-report.md @@ -10,13 +10,15 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { IdentityApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const EntityPageRollbar: () => JSX.Element; +export const EntityPageRollbar: () => React_2.JSX.Element; // @public (undocumented) -export const EntityRollbarContent: () => JSX.Element; +export const EntityRollbarContent: () => JSX_2.Element; // @public (undocumented) const isPluginApplicableToEntity: (entity: Entity) => boolean; @@ -238,5 +240,5 @@ export type RollbarTopActiveItem = { }; // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; ``` diff --git a/plugins/rollbar/catalog-info.yaml b/plugins/rollbar/catalog-info.yaml new file mode 100644 index 0000000000..bc3f88019b --- /dev/null +++ b/plugins/rollbar/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-rollbar + title: '@backstage/plugin-rollbar' + description: A Backstage plugin that integrates towards Rollbar +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: andrewthauer diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 8c17895e48..e6183240d8 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar", "description": "A Backstage plugin that integrates towards Rollbar", - "version": "0.4.21-next.0", + "version": "0.4.24-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,6 +23,7 @@ "backstage", "rollbar" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -41,6 +42,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "lodash": "^4.17.21", "react-sparklines": "^1.7.0", "react-use": "^17.2.4" @@ -60,8 +62,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", - "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "msw": "^1.0.0" }, diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md index ea3e24c720..c8216e84fe 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md @@ -1,5 +1,65 @@ # @backstage/plugin-scaffolder-backend-module-confluence-to-markdown +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/plugin-scaffolder-node@0.2.3-next.2 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## 0.2.1 + +### Patch Changes + +- 0a9b3b14e904: Added example for the `confluence:transform:markdown` that will show in the installed actions list +- 12a8c94eda8d: Add package repository and homepage metadata +- 0d347efc8f18: Use `fetchContents` directly instead of a `fetchPlainAction` +- c186c631b429: Import helpers from the node package instead of the backend package +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-scaffolder-node@0.2.0 + - @backstage/integration@1.6.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.2.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.6-next.2 + - @backstage/backend-common@0.19.2-next.2 + ## 0.2.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/catalog-info.yaml b/plugins/scaffolder-backend-module-confluence-to-markdown/catalog-info.yaml new file mode 100644 index 0000000000..8daba65491 --- /dev/null +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-scaffolder-backend-module-confluence-to-markdown + title: '@backstage/plugin-scaffolder-backend-module-confluence-to-markdown' + description: The confluence-to-markdown module for @backstage/plugin-scaffolder-backend +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json index 0d342d1717..55edfabbd5 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown", "description": "The confluence-to-markdown module for @backstage/plugin-scaffolder-backend", - "version": "0.2.1-next.1", + "version": "0.2.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.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 new file mode 100644 index 0000000000..e5aeadbf00 --- /dev/null +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts @@ -0,0 +1,162 @@ +/* + * 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 { PassThrough } from 'stream'; +import { createConfluenceToMarkdownAction } from './confluenceToMarkdown'; +import { getVoidLogger } from '@backstage/backend-common'; +import { UrlReader } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import mockFs from 'mock-fs'; +import os from 'os'; +import { readFile, writeFile, createWriteStream } from 'fs-extra'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { examples } from './confluenceToMarkdown.examples'; +import yaml from 'yaml'; +import { ActionContext } from '@backstage/plugin-scaffolder-node'; + +jest.mock('fs-extra', () => ({ + mkdirSync: jest.fn(), + readFile: jest.fn().mockResolvedValue('File contents'), + writeFile: jest.fn().mockImplementation(() => { + return Promise.resolve(); + }), + outputFile: jest.fn(), + openSync: jest.fn(), + createWriteStream: jest.fn().mockReturnValue(new PassThrough()), + ensureDir: jest.fn(), +})); + +describe('confluence:transform:markdown examples', () => { + const baseUrl = `https://confluence.example.com`; + const worker = setupServer(); + setupRequestMockHandlers(worker); + + const config = new ConfigReader({ + confluence: { + baseUrl: baseUrl, + auth: { + token: 'fake_token', + }, + }, + }); + + const integrations = ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + github: [{ host: 'github.com', token: 'token' }], + }, + }), + ); + + let reader: UrlReader; + let mockContext: ActionContext<{ + confluenceUrls: string[]; + repoUrl: string; + }>; + + const logger = getVoidLogger(); + jest.spyOn(logger, 'info'); + + const mockTmpDir = os.tmpdir(); + + beforeEach(() => { + reader = { + readUrl: jest.fn(), + readTree: jest.fn().mockResolvedValue({ + dir: jest.fn(), + }), + search: jest.fn(), + }; + mockContext = { + input: yaml.parse(examples[0].example).steps[0].input, + workspacePath: '/tmp', + logger, + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), + }; + mockFs({ [`${mockTmpDir}/src/docs`]: {} }); + }); + afterEach(() => { + jest.clearAllMocks(); + mockFs.restore(); + }); + + it('should call confluence to markdown action successfully with results array', async () => { + const options = { + reader, + integrations, + config, + }; + const responseBody = { + results: [ + { + id: '4444444', + type: 'page', + title: 'Testing', + body: { + export_view: { + value: '

hello world

', + }, + }, + }, + ], + }; + const responseBodyTwo = { + results: [ + { + id: '4444444', + type: 'attachment', + title: 'testing.pdf', + metadata: { + mediaType: 'application/pdf', + }, + _links: { + download: '/download/attachments/4444444/testing.pdf', + }, + }, + ], + }; + + worker.use( + rest.get(`${baseUrl}/rest/api/content`, (_, res, ctx) => + res(ctx.status(200, 'OK'), ctx.json(responseBody)), + ), + rest.get( + `${baseUrl}/rest/api/content/4444444/child/attachment`, + (_, res, ctx) => res(ctx.status(200, 'OK'), ctx.json(responseBodyTwo)), + ), + rest.get( + `${baseUrl}/download/attachments/4444444/testing.pdf`, + (_, res, ctx) => res(ctx.status(200, 'OK'), ctx.body('hello')), + ), + ); + + const action = createConfluenceToMarkdownAction(options); + + await action.handler(mockContext); + + expect(logger.info).toHaveBeenCalledWith( + `Fetching the mkdocs.yml catalog from https://github.com/organization-name/repo-name/blob/main/mkdocs.yml`, + ); + expect(logger.info).toHaveBeenCalledTimes(5); + expect(createWriteStream).toHaveBeenCalledTimes(1); + expect(readFile).toHaveBeenCalledTimes(1); + expect(writeFile).toHaveBeenCalledTimes(1); + }); +}); diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.ts new file mode 100644 index 0000000000..e5d10644a6 --- /dev/null +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.ts @@ -0,0 +1,41 @@ +/* + * 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: + 'Downloads content from provided Confluence URLs and converts it to Markdown.', + example: yaml.stringify({ + steps: [ + { + action: 'confluence:transform:markdown', + id: 'confluence-transform-markdown', + name: 'Transform Confluence content to Markdown', + input: { + confluenceUrls: [ + 'https://confluence.example.com/display/SPACEKEY/Page+Title', + ], + repoUrl: + 'https://github.com/organization-name/repo-name/blob/main/mkdocs.yml', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.ts index 71a3af11ae..e88f42e39c 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.ts @@ -33,6 +33,7 @@ import { createConfluenceVariables, getConfluenceConfig, } from './helpers'; +import { examples } from './confluenceToMarkdown.examples'; /** * @public @@ -53,6 +54,8 @@ export const createConfluenceToMarkdownAction = (options: { repoUrl: string; }>({ id: 'confluence:transform:markdown', + description: 'Transforms Confluence content to Markdown', + examples, schema: { input: { properties: { diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index f6d1e6bd01..61a582692d 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,63 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.2.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/plugin-scaffolder-node@0.2.3-next.2 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.2.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.2.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## 0.2.24 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- c186c631b429: Import helpers from the node package instead of the backend package +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-scaffolder-node@0.2.0 + - @backstage/integration@1.6.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.2.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.6-next.2 + - @backstage/backend-common@0.19.2-next.2 + ## 0.2.24-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/catalog-info.yaml b/plugins/scaffolder-backend-module-cookiecutter/catalog-info.yaml new file mode 100644 index 0000000000..18926d3925 --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-scaffolder-backend-module-cookiecutter + title: '@backstage/plugin-scaffolder-backend-module-cookiecutter' + description: >- + A module for the scaffolder backend that lets you template projects using + cookiecutter +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index df1d81bbd3..7812a27b7b 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", - "version": "0.2.24-next.1", + "version": "0.2.27-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md index 4c50ef46fd..ae227974ce 100644 --- a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-scaffolder-backend-module-gitlab +## 0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/integration@1.7.0-next.2 + - @backstage/plugin-scaffolder-node@0.2.3-next.2 + - @backstage/errors@1.2.1 + +## 0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.1 + - @backstage/errors@1.2.1 + +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.2.0 + - @backstage/integration@1.6.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## 0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.6-next.2 + ## 0.2.3-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitlab/catalog-info.yaml b/plugins/scaffolder-backend-module-gitlab/catalog-info.yaml new file mode 100644 index 0000000000..c8af1e7fa5 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-scaffolder-backend-module-gitlab + title: '@backstage/plugin-scaffolder-backend-module-gitlab' +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index 8c9ba33a77..5931054df3 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.2.3-next.1", + "version": "0.2.6-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index fa3f9201ac..17063054a8 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,63 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.4.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/plugin-scaffolder-node@0.2.3-next.2 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.4.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.4.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## 0.4.17 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- c186c631b429: Import helpers from the node package instead of the backend package +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-scaffolder-node@0.2.0 + - @backstage/integration@1.6.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.4.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.6-next.2 + - @backstage/backend-common@0.19.2-next.2 + ## 0.4.17-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/catalog-info.yaml b/plugins/scaffolder-backend-module-rails/catalog-info.yaml new file mode 100644 index 0000000000..989b88219e --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-scaffolder-backend-module-rails + title: '@backstage/plugin-scaffolder-backend-module-rails' + description: >- + A module for the scaffolder backend that lets you template projects using + Rails +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index c514354955..83d93d4681 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", "description": "A module for the scaffolder backend that lets you template projects using Rails", - "version": "0.4.17-next.1", + "version": "0.4.20-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md index fbeed9e5e3..9ab42214d5 100644 --- a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/plugin-scaffolder-backend-module-sentry +## 0.1.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.2 + - @backstage/errors@1.2.1 + +## 0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-scaffolder-node@0.2.3-next.1 + - @backstage/errors@1.2.1 + +## 0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## 0.1.8 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.2.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + +## 0.1.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.6-next.2 + ## 0.1.8-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-sentry/catalog-info.yaml b/plugins/scaffolder-backend-module-sentry/catalog-info.yaml new file mode 100644 index 0000000000..75b72fa661 --- /dev/null +++ b/plugins/scaffolder-backend-module-sentry/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-scaffolder-backend-module-sentry + title: '@backstage/plugin-scaffolder-backend-module-sentry' +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index 5b58d1ef87..0b747f2000 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.8-next.1", + "version": "0.1.11-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index fcf915a348..a0f424edc2 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.2.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.2 + - @backstage/types@1.1.0 + +## 0.2.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/plugin-scaffolder-node@0.2.3-next.1 + - @backstage/types@1.1.0 + +## 0.2.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## 0.2.21 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.2.0 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + +## 0.2.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.6-next.2 + ## 0.2.21-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/catalog-info.yaml b/plugins/scaffolder-backend-module-yeoman/catalog-info.yaml new file mode 100644 index 0000000000..28d6030832 --- /dev/null +++ b/plugins/scaffolder-backend-module-yeoman/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-scaffolder-backend-module-yeoman + title: '@backstage/plugin-scaffolder-backend-module-yeoman' +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index 6e90170dab..ff366f9088 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.2.21-next.1", + "version": "0.2.24-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index dc8140c996..0995a4b047 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,146 @@ # @backstage/plugin-scaffolder-backend +## 1.16.6-next.2 + +### Patch Changes + +- ded27b83ead2: Add examples for `publish:bitbucket` scaffolder actions. +- 814feeed7343: Update to handle invalid luxon values +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.2 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-scaffolder-common@1.4.1-next.1 + +## 1.16.6-next.1 + +### Patch Changes + +- f3c0b95e3ef1: Add examples for `github:actions:dispatch` scaffolder actions. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.13.0-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/plugin-scaffolder-node@0.2.3-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/plugin-scaffolder-common@1.4.1-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.16.3-next.0 + +### Patch Changes + +- a4989552d828: Add examples for `publish:github` and `publish:gitlab` scaffolder actions. +- 5f1a92b9f19f: Use `DefaultAzureDevOpsCredentialsProvider` to retrieve credentials for Azure DevOps. +- fb57a4694fc6: Fixed the plugin and module ID of the alpha `catalogModuleTemplateKind` export. +- cfc3ca6ce060: Changes needed to support MySQL +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.2-next.0 + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/plugin-scaffolder-node@0.2.2-next.0 + +## 1.16.0 + +### Minor Changes + +- e514aac3eac0: Introduce `each` property on action steps, allowing them to be ran repeatedly. + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 349611126ae2: Removed the options from the alpha `scaffolderPlugin` export. To extend the scaffolder plugin you instead now use the available extension points, `scaffolderActionsExtensionPoint`, `scaffolderTaskBrokerExtensionPoint`, and `scaffolderTemplatingExtensionPoint`. +- 33c76caef72a: Added examples for the `fs:delete` and `fs:rename` actions +- e07a4914f621: Deprecated the following type exports, which have been moved to `@backstage/plugin-scaffolder-node` instead: + + - `TemplateFilter` + - `TemplateGlobal` + - `TaskStatus` + - `TaskCompletionState` + - `SerializedTask` + - `TaskEventType` + - `SerializedTaskEvent` + - `TaskBrokerDispatchResult` + - `TaskBrokerDispatchOptions` + - `TaskContext` + - `TaskBroker` + +- 0b1d775be05b: Adds examples to a few scaffolder actions. +- 88bc6e27a213: The `concurrentTasksLimit` option can now be configured via static configuration as well. Setting it to 0 will now also disable the task worker. +- 0f873325068d: Add examples for `github:repo:create` and `github:repo:push` scaffolder actions. +- 5c28ebc79fd6: Updated dependency `esbuild` to `^0.19.0`. +- d3b31a791eb1: Deprecated `executeShellCommand`, `RunCommandOptions`, and `fetchContents` from `@backstage/plugin-scaffolder-backend`, since they are useful for Scaffolder modules (who should not be importing from the plugin package itself). You should now import these from `@backstage/plugin-scaffolder-backend-node` instead. `RunCommandOptions` was renamed in the Node package as `ExecuteShellCommandOptions`, for consistency. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/plugin-catalog-backend@1.12.0 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-scaffolder-node@0.2.0 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/integration@1.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + +## 1.15.2-next.2 + +### Patch Changes + +- 33c76caef72a: Added examples for the fs:delete and fs:rename actions +- 0b1d775be05b: Adds examples to a few scaffolder actions. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.12.0-next.2 + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/plugin-scaffolder-node@0.1.6-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + ## 1.15.2-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend/alpha-api-report.md b/plugins/scaffolder-backend/alpha-api-report.md index 363fcafd17..20def133ad 100644 --- a/plugins/scaffolder-backend/alpha-api-report.md +++ b/plugins/scaffolder-backend/alpha-api-report.md @@ -12,11 +12,7 @@ import { PermissionCriteria } from '@backstage/plugin-permission-common'; import { PermissionRule } from '@backstage/plugin-permission-node'; import { PermissionRuleParams } from '@backstage/plugin-permission-common'; import { ResourcePermission } from '@backstage/plugin-permission-common'; -import { TaskBroker } from '@backstage/plugin-scaffolder-backend'; -import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { TemplateEntityStepV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { TemplateFilter } from '@backstage/plugin-scaffolder-backend'; -import { TemplateGlobal } from '@backstage/plugin-scaffolder-backend'; import { TemplateParametersV1beta3 } from '@backstage/plugin-scaffolder-common'; // @alpha @@ -90,18 +86,8 @@ export const scaffolderActionConditions: Conditions<{ }>; // @alpha -export const scaffolderPlugin: ( - options?: ScaffolderPluginOptions | undefined, -) => BackendFeature; - -// @alpha -export type ScaffolderPluginOptions = { - actions?: TemplateAction[]; - taskWorkers?: number; - taskBroker?: TaskBroker; - additionalTemplateFilters?: Record; - additionalTemplateGlobals?: Record; -}; +const scaffolderPlugin: () => BackendFeature; +export default scaffolderPlugin; // @alpha export const scaffolderTemplateConditions: Conditions<{ diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 4fe961056f..7fe4964827 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -19,11 +19,9 @@ import { GithubCredentialsProvider } from '@backstage/integration'; import { HumanDuration } from '@backstage/types'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { JsonObject } from '@backstage/types'; -import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; import { LocationSpec } from '@backstage/plugin-catalog-common'; import { Logger } from 'winston'; -import { Observable } from '@backstage/types'; import { Octokit } from 'octokit'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { PermissionRule } from '@backstage/plugin-permission-node'; @@ -35,12 +33,23 @@ import { RESOURCE_TYPE_SCAFFOLDER_TEMPLATE } from '@backstage/plugin-scaffolder- import { Schema } from 'jsonschema'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmIntegrations } from '@backstage/integration'; +import { SerializedTask as SerializedTask_2 } from '@backstage/plugin-scaffolder-node'; +import { SerializedTaskEvent as SerializedTaskEvent_2 } from '@backstage/plugin-scaffolder-node'; +import { TaskBroker as TaskBroker_2 } from '@backstage/plugin-scaffolder-node'; +import { TaskBrokerDispatchOptions as TaskBrokerDispatchOptions_2 } from '@backstage/plugin-scaffolder-node'; +import { TaskBrokerDispatchResult as TaskBrokerDispatchResult_2 } from '@backstage/plugin-scaffolder-node'; +import { TaskCompletionState as TaskCompletionState_2 } from '@backstage/plugin-scaffolder-node'; +import { TaskContext as TaskContext_2 } from '@backstage/plugin-scaffolder-node'; +import { TaskEventType as TaskEventType_2 } from '@backstage/plugin-scaffolder-node'; import { TaskSecrets as TaskSecrets_2 } from '@backstage/plugin-scaffolder-node'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TaskSpecV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { TaskStatus as TaskStatus_2 } from '@backstage/plugin-scaffolder-node'; import { TemplateAction as TemplateAction_2 } from '@backstage/plugin-scaffolder-node'; import { TemplateActionOptions } from '@backstage/plugin-scaffolder-node'; import { TemplateEntityStepV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { TemplateFilter as TemplateFilter_2 } from '@backstage/plugin-scaffolder-node'; +import { TemplateGlobal as TemplateGlobal_2 } from '@backstage/plugin-scaffolder-node'; import { TemplateParametersV1beta3 } from '@backstage/plugin-scaffolder-common'; import { UrlReader } from '@backstage/backend-common'; import { ZodType } from 'zod'; @@ -886,89 +895,29 @@ export class ScaffolderEntitiesProcessor implements CatalogProcessor { validateEntityKind(entity: Entity): Promise; } -// @public -export type SerializedTask = { - id: string; - spec: TaskSpec; - status: TaskStatus; - createdAt: string; - lastHeartbeatAt?: string; - createdBy?: string; - secrets?: TaskSecrets_2; -}; +// @public @deprecated +export type SerializedTask = SerializedTask_2; -// @public -export type SerializedTaskEvent = { - id: number; - taskId: string; - body: JsonObject; - type: TaskEventType; - createdAt: string; -}; +// @public @deprecated +export type SerializedTaskEvent = SerializedTaskEvent_2; -// @public -export interface TaskBroker { - // (undocumented) - cancel?(taskId: string): Promise; - // (undocumented) - claim(): Promise; - // (undocumented) - dispatch( - options: TaskBrokerDispatchOptions, - ): Promise; - // (undocumented) - event$(options: { taskId: string; after: number | undefined }): Observable<{ - events: SerializedTaskEvent[]; - }>; - // (undocumented) - get(taskId: string): Promise; - // (undocumented) - list?(options?: { createdBy?: string }): Promise<{ - tasks: SerializedTask[]; - }>; - // (undocumented) - vacuumTasks(options: { timeoutS: number }): Promise; -} +// @public @deprecated +export type TaskBroker = TaskBroker_2; -// @public -export type TaskBrokerDispatchOptions = { - spec: TaskSpec; - secrets?: TaskSecrets_2; - createdBy?: string; -}; +// @public @deprecated +export type TaskBrokerDispatchOptions = TaskBrokerDispatchOptions_2; -// @public -export type TaskBrokerDispatchResult = { - taskId: string; -}; +// @public @deprecated +export type TaskBrokerDispatchResult = TaskBrokerDispatchResult_2; -// @public -export type TaskCompletionState = 'failed' | 'completed'; +// @public @deprecated +export type TaskCompletionState = TaskCompletionState_2; -// @public -export interface TaskContext { - // (undocumented) - cancelSignal: AbortSignal; - // (undocumented) - complete(result: TaskCompletionState, metadata?: JsonObject): Promise; - // (undocumented) - createdBy?: string; - // (undocumented) - done: boolean; - // (undocumented) - emitLog(message: string, logMetadata?: JsonObject): Promise; - // (undocumented) - getWorkspaceName(): Promise; - // (undocumented) - isDryRun?: boolean; - // (undocumented) - secrets?: TaskSecrets_2; - // (undocumented) - spec: TaskSpec; -} +// @public @deprecated +export type TaskContext = TaskContext_2; -// @public -export type TaskEventType = 'completion' | 'log' | 'cancelled'; +// @public @deprecated +export type TaskEventType = TaskEventType_2; // @public export class TaskManager implements TaskContext { @@ -1000,13 +949,8 @@ export class TaskManager implements TaskContext { // @public @deprecated (undocumented) export type TaskSecrets = TaskSecrets_2; -// @public -export type TaskStatus = - | 'cancelled' - | 'completed' - | 'failed' - | 'open' - | 'processing'; +// @public @deprecated +export type TaskStatus = TaskStatus_2; // @public export interface TaskStore { @@ -1103,13 +1047,11 @@ export class TemplateActionRegistry { register(action: TemplateAction_2): void; } -// @public (undocumented) -export type TemplateFilter = (...args: JsonValue[]) => JsonValue | undefined; +// @public @deprecated (undocumented) +export type TemplateFilter = TemplateFilter_2; -// @public (undocumented) -export type TemplateGlobal = - | ((...args: JsonValue[]) => JsonValue | undefined) - | JsonValue; +// @public @deprecated (undocumented) +export type TemplateGlobal = TemplateGlobal_2; // @public (undocumented) export type TemplatePermissionRuleInput< diff --git a/plugins/scaffolder-backend/catalog-info.yaml b/plugins/scaffolder-backend/catalog-info.yaml new file mode 100644 index 0000000000..5bb93a2483 --- /dev/null +++ b/plugins/scaffolder-backend/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-scaffolder-backend + title: '@backstage/plugin-scaffolder-backend' + description: The Backstage backend plugin that helps you create new things +spec: + lifecycle: production + type: backstage-backend-plugin + owner: maintainers diff --git a/plugins/scaffolder-backend/config.d.ts b/plugins/scaffolder-backend/config.d.ts index 7883284165..60ccd52257 100644 --- a/plugins/scaffolder-backend/config.d.ts +++ b/plugins/scaffolder-backend/config.d.ts @@ -28,5 +28,14 @@ export interface Config { * The commit message used when new components are created. */ defaultCommitMessage?: string; + + /** + * Sets the number of concurrent tasks that can be run at any given time on the TaskWorker. + * + * Defaults to 10. + * + * Set to 0 to disable task workers altogether. + */ + concurrentTasksLimit?: number; }; } diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index d1481e2068..0399684144 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend", "description": "The Backstage backend plugin that helps you create new things", - "version": "1.15.2-next.1", + "version": "1.16.6-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -110,7 +110,7 @@ "@types/nunjucks": "^3.1.4", "@types/supertest": "^2.0.8", "@types/zen-observable": "^0.8.0", - "esbuild": "^0.18.0", + "esbuild": "^0.19.0", "jest-when": "^3.1.0", "mock-fs": "^5.1.0", "msw": "^1.0.0", diff --git a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts index f361bfcdf9..98bd4a1d66 100644 --- a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts +++ b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts @@ -23,120 +23,104 @@ import { ScmIntegrations } from '@backstage/integration'; import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; import { scaffolderActionsExtensionPoint, - ScaffolderActionsExtensionPoint, + scaffolderTaskBrokerExtensionPoint, + scaffolderTemplatingExtensionPoint, + TaskBroker, TemplateAction, -} from '@backstage/plugin-scaffolder-node'; -import { TemplateFilter, TemplateGlobal, - TaskBroker, -} from '@backstage/plugin-scaffolder-backend'; +} from '@backstage/plugin-scaffolder-node'; import { createBuiltinActions } from './scaffolder'; import { createRouter } from './service/router'; /** - * Catalog plugin options + * Scaffolder plugin * * @alpha */ -export type ScaffolderPluginOptions = { - actions?: TemplateAction[]; - taskWorkers?: number; - taskBroker?: TaskBroker; - additionalTemplateFilters?: Record; - additionalTemplateGlobals?: Record; -}; +export const scaffolderPlugin = createBackendPlugin({ + pluginId: 'scaffolder', + register(env) { + const addedActions = new Array>(); + env.registerExtensionPoint(scaffolderActionsExtensionPoint, { + addActions(...newActions: TemplateAction[]) { + addedActions.push(...newActions); + }, + }); -class ScaffolderActionsExtensionPointImpl - implements ScaffolderActionsExtensionPoint -{ - #actions = new Array>(); + let taskBroker: TaskBroker | undefined; + env.registerExtensionPoint(scaffolderTaskBrokerExtensionPoint, { + setTaskBroker(newTaskBroker) { + if (taskBroker) { + throw new Error('Task broker may only be set once'); + } + taskBroker = newTaskBroker; + }, + }); - addActions(...actions: TemplateAction[]): void { - this.#actions.push(...actions); - } + const additionalTemplateFilters: Record = {}; + const additionalTemplateGlobals: Record = {}; + env.registerExtensionPoint(scaffolderTemplatingExtensionPoint, { + addTemplateFilters(newFilters) { + Object.assign(additionalTemplateFilters, newFilters); + }, + addTemplateGlobals(newGlobals) { + Object.assign(additionalTemplateGlobals, newGlobals); + }, + }); - get actions() { - return this.#actions; - } -} + env.registerInit({ + deps: { + logger: coreServices.logger, + config: coreServices.rootConfig, + reader: coreServices.urlReader, + permissions: coreServices.permissions, + database: coreServices.database, + httpRouter: coreServices.httpRouter, + catalogClient: catalogServiceRef, + }, + async init({ + logger, + config, + reader, + database, + httpRouter, + catalogClient, + permissions, + }) { + const log = loggerToWinstonLogger(logger); -/** - * Catalog plugin - * - * @alpha - */ -export const scaffolderPlugin = createBackendPlugin( - (options?: ScaffolderPluginOptions) => ({ - pluginId: 'scaffolder', - register(env) { - const actionsExtensions = new ScaffolderActionsExtensionPointImpl(); - - env.registerExtensionPoint( - scaffolderActionsExtensionPoint, - actionsExtensions, - ); - - env.registerInit({ - deps: { - logger: coreServices.logger, - config: coreServices.rootConfig, - reader: coreServices.urlReader, - permissions: coreServices.permissions, - database: coreServices.database, - httpRouter: coreServices.httpRouter, - catalogClient: catalogServiceRef, - }, - async init({ - logger, - config, - reader, - database, - httpRouter, - catalogClient, - permissions, - }) { - const { - additionalTemplateFilters, - taskBroker, - taskWorkers, - additionalTemplateGlobals, - } = options ?? {}; - const log = loggerToWinstonLogger(logger); - - const actions = options?.actions || [ - ...actionsExtensions.actions, - ...createBuiltinActions({ - integrations: ScmIntegrations.fromConfig(config), - catalogClient, - reader, - config, - additionalTemplateFilters, - additionalTemplateGlobals, - }), - ]; - - const actionIds = actions.map(action => action.id).join(', '); - log.info( - `Starting scaffolder with the following actions enabled ${actionIds}`, - ); - - const router = await createRouter({ - logger: log, - config, - database, + const actions = [ + ...addedActions, + ...createBuiltinActions({ + integrations: ScmIntegrations.fromConfig(config), catalogClient, reader, - actions, - taskBroker, - taskWorkers, + config, additionalTemplateFilters, additionalTemplateGlobals, - permissions, - }); - httpRouter.use(router); - }, - }); - }, - }), -); + }), + ]; + + const actionIds = actions.map(action => action.id).join(', '); + log.info( + `Starting scaffolder with the following actions enabled ${actionIds}`, + ); + + const router = await createRouter({ + logger: log, + config, + database, + catalogClient, + reader, + actions, + taskBroker, + additionalTemplateFilters, + additionalTemplateGlobals, + permissions, + }); + httpRouter.use(router); + }, + }); + }, +}); diff --git a/plugins/scaffolder-backend/src/alpha.ts b/plugins/scaffolder-backend/src/alpha.ts index 713f2569aa..07228fab88 100644 --- a/plugins/scaffolder-backend/src/alpha.ts +++ b/plugins/scaffolder-backend/src/alpha.ts @@ -16,5 +16,4 @@ export * from './modules'; export * from './service'; -export { scaffolderPlugin } from './ScaffolderPlugin'; -export type { ScaffolderPluginOptions } from './ScaffolderPlugin'; +export { scaffolderPlugin as default } from './ScaffolderPlugin'; diff --git a/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts b/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts index 488604e981..701863c7f6 100644 --- a/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts +++ b/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts @@ -16,6 +16,10 @@ import { Isolate } from 'isolated-vm'; import { resolvePackagePath } from '@backstage/backend-common'; +import { + TemplateFilter as _TemplateFilter, + TemplateGlobal as _TemplateGlobal, +} from '@backstage/plugin-scaffolder-node'; import fs from 'fs-extra'; import { JsonValue } from '@backstage/types'; @@ -86,13 +90,17 @@ const { render, renderCompat } = (() => { })(); `; -/** @public */ -export type TemplateFilter = (...args: JsonValue[]) => JsonValue | undefined; +/** + * @public + * @deprecated Import from `@backstage/plugin-scaffolder-node` instead. + */ +export type TemplateFilter = _TemplateFilter; -/** @public */ -export type TemplateGlobal = - | ((...args: JsonValue[]) => JsonValue | undefined) - | JsonValue; +/** + * @public + * @deprecated Import from `@backstage/plugin-scaffolder-node` instead. + */ +export type TemplateGlobal = _TemplateGlobal; export interface SecureTemplaterOptions { /* Enables jinja compatibility and the "jsonify" filter */ diff --git a/plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.ts b/plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.ts index 3359bd1dba..81d3dc3e93 100644 --- a/plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.ts +++ b/plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.ts @@ -24,8 +24,8 @@ import { ScaffolderEntitiesProcessor } from '../processor'; * @alpha */ export const catalogModuleTemplateKind = createBackendModule({ - moduleId: 'scaffolder', - pluginId: 'templateKind', + moduleId: 'templateKind', + pluginId: 'catalog', register(env) { env.registerInit({ deps: { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts new file mode 100644 index 0000000000..10994e3824 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts @@ -0,0 +1,79 @@ +/* + * 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 { createFilesystemDeleteAction } from './delete'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PassThrough } from 'stream'; +import { resolve as resolvePath } from 'path'; +import * as os from 'os'; +import mockFs from 'mock-fs'; +import fs from 'fs-extra'; +import yaml from 'yaml'; +import { examples } from './delete.examples'; + +const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; +const workspacePath = resolvePath(root, 'my-workspace'); + +describe('fs:delete examples', () => { + const action = createFilesystemDeleteAction(); + + const files: string[] = yaml.parse(examples[0].example).steps[0].input.files; + + const mockContext = { + input: { + files: files, + }, + workspacePath, + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.restoreAllMocks(); + + mockFs({ + [workspacePath]: { + [files[0]]: 'hello', + [files[1]]: 'world', + 'a-folder': { + 'unit-test-in-a-folder.js2': 'content', + }, + }, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should call fs.rm with the correct values', async () => { + files.forEach(file => { + const filePath = resolvePath(workspacePath, file); + const fileExists = fs.existsSync(filePath); + expect(fileExists).toBe(true); + }); + + await action.handler(mockContext); + + files.forEach(file => { + const filePath = resolvePath(workspacePath, file); + const fileExists = fs.existsSync(filePath); + expect(fileExists).toBe(false); + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.ts new file mode 100644 index 0000000000..8aea076b9d --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.ts @@ -0,0 +1,36 @@ +/* + * 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 * as yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Delete specified files', + example: yaml.stringify({ + steps: [ + { + action: 'fs:delete', + id: 'deleteFiles', + name: 'Delete files', + input: { + files: ['file1.txt', 'file2.txt'], + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts index 2f955c16ff..5787d245ab 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts @@ -18,6 +18,7 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { InputError } from '@backstage/errors'; import { resolveSafeChildPath } from '@backstage/backend-common'; import fs from 'fs-extra'; +import { examples } from './delete.examples'; /** * Creates new action that enables deletion of files and directories in the workspace. @@ -27,6 +28,7 @@ export const createFilesystemDeleteAction = () => { return createTemplateAction<{ files: string[] }>({ id: 'fs:delete', description: 'Deletes files and directories from the workspace', + examples, schema: { input: { required: ['files'], diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts new file mode 100644 index 0000000000..c4c27cc68e --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts @@ -0,0 +1,104 @@ +/* + * 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 * as os from 'os'; +import mockFs from 'mock-fs'; +import { resolve as resolvePath } from 'path'; +import { createFilesystemRenameAction } from './rename'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PassThrough } from 'stream'; +import fs from 'fs-extra'; +import yaml from 'yaml'; +import { examples } from './rename.examples'; + +const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; +const workspacePath = resolvePath(root, 'my-workspace'); + +describe('fs:rename examples', () => { + const action = createFilesystemRenameAction(); + const files: { from: string; to: string }[] = yaml.parse(examples[0].example) + .steps[0].input.files; + + const mockContext = { + input: { + files: files, + }, + workspacePath, + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.restoreAllMocks(); + + mockFs({ + [workspacePath]: { + [files[0].from]: 'hello', + [files[1].from]: 'world', + [files[2].from]: '!!!', + 'file3Renamed.txt': 'I will be overwritten :(', + 'a-folder': { + 'file.md': 'content', + }, + }, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should call fs.move with the correct values', async () => { + mockContext.input.files.forEach(file => { + const filePath = resolvePath(workspacePath, file.from); + const fileExists = fs.existsSync(filePath); + expect(fileExists).toBe(true); + }); + + await action.handler(mockContext); + + mockContext.input.files.forEach(file => { + const filePath = resolvePath(workspacePath, file.from); + const fileExists = fs.existsSync(filePath); + expect(fileExists).toBe(false); + }); + }); + + it('should override when requested', async () => { + const sourceFile = files[2].from; + const destFile = files[2].to; + const sourceFilePath = resolvePath(workspacePath, sourceFile); + const destFilePath = resolvePath(workspacePath, destFile); + + const sourceBeforeContent = fs.readFileSync(sourceFilePath, 'utf-8'); + const destBeforeContent = fs.readFileSync(destFilePath, 'utf-8'); + + expect(sourceBeforeContent).not.toEqual(destBeforeContent); + + await action.handler({ + ...mockContext, + input: { + files, + }, + }); + + const destAfterContent = fs.readFileSync(destFilePath, 'utf-8'); + + expect(sourceBeforeContent).toEqual(destAfterContent); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.ts new file mode 100644 index 0000000000..59d7483a10 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.ts @@ -0,0 +1,40 @@ +/* + * 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 * as yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Rename specified files ', + example: yaml.stringify({ + steps: [ + { + action: 'fs:rename', + id: 'renameFiles', + name: 'Rename files', + input: { + files: [ + { from: 'file1.txt', to: 'file1Renamed.txt' }, + { from: 'file2.txt', to: 'file2Renamed.txt' }, + { from: 'file3.txt', to: 'file3Renamed.txt', overwrite: true }, + ], + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts index c29ba221c4..5d9d666d15 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts @@ -18,6 +18,7 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { resolveSafeChildPath } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; import fs from 'fs-extra'; +import { examples } from './rename.examples'; /** * Creates a new action that allows renames of files and directories in the workspace. @@ -33,6 +34,7 @@ export const createFilesystemRenameAction = () => { }>({ id: 'fs:rename', description: 'Renames files and directories within the workspace', + examples, schema: { input: { required: ['files'], diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.examples.test.ts new file mode 100644 index 0000000000..fe04142a98 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.examples.test.ts @@ -0,0 +1,133 @@ +/* + * 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 { + ScmIntegrations, + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, +} from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '@backstage/backend-common'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { PassThrough } from 'stream'; +import { createGithubActionsDispatchAction } from './githubActionsDispatch'; +import yaml from 'yaml'; +import { examples } from './githubActionsDispatch.examples'; + +const mockOctokit = { + rest: { + actions: { + createWorkflowDispatch: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +describe('github:actions:dispatch', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + let githubCredentialsProvider: GithubCredentialsProvider; + let action: TemplateAction; + + const mockContext = { + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + workflowId: 'a-workflow-id', + branchOrTagName: 'main', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + action = createGithubActionsDispatchAction({ + integrations, + githubCredentialsProvider, + }); + }); + + it('should call the githubApis for creating WorkflowDispatch without an input object', async () => { + mockOctokit.rest.actions.createWorkflowDispatch.mockResolvedValue({ + data: { + foo: 'bar', + }, + }); + + const repoUrl = 'github.com?repo=repo&owner=owner'; + const workflowId = 'dispatch_workflow'; + const branchOrTagName = 'main'; + const ctx = Object.assign({}, mockContext, { + input: { repoUrl, workflowId, branchOrTagName }, + }); + await action.handler(ctx); + + expect( + mockOctokit.rest.actions.createWorkflowDispatch, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + workflow_id: workflowId, + ref: branchOrTagName, + }); + }); + + it('should call the githubApis for creating WorkflowDispatch with an input object', async () => { + mockOctokit.rest.actions.createWorkflowDispatch.mockResolvedValue({ + data: { + foo: 'bar', + }, + }); + + const repoUrl = 'github.com?repo=repo&owner=owner'; + const workflowId = 'dispatch_workflow'; + const branchOrTagName = 'main'; + const workflowInputs = yaml.parse(examples[1].example).steps[0].input + .workflowInputs; + const ctx = Object.assign({}, mockContext, { + input: { repoUrl, workflowId, branchOrTagName, workflowInputs }, + }); + await action.handler(ctx); + expect( + mockOctokit.rest.actions.createWorkflowDispatch, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + workflow_id: workflowId, + ref: branchOrTagName, + inputs: workflowInputs, + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.examples.ts new file mode 100644 index 0000000000..33ad86dd89 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.examples.ts @@ -0,0 +1,74 @@ +/* + * 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: 'GitHub Action Workflow Without Inputs.', + example: yaml.stringify({ + steps: [ + { + action: 'github:actions:dispatch', + name: 'Dispatch Github Action Workflow', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + workflowId: 'WORKFLOW_ID', + branchOrTagName: 'main', + }, + }, + ], + }), + }, + { + description: 'GitHub Action Workflow With Inputs', + example: yaml.stringify({ + steps: [ + { + action: 'github:actions:dispatch', + name: 'Dispatch Github Action Workflow with inputs', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + workflowId: 'WORKFLOW_ID', + branchOrTagName: 'main', + workflowInputs: { + input1: 'value1', + input2: 'value2', + }, + }, + }, + ], + }), + }, + { + description: 'GitHub Action Workflow With Custom Token', + example: yaml.stringify({ + steps: [ + { + action: 'github:actions:dispatch', + name: 'Dispatch GitHub Action Workflow (custom token)', + input: { + repoUrl: 'github.com?repo=reponame&owner=owner', + workflowId: 'WORKFLOW_ID', + branchOrTagName: 'release-1.0', + token: '${{ secrets.MY_CUSTOM_TOKEN }}', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts index cd1593ee9f..6623ee1f27 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts @@ -23,6 +23,7 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Octokit } from 'octokit'; import { parseRepoUrl } from '../publish/util'; import { getOctokitOptions } from './helpers'; +import { examples } from './githubActionsDispatch.examples'; /** * Creates a new action that dispatches a GitHub Action workflow for a given branch or tag. @@ -44,6 +45,7 @@ export function createGithubActionsDispatchAction(options: { id: 'github:actions:dispatch', description: 'Dispatches a GitHub Action workflow for a given branch or tag', + examples, schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.examples.test.ts new file mode 100644 index 0000000000..71bbc7c1c6 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.examples.test.ts @@ -0,0 +1,192 @@ +/* + * 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 { TemplateAction } from '@backstage/plugin-scaffolder-node'; + +jest.mock('../helpers'); + +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { PassThrough } from 'stream'; +import { createGithubRepoCreateAction } from './githubRepoCreate'; +import { entityRefToName } from '../helpers'; +import yaml from 'yaml'; +import { examples } from './githubRepoCreate.examples'; + +const publicKey = '2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvvcCU='; + +const mockOctokit = { + rest: { + users: { + getByUsername: jest.fn(), + }, + repos: { + addCollaborator: jest.fn(), + createInOrg: jest.fn(), + createForAuthenticatedUser: jest.fn(), + replaceAllTopics: jest.fn(), + }, + teams: { + addOrUpdateRepoPermissionsInOrg: jest.fn(), + getByName: jest.fn(), + }, + actions: { + createRepoVariable: jest.fn(), + createOrUpdateRepoSecret: jest.fn(), + getRepoPublicKey: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +describe('github:repo:create examples', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + let githubCredentialsProvider: GithubCredentialsProvider; + let action: TemplateAction; + + const mockContext = { + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + action = createGithubRepoCreateAction({ + integrations, + githubCredentialsProvider, + }); + (entityRefToName as jest.Mock).mockImplementation((s: string) => s); + mockOctokit.rest.actions.getRepoPublicKey.mockResolvedValue({ + data: { + key: publicKey, + key_id: 'keyid', + }, + }); + }); + + afterEach(jest.resetAllMocks); + + it('should call the githubApis with the correct values for createInOrg', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[0].example).steps[0].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', + }); + }); + + it('should call the githubApis with a description for createInOrg', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[1].example).steps[0].input, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + description: 'My new repository', + 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', + }); + }); + + it('should call the githubApis with wiki and issues disabled for createInOrg', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[2].example).steps[0].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, // disable issues + has_wiki: false, // disable wiki + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.examples.ts new file mode 100644 index 0000000000..a58da1ee29 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.examples.ts @@ -0,0 +1,66 @@ +/* + * 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: 'Creates a GitHub repository with default configuration.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + }, + }, + ], + }), + }, + { + description: 'Add a description.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with a description', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'My new repository', + }, + }, + ], + }), + }, + { + description: 'Disable wiki and issues.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository without wiki and issues', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + hasIssues: false, + hasWiki: false, + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts index 0ec1c83910..e0fde3b193 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts @@ -28,6 +28,7 @@ import { } from './helpers'; import * as inputProps from './inputProperties'; import * as outputProps from './outputProperties'; +import { examples } from './githubRepoCreate.examples'; /** * Creates a new action that initializes a git repository @@ -96,6 +97,7 @@ export function createGithubRepoCreateAction(options: { }>({ id: 'github:repo:create', description: 'Creates a GitHub repository.', + examples, schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.examples.test.ts new file mode 100644 index 0000000000..84d489272d --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.examples.test.ts @@ -0,0 +1,131 @@ +/* + * 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 { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { PassThrough } from 'stream'; +import { initRepoAndPush } from '../helpers'; +import { createGithubRepoPushAction } from './githubRepoPush'; +import { examples } from './githubRepoPush.examples'; +import yaml from 'yaml'; + +const mockGit = { + init: jest.fn(), + add: jest.fn(), + checkout: jest.fn(), + commit: jest + .fn() + .mockResolvedValue('220f19cc36b551763d157f1b5e4a4b446165dbd6'), + fetch: jest.fn(), + addRemote: jest.fn(), + push: jest.fn(), +}; + +jest.mock('@backstage/backend-common', () => ({ + Git: { + fromAuth() { + return mockGit; + }, + }, + getVoidLogger: jest.requireActual('@backstage/backend-common').getVoidLogger, +})); + +jest.mock('../helpers'); + +const initRepoAndPushMocked = initRepoAndPush as jest.Mock< + Promise<{ commitHash: string }> +>; + +const mockOctokit = { + rest: { + repos: { + get: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +describe('github:repo:push examples', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + let githubCredentialsProvider: GithubCredentialsProvider; + let action: TemplateAction; + + const mockContext = { + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + + initRepoAndPushMocked.mockResolvedValue({ commitHash: 'test123' }); + + githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + action = createGithubRepoPushAction({ + integrations, + config, + githubCredentialsProvider, + }); + }); + + it('should call initRepoAndPush with the correct values', 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, + input: yaml.parse(examples[0].example).steps[0].input, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://github.com/clone/url.git', + defaultBranch: 'master', + auth: { username: 'x-access-token', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.examples.ts new file mode 100644 index 0000000000..5d3ba1e9d8 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.examples.ts @@ -0,0 +1,65 @@ +/* + * 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: 'Setup repo with no modifications to branch protection rules', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:push', + name: 'Create test repo with testuser as owner.', + input: { + repoUrl: 'github.com?repo=test&owner=testuser', + }, + }, + ], + }), + }, + { + description: 'Setup repo with required codeowners check', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:push', + name: 'Require codeowner branch protection rule', + input: { + repoUrl: 'github.com?repo=reponame&owner=owner', + requireCodeOwnerReviews: true, + }, + }, + ], + }), + }, + { + description: 'Change the default required number of approvals', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:push', + name: 'Require two approvals before merging', + input: { + repoUrl: 'github.com?repo=reponame&owner=owner', + requiredApprovingReviewCount: 2, + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts index fb15535453..e30eedad02 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts @@ -26,6 +26,7 @@ import { parseRepoUrl } from '../publish/util'; import { getOctokitOptions, initRepoPushAndProtect } from './helpers'; import * as inputProps from './inputProperties'; import * as outputProps from './outputProperties'; +import { examples } from './githubRepoPush.examples'; /** * Creates a new action that initializes a git repository of the content in the workspace @@ -76,6 +77,7 @@ export function createGithubRepoPushAction(options: { id: 'github:repo:push', description: 'Initializes a git repository of contents in workspace and publishes it to GitHub.', + examples, schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts index 9f68eb848a..9b7673ea29 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts @@ -42,7 +42,10 @@ describe('publish:azure', () => { const config = new ConfigReader({ integrations: { azure: [ - { host: 'dev.azure.com', token: 'tokenlols' }, + { + host: 'dev.azure.com', + credentials: [{ personalAccessToken: 'tokenlols' }], + }, { host: 'myazurehostnotoken.com' }, ], }, @@ -115,7 +118,9 @@ describe('publish:azure', () => { 'myazurehostnotoken.com?repo=bob&owner=owner&organization=org', }, }), - ).rejects.toThrow(/No token provided for Azure Integration/); + ).rejects.toThrow( + /No credentials provided https:\/\/myazurehostnotoken.com\/org, please check your integrations config/, + ); }); it('should throw when no repo is returned', async () => { @@ -260,7 +265,10 @@ describe('publish:azure', () => { const customAuthorConfig = new ConfigReader({ integrations: { azure: [ - { host: 'dev.azure.com', token: 'tokenlols' }, + { + host: 'dev.azure.com', + credentials: [{ personalAccessToken: 'tokenlols' }], + }, { host: 'myazurehostnotoken.com' }, ], }, @@ -301,7 +309,10 @@ describe('publish:azure', () => { const customAuthorConfig = new ConfigReader({ integrations: { azure: [ - { host: 'dev.azure.com', token: 'tokenlols' }, + { + host: 'dev.azure.com', + credentials: [{ personalAccessToken: 'tokenlols' }], + }, { host: 'myazurehostnotoken.com' }, ], }, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts index 96b7c87204..b6c0226163 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts @@ -15,10 +15,17 @@ */ import { InputError } from '@backstage/errors'; -import { ScmIntegrationRegistry } from '@backstage/integration'; +import { + DefaultAzureDevOpsCredentialsProvider, + ScmIntegrationRegistry, +} from '@backstage/integration'; import { initRepoAndPush } from '../helpers'; import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api'; +import { + getBearerHandler, + getPersonalAccessTokenHandler, + WebApi, +} from 'azure-devops-node-api'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Config } from '@backstage/config'; @@ -135,22 +142,23 @@ export function createPublishAzureAction(options: { ); } - const integrationConfig = integrations.azure.byHost(host); + const url = `https://${host}/${organization}`; + const credentialProvider = + DefaultAzureDevOpsCredentialsProvider.fromIntegrations(integrations); + const credentials = await credentialProvider.getCredentials({ url: url }); - if (!integrationConfig) { + if (credentials === undefined && ctx.input.token === undefined) { throw new InputError( - `No matching integration configuration for host ${host}, please check your integrations config`, + `No credentials provided ${url}, please check your integrations config`, ); } - if (!integrationConfig.config.token && !ctx.input.token) { - throw new InputError(`No token provided for Azure Integration ${host}`); - } + const authHandler = + ctx.input.token || credentials?.type === 'pat' + ? getPersonalAccessTokenHandler(ctx.input.token ?? credentials!.token) + : getBearerHandler(credentials!.token); - const token = ctx.input.token ?? integrationConfig.config.token!; - const authHandler = getPersonalAccessTokenHandler(token); - - const webApi = new WebApi(`https://${host}/${organization}`, authHandler); + const webApi = new WebApi(url, authHandler); const client = await webApi.getGitApi(); const createOptions: GitRepositoryCreateOptions = { name: repo }; const returnedRepo = await client.createRepository(createOptions, owner); @@ -187,14 +195,19 @@ export function createPublishAzureAction(options: { : config.getOptionalString('scaffolder.defaultAuthor.email'), }; + const auth = + ctx.input.token || credentials?.type === 'pat' + ? { + username: 'notempty', + password: ctx.input.token ?? credentials!.token, + } + : { token: credentials!.token }; + const commitResult = await initRepoAndPush({ dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), remoteUrl, defaultBranch, - auth: { - username: 'notempty', - password: token, - }, + auth: auth, logger: ctx.logger, commitMessage: gitCommitMessage ? gitCommitMessage diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.examples.test.ts new file mode 100644 index 0000000000..2514c44637 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.examples.test.ts @@ -0,0 +1,369 @@ +/* + * 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. + */ + +jest.mock('../helpers', () => { + return { + initRepoAndPush: jest.fn().mockResolvedValue({ + commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', + }), + commitAndPushRepo: jest.fn().mockResolvedValue({ + commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', + }), + }; +}); + +import { createPublishBitbucketAction } from './bitbucket'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PassThrough } from 'stream'; +import { initRepoAndPush } from '../helpers'; +import yaml from 'yaml'; +import { examples } from './bitbucket.examples'; + +describe('publish:bitbucket', () => { + const config = new ConfigReader({ + integrations: { + bitbucket: [ + { + host: 'bitbucket.org', + token: 'tokenlols', + }, + { + host: 'hosted.bitbucket.com', + token: 'thing', + apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0', + }, + { + host: 'notoken.bitbucket.com', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createPublishBitbucketAction({ integrations, config }); + const mockContext = { + input: { + repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', + repoVisibility: 'private' as const, + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + const server = setupServer(); + setupRequestMockHandlers(server); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should call initAndPush with the correct values', async () => { + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[0].example).steps[0].input, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.org/workspace/cloneurl', + defaultBranch: 'master', + auth: { username: 'x-token-auth', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); + + it('should call initAndPush with the correct default branch', async () => { + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[3].example).steps[0].input, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.org/workspace/cloneurl', + defaultBranch: 'main', + auth: { username: 'x-token-auth', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); + + it('should call initAndPush with the specified source path', async () => { + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[4].example).steps[0].input, + }); + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: `${mockContext.workspacePath}/repoRoot`, + remoteUrl: 'https://bitbucket.org/workspace/cloneurl', + auth: { username: 'x-token-auth', password: 'tokenlols' }, + logger: mockContext.logger, + defaultBranch: 'main', + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); + + it('should call initAndPush with the authentication token', async () => { + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[6].example).steps[0].input, + }); + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.org/workspace/cloneurl', + auth: { username: 'x-token-auth', password: 'your-auth-token' }, + logger: mockContext.logger, + defaultBranch: 'main', + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); + + it('should call initAndPush with the custom commit message', async () => { + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[7].example).steps[0].input, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.org/workspace/cloneurl', + auth: { username: 'x-token-auth', password: 'your-auth-token' }, + logger: mockContext.logger, + defaultBranch: 'main', + commitMessage: 'Initial commit with custom message', + gitAuthorInfo: { email: undefined, name: undefined }, + }); + }); + + it('should call initAndPush with the custom author name and email for the commit.', async () => { + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[8].example).steps[0].input, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.org/workspace/cloneurl', + auth: { username: 'x-token-auth', password: 'your-auth-token' }, + logger: mockContext.logger, + defaultBranch: 'main', + commitMessage: 'Initial commit with custom message', + gitAuthorInfo: { email: 'your.email@example.com', name: 'Your Name' }, + }); + }); + + describe('LFS for hosted bitbucket', () => { + const repoCreationResponse = { + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }; + + it('should call the correct APIs to enable LFS if requested and the host is hosted bitbucket', async () => { + expect.assertions(1); + server.use( + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (_, res, ctx) => { + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json(repoCreationResponse), + ); + }, + ), + 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'); + return res(ctx.status(204)); + }, + ), + ); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[5].example).steps[0].input, + }); + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.examples.ts new file mode 100644 index 0000000000..6667afcc9e --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.examples.ts @@ -0,0 +1,196 @@ +/* + * 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: + 'Initializes a git repository of contents in workspace and publish it to Bitbucket with default configuration.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:bitbucket', + name: 'Publish to Bitbucket', + input: { + repoUrl: + 'bitbucket.org?repo=repo&workspace=workspace&project=project', + }, + }, + ], + }), + }, + { + description: 'Add a description.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:bitbucket', + name: 'Publish to Bitbucket', + input: { + repoUrl: + 'bitbucket.org?repo=repo&workspace=workspace&project=project', + description: 'Initialize a git repository', + }, + }, + ], + }), + }, + { + description: 'Change visibility of the repository.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:bitbucket', + name: 'Publish to Bitbucket', + input: { + repoUrl: + 'bitbucket.org?repo=repo&workspace=workspace&project=project', + description: 'Initialize a git repository', + repoVisibility: 'public', + }, + }, + ], + }), + }, + { + description: 'Set the default branch.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:bitbucket', + name: 'Publish to Bitbucket', + input: { + repoUrl: + 'bitbucket.org?repo=repo&workspace=workspace&project=project', + description: 'Initialize a git repository', + repoVisibility: 'public', + defaultBranch: 'main', + }, + }, + ], + }), + }, + { + description: 'Specify a source path within the workspace.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:bitbucket', + name: 'Publish to Bitbucket', + input: { + repoUrl: + 'bitbucket.org?repo=repo&workspace=workspace&project=project', + description: 'Initialize a git repository', + repoVisibility: 'public', + defaultBranch: 'main', + sourcePath: './repoRoot', + }, + }, + ], + }), + }, + { + description: 'Enable LFS for the repository.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:bitbucket', + name: 'Publish to Bitbucket', + input: { + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + description: 'Initialize a git repository', + repoVisibility: 'public', + defaultBranch: 'main', + sourcePath: './repoRoot', + enableLFS: true, + }, + }, + ], + }), + }, + { + description: 'Provide an authentication token for Bitbucket.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:bitbucket', + name: 'Publish to Bitbucket', + input: { + repoUrl: + 'bitbucket.org?repo=repo&workspace=workspace&project=project', + description: 'Initialize a git repository', + repoVisibility: 'public', + defaultBranch: 'main', + token: 'your-auth-token', + }, + }, + ], + }), + }, + { + description: 'Set a custom commit message.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:bitbucket', + name: 'Publish to Bitbucket', + input: { + repoUrl: + 'bitbucket.org?repo=repo&workspace=workspace&project=project', + description: 'Initialize a git repository', + repoVisibility: 'public', + defaultBranch: 'main', + token: 'your-auth-token', + gitCommitMessage: 'Initial commit with custom message', + }, + }, + ], + }), + }, + { + description: 'Set a custom author name and email for the commit.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:bitbucket', + name: 'Publish to Bitbucket', + input: { + repoUrl: + 'bitbucket.org?repo=repo&workspace=workspace&project=project', + description: 'Initialize a git repository', + repoVisibility: 'public', + defaultBranch: 'main', + token: 'your-auth-token', + gitCommitMessage: 'Initial commit with custom message', + gitAuthorName: 'Your Name', + gitAuthorEmail: 'your.email@example.com', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts index 6a7341bdc9..a4d664e0e7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -24,6 +24,7 @@ import fetch, { Response, RequestInit } from 'node-fetch'; import { initRepoAndPush } from '../helpers'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { Config } from '@backstage/config'; +import { examples } from './bitbucket.examples'; const createBitbucketCloudRepository = async (opts: { workspace: string; @@ -221,6 +222,7 @@ export function createPublishBitbucketAction(options: { id: 'publish:bitbucket', description: 'Initializes a git repository of the content in the workspace, and publishes it to Bitbucket.', + examples, schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.examples.test.ts new file mode 100644 index 0000000000..4d87ec97c4 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.examples.test.ts @@ -0,0 +1,151 @@ +/* + * 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. + */ +jest.mock('../helpers'); + +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { PassThrough } from 'stream'; +import { entityRefToName, initRepoAndPush } from '../helpers'; +import { createPublishGithubAction } from './github'; +import { examples } from './github.examples'; +import yaml from 'yaml'; + +const publicKey = '2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvvcCU='; + +const initRepoAndPushMocked = initRepoAndPush as jest.Mock< + Promise<{ commitHash: string }> +>; + +const mockOctokit = { + rest: { + users: { + getByUsername: jest.fn(), + }, + repos: { + addCollaborator: jest.fn(), + createInOrg: jest.fn(), + createForAuthenticatedUser: jest.fn(), + replaceAllTopics: jest.fn(), + }, + teams: { + getByName: jest.fn(), + addOrUpdateRepoPermissionsInOrg: jest.fn(), + }, + actions: { + createRepoVariable: jest.fn(), + createOrUpdateRepoSecret: jest.fn(), + getRepoPublicKey: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +describe('publish:github', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const { entityRefToName: realFamiliarizeEntityName } = + jest.requireActual('../helpers'); + const integrations = ScmIntegrations.fromConfig(config); + let githubCredentialsProvider: GithubCredentialsProvider; + let action: TemplateAction; + + const mockContext = { + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'description', + repoVisibility: 'private' as const, + access: 'owner/blam', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + initRepoAndPushMocked.mockResolvedValue({ + commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', + }); + githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + action = createPublishGithubAction({ + integrations, + config, + githubCredentialsProvider, + }); + + // restore real implmentation + (entityRefToName as jest.Mock).mockImplementation( + realFamiliarizeEntityName, + ); + mockOctokit.rest.actions.getRepoPublicKey.mockResolvedValue({ + data: { + key: publicKey, + key_id: 'keyid', + }, + }); + }); + + afterEach(jest.resetAllMocks); + + it('should call initRepoAndPush with the correct values', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[0].example).steps[0].input, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://github.com/clone/url.git', + defaultBranch: 'master', + auth: { username: 'x-access-token', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.examples.ts new file mode 100644 index 0000000000..65f2ccdaa3 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.examples.ts @@ -0,0 +1,70 @@ +/* + * 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: + 'Initializes a git repository of contents in workspace and publish it to GitHub with default configuration.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:github', + name: 'Publish to GitHub', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + }, + }, + ], + }), + }, + { + description: 'Add a description.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:github', + name: 'Publish to GitHub', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'Initialize a git repository', + }, + }, + ], + }), + }, + { + description: 'Change visibility of the repository.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:github', + name: 'Publish to GitHub', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'Initialize a git repository', + repoVisibility: 'public', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index 9336c38b63..e5b6071863 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -30,6 +30,7 @@ import { import * as inputProps from '../github/inputProperties'; import * as outputProps from '../github/outputProperties'; import { parseRepoUrl } from './util'; +import { examples } from './github.examples'; /** * Creates a new action that initializes a git repository of the content in the workspace @@ -111,6 +112,7 @@ export function createPublishGithubAction(options: { id: 'publish:github', description: 'Initializes a git repository of contents in workspace and publishes it to GitHub.', + examples, schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.examples.test.ts new file mode 100644 index 0000000000..5d58431c61 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.examples.test.ts @@ -0,0 +1,115 @@ +/* + * 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 yaml from 'yaml'; + +jest.mock('../helpers', () => { + return { + initRepoAndPush: jest.fn().mockResolvedValue({ + commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', + }), + commitAndPushRepo: jest.fn().mockResolvedValue({ + commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6', + }), + }; +}); + +import { createPublishGitlabAction } from './gitlab'; +import { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PassThrough } from 'stream'; +import { initRepoAndPush } from '../helpers'; +import { examples } from './gitlab.examples'; + +const mockGitlabClient = { + Namespaces: { + show: jest.fn(), + }, + Projects: { + create: jest.fn(), + }, + Users: { + current: jest.fn(), + }, + ProjectMembers: { + add: jest.fn(), + }, +}; +jest.mock('@gitbeaker/node', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('publish:gitlab', () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'tokenlols', + apiBaseUrl: 'https://api.gitlab.com', + }, + { + host: 'hosted.gitlab.com', + apiBaseUrl: 'https://api.hosted.gitlab.com', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createPublishGitlabAction({ integrations, config }); + const mockContext = { + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should call initRepoAndPush with the correct values', async () => { + mockGitlabClient.Users.current.mockResolvedValue({ id: 12345 }); + mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Projects.create.mockResolvedValue({ + http_url_to_repo: 'http://mockurl.git', + }); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[0].example).steps[0].input, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + defaultBranch: 'master', + remoteUrl: 'http://mockurl.git', + auth: { username: 'oauth2', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.examples.ts new file mode 100644 index 0000000000..ac259866e3 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.examples.ts @@ -0,0 +1,71 @@ +/* + * 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: + 'Initializes a git repository of the content in the workspace, and publishes it to GitLab.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:gitlab', + name: 'Publish to GitLab', + input: { + repoUrl: 'gitlab.com?repo=project_name&owner=group_name', + }, + }, + ], + }), + }, + { + description: 'Add a description.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:gitlab', + name: 'Publish to GitLab', + input: { + repoUrl: 'gitlab.com?repo=project_name&owner=group_name', + description: 'Initialize a git repository', + }, + }, + ], + }), + }, + { + description: + 'Sets the commit message on the repository. The default value is `initial commit`.', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:gitlab', + name: 'Publish to GitLab', + input: { + repoUrl: 'gitlab.com?repo=project_name&owner=group_name', + description: 'Initialize a git repository', + gitCommitMessage: 'Started a project.', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts index 4045fd6958..dbab7b11b8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts @@ -21,6 +21,7 @@ import { Gitlab } from '@gitbeaker/node'; import { initRepoAndPush } from '../helpers'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { Config } from '@backstage/config'; +import { examples } from './gitlab.examples'; /** * Creates a new action that initializes a git repository of the content in the workspace @@ -49,6 +50,7 @@ export function createPublishGitlabAction(options: { id: 'publish:gitlab', description: 'Initializes a git repository of the content in the workspace, and publishes it to GitLab.', + examples, schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index f3cb9b645e..40ffdb17d4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -69,7 +69,7 @@ export type DatabaseTaskStoreOptions = { }; /** - * Typeguard to help DatabaseTaskStore understand when database is PluginDatabaseManager vs. when database is a Knex instance. + * Type guard to help DatabaseTaskStore understand when database is PluginDatabaseManager vs. when database is a Knex instance. * * * @public */ @@ -81,7 +81,13 @@ function isPluginDatabaseManager( const parseSqlDateToIsoString = (input: T): T | string => { if (typeof input === 'string') { - return DateTime.fromSQL(input, { zone: 'UTC' }).toISO(); + const parsed = DateTime.fromSQL(input, { zone: 'UTC' }); + if (!parsed.isValid) { + throw new Error( + `Failed to parse database timestamp '${input}', ${parsed.invalidReason}: ${parsed.invalidExplanation}`, + ); + } + return parsed.toISO()!; } return input; @@ -261,18 +267,21 @@ export class DatabaseTaskStore implements TaskStore { tasks: { taskId: string }[]; }> { const { timeoutS } = options; - + let heartbeatInterval = this.db.raw(`? - interval '${timeoutS} seconds'`, [ + this.db.fn.now(), + ]); + if (this.db.client.config.client.includes('mysql')) { + heartbeatInterval = this.db.raw( + `date_sub(now(), interval ${timeoutS} second)`, + ); + } else if (this.db.client.config.client.includes('sqlite3')) { + heartbeatInterval = this.db.raw(`datetime('now', ?)`, [ + `-${timeoutS} seconds`, + ]); + } const rawRows = await this.db('tasks') .where('status', 'processing') - .andWhere( - 'last_heartbeat_at', - '<=', - this.db.client.config.client.includes('sqlite3') - ? this.db.raw(`datetime('now', ?)`, [`-${timeoutS} seconds`]) - : this.db.raw(`? - interval '${timeoutS} seconds'`, [ - this.db.fn.now(), - ]), - ); + .andWhere('last_heartbeat_at', '<=', heartbeatInterval); const tasks = rawRows.map(row => ({ taskId: row.id, })); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 5d4f7b73cc..253a6c4388 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -573,6 +573,101 @@ describe('DefaultWorkflowRunner', () => { }); }); + describe('each', () => { + it('should run a step repeatedly - flat values', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + each: '${{parameters.colors}}', + action: 'jest-mock-action', + input: { color: '${{each.value}}' }, + }, + ], + output: {}, + parameters: { + colors: ['blue', 'green', 'red'], + }, + }); + + await runner.execute(task); + + expect(fakeActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ input: { color: 'blue' } }), + ); + expect(fakeActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ input: { color: 'green' } }), + ); + expect(fakeActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ input: { color: 'red' } }), + ); + }); + + it('should run a step repeatedly - object list', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + each: '${{parameters.settings}}', + action: 'jest-mock-action', + input: { + key: '${{each.key}}', + value: '${{each.value}}', + }, + }, + ], + output: {}, + parameters: { + settings: [{ color: 'blue' }], + }, + }); + + await runner.execute(task); + + expect(fakeActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ + input: { key: '0', value: { color: 'blue' } }, + }), + ); + }); + + it('should run a step repeatedly - object', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + each: '${{parameters.settings}}', + action: 'jest-mock-action', + input: { key: '${{each.key}}', value: '${{each.value}}' }, + }, + ], + output: {}, + parameters: { + settings: { color: 'blue', transparent: 'yes' }, + }, + }); + + await runner.execute(task); + + expect(fakeActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ + input: { key: 'color', value: 'blue' }, + }), + ); + expect(fakeActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ + input: { key: 'transparent', value: 'yes' }, + }), + ); + }); + }); + describe('secrets', () => { it('should pass through the secrets to the context', async () => { const task = createMockTaskWithSpec( diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index da690bcb82..1f93ded94c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -25,7 +25,7 @@ import * as winston from 'winston'; import fs from 'fs-extra'; import path from 'path'; import nunjucks from 'nunjucks'; -import { JsonObject, JsonValue } from '@backstage/types'; +import { JsonArray, JsonObject, JsonValue } from '@backstage/types'; import { InputError, NotAllowedError } from '@backstage/errors'; import { PassThrough } from 'stream'; import { generateExampleOutput, isTruthy } from './helper'; @@ -76,6 +76,7 @@ type TemplateContext = { entity?: UserEntity; ref?: string; }; + each?: JsonValue; }; const isValidTaskSpec = (taskSpec: TaskSpec): taskSpec is TaskSpecV1beta3 => { @@ -311,25 +312,63 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { const tmpDirs = new Array(); const stepOutput: { [outputName: string]: JsonValue } = {}; - await action.handler({ - input, - secrets: task.secrets ?? {}, - logger: taskLogger, - logStream: streamLogger, - workspacePath, - createTemporaryDirectory: async () => { - const tmpDir = await fs.mkdtemp(`${workspacePath}_step-${step.id}-`); - tmpDirs.push(tmpDir); - return tmpDir; - }, - output(name: string, value: JsonValue) { - stepOutput[name] = value; - }, - templateInfo: task.spec.templateInfo, - user: task.spec.user, - isDryRun: task.isDryRun, - signal: task.cancelSignal, - }); + const iterations = new Array(); + if (step.each) { + const each = await this.render(step.each, context, renderTemplate); + iterations.push( + ...Object.keys(each).map((key: any) => { + return { key: key, value: each[key] }; + }), + ); + } else { + iterations.push({}); + } + + let actionInput = input; + for (const iteration of iterations) { + if (step.each) { + taskLogger.info(`Running step each: ${iteration}`); + const iterationContext = { + ...context, + each: iteration, + }; + // re-render input with the modified context that includes each + actionInput = + (step.input && + this.render( + step.input, + { ...iterationContext, secrets: task.secrets ?? {} }, + renderTemplate, + )) ?? + {}; + } + await action.handler({ + input: actionInput, + secrets: task.secrets ?? {}, + logger: taskLogger, + logStream: streamLogger, + workspacePath, + createTemporaryDirectory: async () => { + const tmpDir = await fs.mkdtemp( + `${workspacePath}_step-${step.id}-`, + ); + tmpDirs.push(tmpDir); + return tmpDir; + }, + output(name: string, value: JsonValue) { + if (step.each) { + stepOutput[name] = stepOutput[name] || []; + (stepOutput[name] as JsonArray).push(value); + } else { + stepOutput[name] = value; + } + }, + templateInfo: task.spec.templateInfo, + user: task.spec.user, + isDryRun: task.isDryRun, + signal: task.cancelSignal, + }); + } // Remove all temporary directories that were created when executing the action for (const tmpDir of tmpDirs) { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 112276b9dd..4560f27c0b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -14,131 +14,94 @@ * limitations under the License. */ -import { JsonValue, JsonObject, Observable } from '@backstage/types'; +import { JsonValue, JsonObject } from '@backstage/types'; import { TaskSpec, TaskStep } from '@backstage/plugin-scaffolder-common'; import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; -import { TemplateAction } 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, +} from '@backstage/plugin-scaffolder-node'; /** * The status of each step of the Task * * @public + * @deprecated Import from `@backstage/plugin-scaffolder-node` instead. */ -export type TaskStatus = - | 'cancelled' - | 'completed' - | 'failed' - | 'open' - | 'processing'; +export type TaskStatus = _TaskStatus; /** * The state of a completed task. * * @public + * @deprecated Import from `@backstage/plugin-scaffolder-node` instead. */ -export type TaskCompletionState = 'failed' | 'completed'; +export type TaskCompletionState = _TaskCompletionState; /** * SerializedTask * * @public + * @deprecated Import from `@backstage/plugin-scaffolder-node` instead. */ -export type SerializedTask = { - id: string; - spec: TaskSpec; - status: TaskStatus; - createdAt: string; - lastHeartbeatAt?: string; - createdBy?: string; - secrets?: TaskSecrets; -}; +export type SerializedTask = _SerializedTask; /** * TaskEventType * * @public + * @deprecated Import from `@backstage/plugin-scaffolder-node` instead. */ -export type TaskEventType = 'completion' | 'log' | 'cancelled'; +export type TaskEventType = _TaskEventType; /** * SerializedTaskEvent * * @public + * @deprecated Import from `@backstage/plugin-scaffolder-node` instead. */ -export type SerializedTaskEvent = { - id: number; - taskId: string; - body: JsonObject; - type: TaskEventType; - createdAt: string; -}; +export type SerializedTaskEvent = _SerializedTaskEvent; /** - * The result of {@link TaskBroker.dispatch} + * The result of `TaskBroker.dispatch`. * * @public + * @deprecated Import from `@backstage/plugin-scaffolder-node` instead. */ -export type TaskBrokerDispatchResult = { - taskId: string; -}; +export type TaskBrokerDispatchResult = _TaskBrokerDispatchResult; /** - * The options passed to {@link TaskBroker.dispatch} + * The options passed to `TaskBroker.dispatch`. * Currently a spec and optional secrets * * @public + * @deprecated Import from `@backstage/plugin-scaffolder-node` instead. */ -export type TaskBrokerDispatchOptions = { - spec: TaskSpec; - secrets?: TaskSecrets; - createdBy?: string; -}; +export type TaskBrokerDispatchOptions = _TaskBrokerDispatchOptions; /** * Task * * @public + * @deprecated Import from `@backstage/plugin-scaffolder-node` instead. */ -export interface TaskContext { - cancelSignal: AbortSignal; - spec: TaskSpec; - secrets?: TaskSecrets; - createdBy?: string; - done: boolean; - isDryRun?: boolean; - - complete(result: TaskCompletionState, metadata?: JsonObject): Promise; - - emitLog(message: string, logMetadata?: JsonObject): Promise; - - getWorkspaceName(): Promise; -} +export type TaskContext = _TaskContext; /** * TaskBroker * * @public + * @deprecated Import from `@backstage/plugin-scaffolder-node` instead. */ -export interface TaskBroker { - cancel?(taskId: string): Promise; - - claim(): Promise; - - dispatch( - options: TaskBrokerDispatchOptions, - ): Promise; - - vacuumTasks(options: { timeoutS: number }): Promise; - - event$(options: { - taskId: string; - after: number | undefined; - }): Observable<{ events: SerializedTaskEvent[] }>; - - get(taskId: string): Promise; - - list?(options?: { createdBy?: string }): Promise<{ tasks: SerializedTask[] }>; -} +export type TaskBroker = _TaskBroker; /** * TaskStoreEmitOptions diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 1771067367..15f7c11eba 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -231,13 +231,15 @@ export async function createRouter( catalogClient, actions, taskWorkers, - concurrentTasksLimit, scheduler, additionalTemplateFilters, additionalTemplateGlobals, permissions, permissionRules, } = options; + const concurrentTasksLimit = + options.concurrentTasksLimit ?? + options.config.getOptionalNumber('scaffolder.concurrentTasksLimit'); const logger = parentLogger.child({ plugin: 'scaffolder' }); @@ -275,19 +277,21 @@ export async function createRouter( const actionRegistry = new TemplateActionRegistry(); const workers = []; - for (let i = 0; i < (taskWorkers || 1); i++) { - const worker = await TaskWorker.create({ - taskBroker, - actionRegistry, - integrations, - logger, - workingDirectory, - additionalTemplateFilters, - additionalTemplateGlobals, - concurrentTasksLimit, - permissions, - }); - workers.push(worker); + if (concurrentTasksLimit !== 0) { + for (let i = 0; i < (taskWorkers || 1); i++) { + const worker = await TaskWorker.create({ + taskBroker, + actionRegistry, + integrations, + logger, + workingDirectory, + additionalTemplateFilters, + additionalTemplateGlobals, + concurrentTasksLimit, + permissions, + }); + workers.push(worker); + } } const actionsToRegister = Array.isArray(actions) diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md index 6fd5948a5c..1478dd8ba9 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-scaffolder-common +## 1.4.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/types@1.1.0 + +## 1.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/types@1.1.0 + +## 1.4.0 + +### Minor Changes + +- e514aac3eac0: Introduce `each` property on action steps, allowing them to be ran repeatedly. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + ## 1.3.2 ### Patch Changes diff --git a/plugins/scaffolder-common/api-report.md b/plugins/scaffolder-common/api-report.md index 7c40780b57..6113c72659 100644 --- a/plugins/scaffolder-common/api-report.md +++ b/plugins/scaffolder-common/api-report.md @@ -5,6 +5,7 @@ ```ts import { Entity } from '@backstage/catalog-model'; import type { EntityMeta } from '@backstage/catalog-model'; +import type { JsonArray } from '@backstage/types'; import { JsonObject } from '@backstage/types'; import type { JsonValue } from '@backstage/types'; import { KindValidator } from '@backstage/catalog-model'; @@ -36,6 +37,7 @@ export interface TaskSpecV1beta3 { // @public export interface TaskStep { action: string; + each?: string | JsonArray; id: string; if?: string | boolean; input?: JsonObject; diff --git a/plugins/scaffolder-common/catalog-info.yaml b/plugins/scaffolder-common/catalog-info.yaml new file mode 100644 index 0000000000..1a8d2481f0 --- /dev/null +++ b/plugins/scaffolder-common/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-scaffolder-common + title: '@backstage/plugin-scaffolder-common' + description: >- + Common functionalities for the scaffolder, to be shared between scaffolder + and scaffolder-backend plugin +spec: + lifecycle: production + type: backstage-common-library + owner: maintainers diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index a0baa4470f..98d99ee303 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,15 +1,12 @@ { "name": "@backstage/plugin-scaffolder-common", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", - "version": "1.3.2", + "version": "1.4.1-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "module": "dist/index.esm.js", - "types": "dist/index.d.ts" + "access": "public" }, "exports": { ".": "./src/index.ts", @@ -41,6 +38,7 @@ "files": [ "dist" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/plugins/scaffolder-common/src/TaskSpec.ts b/plugins/scaffolder-common/src/TaskSpec.ts index 7c06fe2b99..6b144b4f78 100644 --- a/plugins/scaffolder-common/src/TaskSpec.ts +++ b/plugins/scaffolder-common/src/TaskSpec.ts @@ -15,7 +15,7 @@ */ import type { EntityMeta, UserEntity } from '@backstage/catalog-model'; -import type { JsonObject, JsonValue } from '@backstage/types'; +import type { JsonArray, JsonObject, JsonValue } from '@backstage/types'; /** * Information about a template that is stored on a task specification. @@ -70,6 +70,10 @@ export interface TaskStep { * When this is false, or if the templated value string evaluates to something that is falsy the step will be skipped. */ if?: string | boolean; + /** + * Run step repeatedly + */ + each?: string | JsonArray; } /** diff --git a/plugins/scaffolder-node/CHANGELOG.md b/plugins/scaffolder-node/CHANGELOG.md index fd1605c2df..eb1981df26 100644 --- a/plugins/scaffolder-node/CHANGELOG.md +++ b/plugins/scaffolder-node/CHANGELOG.md @@ -1,5 +1,75 @@ # @backstage/plugin-scaffolder-node +## 0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.5-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-common@1.4.1-next.1 + +## 0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.7.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-scaffolder-common@1.4.1-next.0 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-scaffolder-common@1.4.0 + +## 0.2.0 + +### Minor Changes + +- e514aac3eac0: Introduce `each` property on action steps, allowing them to be ran repeatedly. + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- e07a4914f621: Added several new types that were moved from `@backstage/plugin-scaffolder-backend`. +- 349611126ae2: Added two new alpha extension points, `scaffolderTaskBrokerExtensionPoint` and `scaffolderTemplatingExtensionPoint`. +- 0b1d775be05b: Export `TemplateExample` from the `createTemplateAction` type. +- d3b31a791eb1: Deprecated `executeShellCommand`, `RunCommandOptions`, and `fetchContents` from `@backstage/plugin-scaffolder-backend`, since they are useful for Scaffolder modules (who should not be importing from the plugin package itself). You should now import these from `@backstage/plugin-scaffolder-backend-node` instead. `RunCommandOptions` was renamed in the Node package as `ExecuteShellCommandOptions`, for consistency. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/integration@1.6.0 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 0.1.6-next.2 + +### Patch Changes + +- 0b1d775be05b: Export `TemplateExample` from the `createTemplateAction` type. +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + ## 0.1.6-next.1 ### Patch Changes diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 52e729fcb7..52ce3c8d13 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -7,10 +7,17 @@ import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { JsonObject } from '@backstage/types'; +import { JsonValue } from '@backstage/types'; import { Logger } from 'winston'; +import { Observable } from '@backstage/types'; import { Schema } from 'jsonschema'; import { ScmIntegrations } from '@backstage/integration'; import { SpawnOptionsWithoutStdio } from 'child_process'; +import { TaskBroker as TaskBroker_2 } from '@backstage/plugin-scaffolder-node'; +import { TaskSpec } from '@backstage/plugin-scaffolder-common'; +import { TemplateAction as TemplateAction_2 } from '@backstage/plugin-scaffolder-node'; +import { TemplateFilter as TemplateFilter_2 } from '@backstage/plugin-scaffolder-node'; +import { TemplateGlobal as TemplateGlobal_2 } from '@backstage/plugin-scaffolder-node'; import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; import { UrlReader } from '@backstage/backend-common'; import { UserEntity } from '@backstage/catalog-model'; @@ -39,6 +46,7 @@ export type ActionContext< ref?: string; }; signal?: AbortSignal; + each?: JsonObject; }; // @public @@ -104,17 +112,129 @@ export function fetchFile(options: { // @alpha export interface ScaffolderActionsExtensionPoint { // (undocumented) - addActions(...actions: TemplateAction[]): void; + addActions(...actions: TemplateAction_2[]): void; } // @alpha export const scaffolderActionsExtensionPoint: ExtensionPoint; +// @alpha +export interface ScaffolderTaskBrokerExtensionPoint { + // (undocumented) + setTaskBroker(taskBroker: TaskBroker_2): void; +} + +// @alpha +export const scaffolderTaskBrokerExtensionPoint: ExtensionPoint; + +// @alpha +export interface ScaffolderTemplatingExtensionPoint { + // (undocumented) + addTemplateFilters(filters: Record): void; + // (undocumented) + addTemplateGlobals(filters: Record): void; +} + +// @alpha +export const scaffolderTemplatingExtensionPoint: ExtensionPoint; + +// @public +export type SerializedTask = { + id: string; + spec: TaskSpec; + status: TaskStatus; + createdAt: string; + lastHeartbeatAt?: string; + createdBy?: string; + secrets?: TaskSecrets; +}; + +// @public +export type SerializedTaskEvent = { + id: number; + taskId: string; + body: JsonObject; + type: TaskEventType; + createdAt: string; +}; + +// @public +export interface TaskBroker { + // (undocumented) + cancel?(taskId: string): Promise; + // (undocumented) + claim(): Promise; + // (undocumented) + dispatch( + options: TaskBrokerDispatchOptions, + ): Promise; + // (undocumented) + event$(options: { taskId: string; after: number | undefined }): Observable<{ + events: SerializedTaskEvent[]; + }>; + // (undocumented) + get(taskId: string): Promise; + // (undocumented) + list?(options?: { createdBy?: string }): Promise<{ + tasks: SerializedTask[]; + }>; + // (undocumented) + vacuumTasks(options: { timeoutS: number }): Promise; +} + +// @public +export type TaskBrokerDispatchOptions = { + spec: TaskSpec; + secrets?: TaskSecrets; + createdBy?: string; +}; + +// @public +export type TaskBrokerDispatchResult = { + taskId: string; +}; + +// @public +export type TaskCompletionState = 'failed' | 'completed'; + +// @public +export interface TaskContext { + // (undocumented) + cancelSignal: AbortSignal; + // (undocumented) + complete(result: TaskCompletionState, metadata?: JsonObject): Promise; + // (undocumented) + createdBy?: string; + // (undocumented) + done: boolean; + // (undocumented) + emitLog(message: string, logMetadata?: JsonObject): Promise; + // (undocumented) + getWorkspaceName(): Promise; + // (undocumented) + isDryRun?: boolean; + // (undocumented) + secrets?: TaskSecrets; + // (undocumented) + spec: TaskSpec; +} + +// @public +export type TaskEventType = 'completion' | 'log' | 'cancelled'; + // @public export type TaskSecrets = Record & { backstageToken?: string; }; +// @public +export type TaskStatus = + | 'cancelled' + | 'completed' + | 'failed' + | 'open' + | 'processing'; + // @public (undocumented) export type TemplateAction< TActionInput extends JsonObject = JsonObject, @@ -157,4 +277,12 @@ export type TemplateExample = { description: string; example: string; }; + +// @public (undocumented) +export type TemplateFilter = (...args: JsonValue[]) => JsonValue | undefined; + +// @public (undocumented) +export type TemplateGlobal = + | ((...args: JsonValue[]) => JsonValue | undefined) + | JsonValue; ``` diff --git a/plugins/scaffolder-node/catalog-info.yaml b/plugins/scaffolder-node/catalog-info.yaml new file mode 100644 index 0000000000..726591cda5 --- /dev/null +++ b/plugins/scaffolder-node/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-scaffolder-node + title: '@backstage/plugin-scaffolder-node' + description: The plugin-scaffolder-node module for @backstage/plugin-scaffolder-backend +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index 555a25417d..37bf100c40 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-node", "description": "The plugin-scaffolder-node module for @backstage/plugin-scaffolder-backend", - "version": "0.1.6-next.1", + "version": "0.2.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-node/src/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts index e35f55aa67..7e8c2b4f5f 100644 --- a/plugins/scaffolder-node/src/actions/types.ts +++ b/plugins/scaffolder-node/src/actions/types.ts @@ -71,6 +71,11 @@ export type ActionContext< * Implement the signal to make your custom step abortable https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal */ signal?: AbortSignal; + + /** + * Optional value of each invocation + */ + each?: JsonObject; }; /** @public */ diff --git a/plugins/scaffolder-node/src/extensions.ts b/plugins/scaffolder-node/src/extensions.ts index 1ae51ce2e6..b85da5b590 100644 --- a/plugins/scaffolder-node/src/extensions.ts +++ b/plugins/scaffolder-node/src/extensions.ts @@ -15,7 +15,12 @@ */ import { createExtensionPoint } from '@backstage/backend-plugin-api'; -import { TemplateAction } from './actions'; +import { + TemplateAction, + TemplateFilter, + TemplateGlobal, + TaskBroker, +} from '@backstage/plugin-scaffolder-node'; /** * Extension point for managing scaffolder actions. @@ -35,3 +40,42 @@ export const scaffolderActionsExtensionPoint = createExtensionPoint({ id: 'scaffolder.actions', }); + +/** + * Extension point for replacing the scaffolder task broker. + * + * @alpha + */ +export interface ScaffolderTaskBrokerExtensionPoint { + setTaskBroker(taskBroker: TaskBroker): void; +} + +/** + * Extension point for replacing the scaffolder task broker. + * + * @alpha + */ +export const scaffolderTaskBrokerExtensionPoint = + createExtensionPoint({ + id: 'scaffolder.taskBroker', + }); + +/** + * Extension point for adding template filters and globals. + * + * @alpha + */ +export interface ScaffolderTemplatingExtensionPoint { + addTemplateFilters(filters: Record): void; + addTemplateGlobals(filters: Record): void; +} + +/** + * Extension point for adding template filters and globals. + * + * @alpha + */ +export const scaffolderTemplatingExtensionPoint = + createExtensionPoint({ + id: 'scaffolder.templating', + }); diff --git a/plugins/scaffolder-node/src/index.ts b/plugins/scaffolder-node/src/index.ts index 2a7c06234e..dcce065108 100644 --- a/plugins/scaffolder-node/src/index.ts +++ b/plugins/scaffolder-node/src/index.ts @@ -22,7 +22,12 @@ export * from './actions'; export * from './tasks'; +export type { TemplateFilter, TemplateGlobal } from './types'; export { scaffolderActionsExtensionPoint, type ScaffolderActionsExtensionPoint, + scaffolderTaskBrokerExtensionPoint, + type ScaffolderTaskBrokerExtensionPoint, + scaffolderTemplatingExtensionPoint, + type ScaffolderTemplatingExtensionPoint, } from './extensions'; diff --git a/plugins/scaffolder-node/src/tasks/index.ts b/plugins/scaffolder-node/src/tasks/index.ts index 0c1d0d813e..930de95237 100644 --- a/plugins/scaffolder-node/src/tasks/index.ts +++ b/plugins/scaffolder-node/src/tasks/index.ts @@ -14,4 +14,15 @@ * limitations under the License. */ -export { type TaskSecrets } from './types'; +export type { + TaskSecrets, + SerializedTask, + SerializedTaskEvent, + TaskBroker, + TaskBrokerDispatchOptions, + TaskBrokerDispatchResult, + TaskCompletionState, + TaskContext, + TaskEventType, + TaskStatus, +} from './types'; diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index c17b3cbde3..8c9cc29e31 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +import { TaskSpec } from '@backstage/plugin-scaffolder-common'; +import { JsonObject, Observable } from '@backstage/types'; + /** * TaskSecrets * @@ -22,3 +25,124 @@ export type TaskSecrets = Record & { backstageToken?: string; }; + +/** + * The status of each step of the Task + * + * @public + */ +export type TaskStatus = + | 'cancelled' + | 'completed' + | 'failed' + | 'open' + | 'processing'; + +/** + * The state of a completed task. + * + * @public + */ +export type TaskCompletionState = 'failed' | 'completed'; + +/** + * SerializedTask + * + * @public + */ +export type SerializedTask = { + id: string; + spec: TaskSpec; + status: TaskStatus; + createdAt: string; + lastHeartbeatAt?: string; + createdBy?: string; + secrets?: TaskSecrets; +}; + +/** + * TaskEventType + * + * @public + */ +export type TaskEventType = 'completion' | 'log' | 'cancelled'; + +/** + * SerializedTaskEvent + * + * @public + */ +export type SerializedTaskEvent = { + id: number; + taskId: string; + body: JsonObject; + type: TaskEventType; + createdAt: string; +}; + +/** + * The result of {@link TaskBroker.dispatch} + * + * @public + */ +export type TaskBrokerDispatchResult = { + taskId: string; +}; + +/** + * The options passed to {@link TaskBroker.dispatch} + * Currently a spec and optional secrets + * + * @public + */ +export type TaskBrokerDispatchOptions = { + spec: TaskSpec; + secrets?: TaskSecrets; + createdBy?: string; +}; + +/** + * Task + * + * @public + */ +export interface TaskContext { + cancelSignal: AbortSignal; + spec: TaskSpec; + secrets?: TaskSecrets; + createdBy?: string; + done: boolean; + isDryRun?: boolean; + + complete(result: TaskCompletionState, metadata?: JsonObject): Promise; + + emitLog(message: string, logMetadata?: JsonObject): Promise; + + getWorkspaceName(): Promise; +} + +/** + * TaskBroker + * + * @public + */ +export interface TaskBroker { + cancel?(taskId: string): Promise; + + claim(): Promise; + + dispatch( + options: TaskBrokerDispatchOptions, + ): Promise; + + vacuumTasks(options: { timeoutS: number }): Promise; + + event$(options: { + taskId: string; + after: number | undefined; + }): Observable<{ events: SerializedTaskEvent[] }>; + + get(taskId: string): Promise; + + list?(options?: { createdBy?: string }): Promise<{ tasks: SerializedTask[] }>; +} diff --git a/plugins/scaffolder-node/src/types.ts b/plugins/scaffolder-node/src/types.ts new file mode 100644 index 0000000000..bae106c84e --- /dev/null +++ b/plugins/scaffolder-node/src/types.ts @@ -0,0 +1,25 @@ +/* + * 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 { JsonValue } from '@backstage/types'; + +/** @public */ +export type TemplateFilter = (...args: JsonValue[]) => JsonValue | undefined; + +/** @public */ +export type TemplateGlobal = + | ((...args: JsonValue[]) => JsonValue | undefined) + | JsonValue; diff --git a/plugins/scaffolder-react/CHANGELOG.md b/plugins/scaffolder-react/CHANGELOG.md index e4762983c7..67a50d8b03 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,78 @@ # @backstage/plugin-scaffolder-react +## 1.5.5-next.2 + +### Patch Changes + +- 27fef07f9229: Updated dependency `use-immer` to `^0.9.0`. +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-scaffolder-common@1.4.1-next.1 + +## 1.5.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-scaffolder-common@1.4.1-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## 1.5.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + +## 1.5.2 + +### Patch Changes + +- ba9ee98a37bd: Fixed bug in Workflow component by passing a prop `templateName` down to Stepper component. +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + +## 1.5.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + ## 1.5.2-next.0 ### Patch Changes diff --git a/plugins/scaffolder-react/alpha-api-report.md b/plugins/scaffolder-react/alpha-api-report.md index 58a3c8e208..11bb2c10c4 100644 --- a/plugins/scaffolder-react/alpha-api-report.md +++ b/plugins/scaffolder-react/alpha-api-report.md @@ -44,10 +44,10 @@ export function createNextScaffolderFieldExtension< // @alpha export const DefaultTemplateOutputs: (props: { output?: ScaffolderTaskOutput; -}) => JSX.Element | null; +}) => React_2.JSX.Element | null; // @alpha (undocumented) -export const EmbeddableWorkflow: (props: WorkflowProps) => JSX.Element; +export const EmbeddableWorkflow: (props: WorkflowProps) => React_2.JSX.Element; // @alpha export const extractSchemaFromStep: (inputStep: JsonObject) => { @@ -56,7 +56,9 @@ export const extractSchemaFromStep: (inputStep: JsonObject) => { }; // @alpha -export const Form: (props: PropsWithChildren) => JSX.Element; +export const Form: ( + props: PropsWithChildren, +) => React_2.JSX.Element; // @alpha export type FormProps = Pick< @@ -123,7 +125,7 @@ export interface ParsedTemplateSchema { } // @alpha -export const ReviewState: (props: ReviewStateProps) => JSX.Element; +export const ReviewState: (props: ReviewStateProps) => React_2.JSX.Element; // @alpha export type ReviewStateProps = { @@ -134,7 +136,7 @@ export type ReviewStateProps = { // @alpha export const ScaffolderField: ( props: PropsWithChildren, -) => JSX.Element; +) => React_2.JSX.Element; // @alpha export interface ScaffolderFieldProps { @@ -159,7 +161,7 @@ export interface ScaffolderFieldProps { // @alpha (undocumented) export function ScaffolderPageContextMenu( props: ScaffolderPageContextMenuProps, -): JSX.Element | null; +): React_2.JSX.Element | null; // @alpha (undocumented) export type ScaffolderPageContextMenuProps = { @@ -169,7 +171,7 @@ export type ScaffolderPageContextMenuProps = { }; // @alpha -export const Stepper: (stepperProps: StepperProps) => JSX.Element; +export const Stepper: (stepperProps: StepperProps) => React_2.JSX.Element; // @alpha export type StepperProps = { @@ -192,10 +194,10 @@ export const TaskLogStream: (props: { logs: { [k: string]: string[]; }; -}) => JSX.Element; +}) => React_2.JSX.Element; // @alpha -export const TaskSteps: (props: TaskStepsProps) => JSX.Element; +export const TaskSteps: (props: TaskStepsProps) => React_2.JSX.Element; // @alpha export interface TaskStepsProps { @@ -210,7 +212,7 @@ export interface TaskStepsProps { } // @alpha -export const TemplateCard: (props: TemplateCardProps) => JSX.Element; +export const TemplateCard: (props: TemplateCardProps) => React_2.JSX.Element; // @alpha export interface TemplateCardProps { @@ -227,10 +229,12 @@ export interface TemplateCardProps { } // @alpha -export const TemplateCategoryPicker: () => JSX.Element | null; +export const TemplateCategoryPicker: () => React_2.JSX.Element | null; // @alpha -export const TemplateGroup: (props: TemplateGroupProps) => JSX.Element | null; +export const TemplateGroup: ( + props: TemplateGroupProps, +) => React_2.JSX.Element | null; // @alpha (undocumented) export type TemplateGroupFilter = { @@ -260,7 +264,9 @@ export interface TemplateGroupProps { } // @alpha (undocumented) -export const TemplateGroups: (props: TemplateGroupsProps) => JSX.Element | null; +export const TemplateGroups: ( + props: TemplateGroupsProps, +) => React_2.JSX.Element | null; // @alpha (undocumented) export interface TemplateGroupsProps { diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md index c618050810..23204695a4 100644 --- a/plugins/scaffolder-react/api-report.md +++ b/plugins/scaffolder-react/api-report.md @@ -295,7 +295,7 @@ export interface ScaffolderUseTemplateSecrets { // @public export const SecretsContextProvider: ( props: PropsWithChildren<{}>, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type TaskStream = { diff --git a/plugins/scaffolder-react/catalog-info.yaml b/plugins/scaffolder-react/catalog-info.yaml new file mode 100644 index 0000000000..6294c39147 --- /dev/null +++ b/plugins/scaffolder-react/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-scaffolder-react + title: '@backstage/plugin-scaffolder-react' + description: >- + A frontend library that helps other Backstage plugins interact with the + Scaffolder +spec: + lifecycle: production + type: backstage-web-library + owner: maintainers diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index ddf27cc83d..15261f64fe 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-react", "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", - "version": "1.5.2-next.0", + "version": "1.5.5-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,6 +35,7 @@ "keywords": [ "backstage" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", @@ -76,7 +77,7 @@ "luxon": "^3.0.0", "qs": "^6.9.4", "react-use": "^17.2.4", - "use-immer": "^0.8.0", + "use-immer": "^0.9.0", "zen-observable": "^0.10.0", "zod": "^3.21.4", "zod-to-json-schema": "^3.20.4" diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 414e7acf53..98f7829279 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,100 @@ # @backstage/plugin-scaffolder +## 1.14.5-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/integration-react@1.1.19-next.2 + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/config@1.1.0-next.1 + - @backstage/plugin-scaffolder-react@1.5.5-next.2 + - @backstage/plugin-catalog-react@1.8.4-next.2 + - @backstage/plugin-permission-react@0.4.15-next.2 + - @backstage/integration@1.7.0-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-scaffolder-common@1.4.1-next.1 + +## 1.14.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.4-next.1 + - @backstage/core-components@0.13.5-next.1 + - @backstage/config@1.1.0-next.0 + - @backstage/integration@1.7.0-next.1 + - @backstage/plugin-scaffolder-react@1.5.5-next.1 + - @backstage/integration-react@1.1.19-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/plugin-permission-react@0.4.15-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/plugin-scaffolder-common@1.4.1-next.0 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + +## 1.14.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration-react@1.1.18-next.0 + - @backstage/integration@1.7.0-next.0 + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/plugin-scaffolder-react@1.5.4-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-react@1.8.3-next.0 + - @backstage/plugin-permission-react@0.4.15-next.0 + - @backstage/plugin-scaffolder-common@1.4.0 + +## 1.14.2 + +### Patch Changes + +- 8a0490fb669e: Fix the get entities query in the `MyGroupsPicker` to query the `kind=Group` entities. +- Updated dependencies + - @backstage/integration-react@1.1.16 + - @backstage/integration@1.6.0 + - @backstage/core-components@0.13.4 + - @backstage/plugin-catalog-react@1.8.1 + - @backstage/plugin-scaffolder-common@1.4.0 + - @backstage/plugin-scaffolder-react@1.5.2 + - @backstage/core-plugin-api@1.5.3 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-react@0.4.14 + +## 1.14.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.8.1-next.1 + - @backstage/plugin-scaffolder-react@1.5.2-next.1 + - @backstage/integration-react@1.1.16-next.1 + ## 1.14.2-next.1 ### Patch Changes diff --git a/plugins/scaffolder/alpha-api-report.md b/plugins/scaffolder/alpha-api-report.md index de657c2138..8cacf6105e 100644 --- a/plugins/scaffolder/alpha-api-report.md +++ b/plugins/scaffolder/alpha-api-report.md @@ -7,6 +7,7 @@ import { FormProps as FormProps_2 } from '@backstage/plugin-scaffolder-react/alpha'; import type { FormProps as FormProps_3 } from '@rjsf/core-v5'; +import { JSX as JSX_2 } from 'react'; import { LayoutOptions } from '@backstage/plugin-scaffolder-react'; import { NextFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha'; import { PropsWithChildren } from 'react'; @@ -47,7 +48,7 @@ export type NextRouterProps = { // @alpha export const NextScaffolderPage: ( props: PropsWithChildren, -) => JSX.Element; +) => JSX_2.Element; // @alpha (undocumented) export type TemplateListPageProps = { diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 48f24868de..59d2d54fcc 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -23,6 +23,7 @@ import { FieldExtensionOptions as FieldExtensionOptions_2 } from '@backstage/plu import { FieldValidation } from '@rjsf/core'; import { IdentityApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; +import { JSX as JSX_2 } from 'react'; import { LayoutOptions as LayoutOptions_2 } from '@backstage/plugin-scaffolder-react'; import { LayoutTemplate as LayoutTemplate_2 } from '@backstage/plugin-scaffolder-react'; import { ListActionsResponse as ListActionsResponse_2 } from '@backstage/plugin-scaffolder-react'; @@ -500,7 +501,7 @@ export const ScaffolderLayouts: ComponentType<{ export type ScaffolderOutputlink = ScaffolderOutputLink; // @public -export const ScaffolderPage: (props: RouterProps) => JSX.Element; +export const ScaffolderPage: (props: RouterProps) => JSX_2.Element; // @public export const scaffolderPlugin: BackstagePlugin< @@ -550,7 +551,7 @@ export type ScaffolderTaskStatus = ScaffolderTaskStatus_2; export type ScaffolderUseTemplateSecrets = ScaffolderUseTemplateSecrets_2; // @public -export const TaskPage: (props: TaskPageProps) => JSX.Element; +export const TaskPage: (props: TaskPageProps) => React_2.JSX.Element; // @public export type TaskPageProps = { @@ -561,7 +562,7 @@ export type TaskPageProps = { export type TemplateParameterSchema = TemplateParameterSchema_2; // @public -export const TemplateTypePicker: () => JSX.Element | null; +export const TemplateTypePicker: () => React_2.JSX.Element | null; // @public @deprecated (undocumented) export const useTemplateSecrets: () => ScaffolderUseTemplateSecrets_2; diff --git a/plugins/scaffolder/catalog-info.yaml b/plugins/scaffolder/catalog-info.yaml new file mode 100644 index 0000000000..ab4321be3e --- /dev/null +++ b/plugins/scaffolder/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-scaffolder + title: '@backstage/plugin-scaffolder' + description: The Backstage plugin that helps you create new things +spec: + lifecycle: production + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index f010329b14..9f7b8aa553 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder", "description": "The Backstage plugin that helps you create new things", - "version": "1.14.2-next.1", + "version": "1.14.5-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,6 +35,7 @@ "keywords": [ "backstage" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "start": "backstage-cli package start", @@ -107,7 +108,6 @@ "@testing-library/user-event": "^14.0.0", "@types/humanize-duration": "^3.18.1", "@types/json-schema": "^7.0.9", - "@types/node": "^16.11.26", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/CreatedAtColumn.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/CreatedAtColumn.test.tsx index a30c1d370b..9cd1353ae1 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/columns/CreatedAtColumn.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/CreatedAtColumn.test.tsx @@ -23,7 +23,7 @@ import { DateTime } from 'luxon'; describe('', () => { it('should render the column with the time', async () => { const props = { - createdAt: DateTime.now().toISO(), + createdAt: DateTime.now().toISO()!, }; const { getByText } = await renderInTestApp(); diff --git a/plugins/search-backend-module-catalog/CHANGELOG.md b/plugins/search-backend-module-catalog/CHANGELOG.md index b2a9d07118..55feee5462 100644 --- a/plugins/search-backend-module-catalog/CHANGELOG.md +++ b/plugins/search-backend-module-catalog/CHANGELOG.md @@ -1,5 +1,94 @@ # @backstage/plugin-search-backend-module-catalog +## 0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## 0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-search-backend-node@1.2.7-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/errors@1.2.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## 0.1.4 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 29f77f923c71: Ensure that all services are dependency injected into the module instead of taken from options +- 12a8c94eda8d: Add package repository and homepage metadata +- ec1032129e47: Breaking change in the alpha export `searchModuleCatalogCollator`: Moved collator settings from module options into app-config. You are now expected to set up the catalog collator under the `search.collators.catalog` configuration key. There is also a new `catalogCollatorExtensionPoint` extension point for the module, wherein you can set custom transformers. +- d4f19a16bd52: Add User Entity email to the search index so that users can be found by their email. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-search-backend-node@1.2.4 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/backend-tasks@0.5.5 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-common@1.2.5 + +## 0.1.4-next.2 + +### Patch Changes + +- 29f77f923c71: Ensure that all services are dependency injected into the module instead of taken from options +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-search-backend-node@1.2.4-next.2 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-catalog/README.md b/plugins/search-backend-module-catalog/README.md index b5960e2789..9deeccb910 100644 --- a/plugins/search-backend-module-catalog/README.md +++ b/plugins/search-backend-module-catalog/README.md @@ -2,11 +2,18 @@ > DISCLAIMER: The new backend system is in alpha, and so are the search backend module support for the new backend system. We don't recommend you to migrate your backend installations to the new system yet. But if you want to experiment, you can find getting started guides below. -This package exports catalog backend modules responsible for extending search. +This package exports a module that extends the search backend to also indexing the entities of your catalog. -## Example +## Installation -### Use default schedule +Add the module package as a dependency: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-search-backend-module-catalog +``` + +Add the collator to your backend instance, along with the search plugin itself: ```tsx // packages/backend-next/src/index.ts @@ -20,22 +27,50 @@ backend.add(searchModuleCatalogCollator()); backend.start(); ``` -### Use custom schedule +You may also want to add configuration parameters to your app-config, for example for controlling the scheduled indexing interval. These parameters should be placed under the `search.collators.catalog` key. See [the config definition file](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-catalog/config.d.ts) for more details. + +## Advanced Customizations + +This module also has an extension point, which lets you inject advanced customizations. Here's an example of how to leverage that extension point to tweak the transformer used for building the search indexer documents: ```tsx // packages/backend-next/src/index.ts import { createBackend } from '@backstage/backend-defaults'; +import { createBackendModule } from '@backstage/backend-plugin-api'; import { searchPlugin } from '@backstage/plugin-search-backend/alpha'; -import { searchModuleCatalogCollator } from '@backstage/plugin-search-backend-module-catalog/alpha'; +import { CatalogCollatorEntityTransformer } from '@backstage/plugin-search-backend-module-catalog'; +import { + searchModuleCatalogCollator, + catalogCollatorExtensionPoint, +} from '@backstage/plugin-search-backend-module-catalog/alpha'; -const schedule = { - frequency: { minutes: 10 }, - timeout: { minutes: 15 }, - initialDelay: { seconds: 3 }, -}; +const customTransformer: CatalogCollatorEntityTransformer = entity => ({ + title: entity.metadata.title || entity.metadata.name, + text: entity.metadata.description || '', + componentType: entity.spec?.type?.toString() || 'other', + type: entity.spec?.type?.toString() || 'other', + namespace: entity.metadata.namespace || 'default', + kind: entity.kind, + lifecycle: (entity.spec?.lifecycle as string) || '', + owner: (entity.spec?.owner as string) || '', +}); const backend = createBackend(); backend.add(searchPlugin()); -backend.add(searchModuleCatalogCollator({ schedule })); +backend.add(searchModuleCatalogCollator()); +backend.add( + createBackendModule({ + pluginId: 'search', + moduleId: 'myCatalogCollatorOptions', + register(reg) { + reg.registerInit({ + deps: { collator: catalogCollatorExtensionPoint }, + init({ collator }) { + collator.setEntityTransformer(customTransformer); + }, + }); + }, + })(), +); backend.start(); ``` diff --git a/plugins/search-backend-module-catalog/alpha-api-report.md b/plugins/search-backend-module-catalog/alpha-api-report.md index 7c6e91b6a1..f840884f68 100644 --- a/plugins/search-backend-module-catalog/alpha-api-report.md +++ b/plugins/search-backend-module-catalog/alpha-api-report.md @@ -4,21 +4,20 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; -import { DefaultCatalogCollatorFactoryOptions } from '@backstage/plugin-search-backend-module-catalog'; -import { TaskScheduleDefinition } from '@backstage/backend-tasks'; +import { CatalogCollatorEntityTransformer } from '@backstage/plugin-search-backend-module-catalog'; +import { ExtensionPoint } from '@backstage/backend-plugin-api'; // @alpha -export const searchModuleCatalogCollator: ( - options?: SearchModuleCatalogCollatorOptions | undefined, -) => BackendFeature; - -// @alpha -export type SearchModuleCatalogCollatorOptions = Omit< - DefaultCatalogCollatorFactoryOptions, - 'discovery' | 'tokenManager' | 'catalogClient' -> & { - schedule?: TaskScheduleDefinition; +export type CatalogCollatorExtensionPoint = { + setEntityTransformer(transformer: CatalogCollatorEntityTransformer): void; }; +// @alpha +export const catalogCollatorExtensionPoint: ExtensionPoint; + +// @alpha +const _default: () => BackendFeature; +export default _default; + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/search-backend-module-catalog/api-report.md b/plugins/search-backend-module-catalog/api-report.md index b773860fe8..6f5813ca9e 100644 --- a/plugins/search-backend-module-catalog/api-report.md +++ b/plugins/search-backend-module-catalog/api-report.md @@ -24,11 +24,11 @@ export type CatalogCollatorEntityTransformer = ( // @public (undocumented) export const defaultCatalogCollatorEntityTransformer: CatalogCollatorEntityTransformer; -// @public (undocumented) +// @public export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { // (undocumented) static fromConfig( - _config: Config, + configRoot: Config, options: DefaultCatalogCollatorFactoryOptions, ): DefaultCatalogCollatorFactory; // (undocumented) diff --git a/plugins/search-backend-module-catalog/catalog-info.yaml b/plugins/search-backend-module-catalog/catalog-info.yaml new file mode 100644 index 0000000000..d36627848e --- /dev/null +++ b/plugins/search-backend-module-catalog/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-backend-module-catalog + title: '@backstage/plugin-search-backend-module-catalog' + description: A module for the search backend that exports catalog modules +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: discoverability-maintainers diff --git a/plugins/search-backend-module-catalog/config.d.ts b/plugins/search-backend-module-catalog/config.d.ts new file mode 100644 index 0000000000..0105764afb --- /dev/null +++ b/plugins/search-backend-module-catalog/config.d.ts @@ -0,0 +1,55 @@ +/* + * 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 { TaskScheduleDefinitionConfig } from '@backstage/backend-tasks'; + +export interface Config { + search?: { + collators?: { + /** + * Configuration options for `@backstage/plugin-search-backend-module-catalog` + */ + catalog?: { + /** + * A templating string with placeholders, to form the final location of + * the entity. + * + * Defaults to '/catalog/:namespace/:kind/:name' + */ + locationTemplate?: string; + /** + * A filter expression passed to the catalog client, to select what + * entities to collate. + * + * Defaults to no filter, ie indexing all entities. + */ + filter?: object; + /** + * The number of entities to process at a time. Keep this at a + * reasonable number to avoid overloading either the catalog or the + * search backend. + * + * Defaults to 500 + */ + batchSize?: number; + /** + * The schedule for how often to run the collation job. + */ + schedule?: TaskScheduleDefinitionConfig; + }; + }; + }; +} diff --git a/plugins/search-backend-module-catalog/package.json b/plugins/search-backend-module-catalog/package.json index 37473ef04b..85fc0dd645 100644 --- a/plugins/search-backend-module-catalog/package.json +++ b/plugins/search-backend-module-catalog/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-catalog", "description": "A module for the search backend that exports catalog modules", - "version": "0.1.4-next.1", + "version": "0.1.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -48,6 +48,7 @@ "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", @@ -61,6 +62,8 @@ "msw": "^1.0.0" }, "files": [ - "dist" - ] + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" } diff --git a/plugins/search-backend-module-catalog/src/alpha.test.ts b/plugins/search-backend-module-catalog/src/alpha.test.ts index 8a3082c6aa..eb2a795ecc 100644 --- a/plugins/search-backend-module-catalog/src/alpha.test.ts +++ b/plugins/search-backend-module-catalog/src/alpha.test.ts @@ -14,17 +14,11 @@ * limitations under the License. */ -import { startTestBackend } from '@backstage/backend-test-utils'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; -import { searchModuleCatalogCollator } from './alpha'; +import searchModuleCatalogCollator from './alpha'; describe('searchModuleCatalogCollator', () => { - const schedule = { - frequency: { minutes: 10 }, - timeout: { minutes: 15 }, - initialDelay: { seconds: 3 }, - }; - it('should register the catalog collator to the search index registry extension point with factory and schedule', async () => { const extensionPointMock = { addCollator: jest.fn(), @@ -34,7 +28,7 @@ describe('searchModuleCatalogCollator', () => { extensionPoints: [ [searchIndexRegistryExtensionPoint, extensionPointMock], ], - features: [searchModuleCatalogCollator({ schedule })], + features: [searchModuleCatalogCollator()], }); expect(extensionPointMock.addCollator).toHaveBeenCalledTimes(1); @@ -43,4 +37,37 @@ describe('searchModuleCatalogCollator', () => { schedule: expect.objectContaining({ run: expect.any(Function) }), }); }); + + it('refuses to start up with a broken schedule', async () => { + await expect( + startTestBackend({ + extensionPoints: [ + [ + searchIndexRegistryExtensionPoint, + { + addCollator: jest.fn(), + }, + ], + ], + features: [ + searchModuleCatalogCollator(), + mockServices.rootConfig.factory({ + data: { + search: { + collators: { + catalog: { + schedule: { + frequency: { minutes: 10 }, + timeout: { minutes: 'boo' }, + initialDelay: { seconds: 3 }, + }, + }, + }, + }, + }, + }), + ], + }), + ).rejects.toThrow(/Invalid schedule/); + }); }); diff --git a/plugins/search-backend-module-catalog/src/alpha.ts b/plugins/search-backend-module-catalog/src/alpha.ts index cfd8c13539..5813bb235e 100644 --- a/plugins/search-backend-module-catalog/src/alpha.ts +++ b/plugins/search-backend-module-catalog/src/alpha.ts @@ -22,73 +22,88 @@ import { coreServices, createBackendModule, + createExtensionPoint, } from '@backstage/backend-plugin-api'; -import { TaskScheduleDefinition } from '@backstage/backend-tasks'; -import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; - -import { - DefaultCatalogCollatorFactory, - DefaultCatalogCollatorFactoryOptions, -} from '@backstage/plugin-search-backend-module-catalog'; import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; +import { + CatalogCollatorEntityTransformer, + DefaultCatalogCollatorFactory, +} from '@backstage/plugin-search-backend-module-catalog'; +import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; +import { readScheduleConfigOptions } from './collators/config'; /** + * Options for {@link catalogCollatorExtensionPoint}. + * * @alpha - * Options for {@link searchModuleCatalogCollator}. */ -export type SearchModuleCatalogCollatorOptions = Omit< - DefaultCatalogCollatorFactoryOptions, - 'discovery' | 'tokenManager' | 'catalogClient' -> & { - schedule?: TaskScheduleDefinition; +export type CatalogCollatorExtensionPoint = { + /** + * Allows you to customize how entities are shaped into documents. + */ + setEntityTransformer(transformer: CatalogCollatorEntityTransformer): void; }; +/** + * Extension point for customizing how catalog entities are shaped into + * documents for the search backend. + * + * @alpha + */ +export const catalogCollatorExtensionPoint = + createExtensionPoint({ + id: 'search.catalogCollator.extension', + }); + /** * Search backend module for the Catalog index. * * @alpha */ -export const searchModuleCatalogCollator = createBackendModule( - (options?: SearchModuleCatalogCollatorOptions) => ({ - moduleId: 'catalogCollator', - pluginId: 'search', - register(env) { - env.registerInit({ - deps: { - config: coreServices.rootConfig, - discovery: coreServices.discovery, - tokenManager: coreServices.tokenManager, - scheduler: coreServices.scheduler, - indexRegistry: searchIndexRegistryExtensionPoint, - catalog: catalogServiceRef, - }, - async init({ - config, - discovery, - tokenManager, - scheduler, - indexRegistry, - catalog, - }) { - const defaultSchedule = { - frequency: { minutes: 10 }, - timeout: { minutes: 15 }, - initialDelay: { seconds: 3 }, - }; +export default createBackendModule({ + moduleId: 'catalogCollator', + pluginId: 'search', + register(env) { + let entityTransformer: CatalogCollatorEntityTransformer | undefined; - indexRegistry.addCollator({ - schedule: scheduler.createScheduledTaskRunner( - options?.schedule ?? defaultSchedule, - ), - factory: DefaultCatalogCollatorFactory.fromConfig(config, { - ...options, - discovery, - tokenManager, - catalogClient: catalog, - }), - }); - }, - }); - }, - }), -); + env.registerExtensionPoint(catalogCollatorExtensionPoint, { + setEntityTransformer(transformer) { + if (entityTransformer) { + throw new Error('setEntityTransformer can only be called once'); + } + entityTransformer = transformer; + }, + }); + + env.registerInit({ + deps: { + config: coreServices.rootConfig, + discovery: coreServices.discovery, + tokenManager: coreServices.tokenManager, + scheduler: coreServices.scheduler, + indexRegistry: searchIndexRegistryExtensionPoint, + catalog: catalogServiceRef, + }, + async init({ + config, + discovery, + tokenManager, + scheduler, + indexRegistry, + catalog, + }) { + indexRegistry.addCollator({ + schedule: scheduler.createScheduledTaskRunner( + readScheduleConfigOptions(config), + ), + factory: DefaultCatalogCollatorFactory.fromConfig(config, { + entityTransformer, + discovery, + tokenManager, + catalogClient: catalog, + }), + }); + }, + }); + }, +}); diff --git a/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.test.ts b/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.test.ts index 462e777dff..7af3a8e9a2 100644 --- a/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.test.ts +++ b/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { PluginEndpointDiscovery, TokenManager, diff --git a/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.ts b/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.ts index ed81d39bbb..586346c385 100644 --- a/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.ts +++ b/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { Config } from '@backstage/config'; import { PluginEndpointDiscovery, TokenManager, @@ -24,27 +23,45 @@ import { CatalogClient, GetEntitiesRequest, } from '@backstage/catalog-client'; -import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; -import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common/alpha'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; +import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common/alpha'; import { Permission } from '@backstage/plugin-permission-common'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { Readable } from 'stream'; import { CatalogCollatorEntityTransformer } from './CatalogCollatorEntityTransformer'; +import { readCollatorConfigOptions } from './config'; import { defaultCatalogCollatorEntityTransformer } from './defaultCatalogCollatorEntityTransformer'; -import { stringifyEntityRef } from '@backstage/catalog-model'; /** @public */ export type DefaultCatalogCollatorFactoryOptions = { discovery: PluginEndpointDiscovery; tokenManager: TokenManager; + /** + * @deprecated Use the config key `search.collators.catalog.locationTemplate` instead. + */ locationTemplate?: string; + /** + * @deprecated Use the config key `search.collators.catalog.filter` instead. + */ filter?: GetEntitiesRequest['filter']; + /** + * @deprecated Use the config key `search.collators.catalog.batchSize` instead. + */ batchSize?: number; catalogClient?: CatalogApi; + /** + * Allows you to customize how entities are shaped into documents. + */ entityTransformer?: CatalogCollatorEntityTransformer; }; -/** @public */ +/** + * Collates entities from the Catalog into documents for the search backend. + * + * @public + */ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { public readonly type = 'software-catalog'; public readonly visibilityPermission: Permission = @@ -58,13 +75,31 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { private entityTransformer: CatalogCollatorEntityTransformer; static fromConfig( - _config: Config, + configRoot: Config, options: DefaultCatalogCollatorFactoryOptions, ) { - return new DefaultCatalogCollatorFactory(options); + const configOptions = readCollatorConfigOptions(configRoot); + return new DefaultCatalogCollatorFactory({ + locationTemplate: + options.locationTemplate ?? configOptions.locationTemplate, + filter: options.filter ?? configOptions.filter, + batchSize: options.batchSize ?? configOptions.batchSize, + entityTransformer: options.entityTransformer, + discovery: options.discovery, + tokenManager: options.tokenManager, + catalogClient: options.catalogClient, + }); } - private constructor(options: DefaultCatalogCollatorFactoryOptions) { + private constructor(options: { + locationTemplate: string; + filter: GetEntitiesRequest['filter']; + batchSize: number; + entityTransformer?: CatalogCollatorEntityTransformer; + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + catalogClient?: CatalogApi; + }) { const { batchSize, discovery, @@ -75,10 +110,9 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { entityTransformer, } = options; - this.locationTemplate = - locationTemplate || '/catalog/:namespace/:kind/:name'; + this.locationTemplate = locationTemplate; this.filter = filter; - this.batchSize = batchSize || 500; + this.batchSize = batchSize; this.catalogClient = catalogClient || new CatalogClient({ discoveryApi: discovery }); this.tokenManager = tokenManager; diff --git a/plugins/search-backend-module-catalog/src/collators/config.test.ts b/plugins/search-backend-module-catalog/src/collators/config.test.ts new file mode 100644 index 0000000000..dc330f6df2 --- /dev/null +++ b/plugins/search-backend-module-catalog/src/collators/config.test.ts @@ -0,0 +1,82 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { + defaults, + readCollatorConfigOptions, + readScheduleConfigOptions, +} from './config'; + +describe('config', () => { + describe('readScheduleConfigOptions', () => { + it('reads config', () => { + const result = readScheduleConfigOptions( + new ConfigReader({ + search: { + collators: { + catalog: { + schedule: { + frequency: { minutes: 1 }, + timeout: { seconds: 2 }, + initialDelay: { hours: 3 }, + }, + }, + }, + }, + }), + ); + expect(result).toEqual({ + frequency: { minutes: 1 }, + timeout: { seconds: 2 }, + initialDelay: { hours: 3 }, + }); + }); + + it('returns defaults', () => { + const result = readScheduleConfigOptions(new ConfigReader({})); + expect(result).toEqual(defaults.schedule); + }); + }); + + describe('readCollatorConfigOptions', () => { + it('reads config', () => { + const result = readCollatorConfigOptions( + new ConfigReader({ + search: { + collators: { + catalog: { + batchSize: 1, + locationTemplate: '/mock/:namespace/:kind/:name', + filter: { a: 1 }, + }, + }, + }, + }), + ); + expect(result).toEqual({ + batchSize: 1, + locationTemplate: '/mock/:namespace/:kind/:name', + filter: { a: 1 }, + }); + }); + + it('returns defaults', () => { + const result = readCollatorConfigOptions(new ConfigReader({})); + expect(result).toEqual(defaults.collatorOptions); + }); + }); +}); diff --git a/plugins/search-backend-module-catalog/src/collators/config.ts b/plugins/search-backend-module-catalog/src/collators/config.ts new file mode 100644 index 0000000000..d4ae9b5934 --- /dev/null +++ b/plugins/search-backend-module-catalog/src/collators/config.ts @@ -0,0 +1,81 @@ +/* + * 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 { + readTaskScheduleDefinitionFromConfig, + TaskScheduleDefinition, +} from '@backstage/backend-tasks'; +import { EntityFilterQuery } from '@backstage/catalog-client'; +import { Config } from '@backstage/config'; +import { InputError } from '@backstage/errors'; + +const configKey = 'search.collators.catalog'; + +export const defaults = { + schedule: { + frequency: { minutes: 10 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 3 }, + }, + collatorOptions: { + locationTemplate: '/catalog/:namespace/:kind/:name', + filter: undefined, + batchSize: 500, + }, +}; + +export function readScheduleConfigOptions( + configRoot: Config, +): TaskScheduleDefinition { + let schedule: TaskScheduleDefinition | undefined = undefined; + + const config = configRoot.getOptionalConfig(configKey); + if (config) { + const scheduleConfig = config.getOptionalConfig('schedule'); + if (scheduleConfig) { + try { + schedule = readTaskScheduleDefinitionFromConfig(scheduleConfig); + } catch (error) { + throw new InputError(`Invalid schedule at ${configKey}, ${error}`); + } + } + } + + return schedule ?? defaults.schedule; +} + +export function readCollatorConfigOptions(configRoot: Config): { + locationTemplate: string; + filter: EntityFilterQuery | undefined; + batchSize: number; +} { + const config = configRoot.getOptionalConfig(configKey); + if (!config) { + return defaults.collatorOptions; + } + + return { + locationTemplate: + config.getOptionalString('locationTemplate') ?? + defaults.collatorOptions.locationTemplate, + filter: + config.getOptionalConfig('filter')?.get() ?? + defaults.collatorOptions.filter, + batchSize: + config.getOptionalNumber('batchSize') ?? + defaults.collatorOptions.batchSize, + }; +} diff --git a/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.test.ts b/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.test.ts index 8206a58599..3fe6a9c3e5 100644 --- a/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.test.ts +++ b/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.test.ts @@ -42,6 +42,7 @@ const userEntity = { spec: { profile: { displayName: 'User 1', + email: 'test@test.com', }, }, }; @@ -94,7 +95,7 @@ describe('DefaultCatalogCollatorEntityTransformer', () => { expect(document).toMatchObject({ title: userEntity.metadata.name, - text: `${userEntity.metadata.description} : ${userEntity.spec.profile.displayName}`, + text: `${userEntity.metadata.description} : ${userEntity.spec.profile.displayName} : ${userEntity.spec.profile.email}`, namespace: 'default', componentType: 'other', lifecycle: '', diff --git a/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.ts b/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.ts index 2fc0c5d50d..db239b8669 100644 --- a/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.ts +++ b/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.ts @@ -27,6 +27,12 @@ const getDocumentText = (entity: Entity): string => { } } + if (isUserEntity(entity)) { + if (entity.spec?.profile?.email) { + documentTexts.push(entity.spec.profile.email); + } + } + return documentTexts.join(' : '); }; diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index 2191fa05ff..3e2af01514 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,68 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 1.3.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/integration-aws-node@0.1.6-next.1 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/plugin-search-common@1.2.6-next.1 + +## 1.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/integration-aws-node@0.1.6-next.0 + - @backstage/plugin-search-backend-node@1.2.7-next.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## 1.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/integration-aws-node@0.1.5 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## 1.3.3 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- 631eb3816b48: Index templates can now be configured through configuration under the `search.elasticsearch.indexTemplates`. In addition, the `ElasticSearchSearchEngine.fromConfig` now also accepts a `LoggerService` as the `logger` option as well as a new `translator` option. + + The alpha `searchModuleElasticsearchEngine` export no longer accepts options and a new alpha `elasticsearchTranslatorExtensionPoint` export has been added which lets you customize the query translator. + +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-search-backend-node@1.2.4 + - @backstage/integration-aws-node@0.1.5 + - @backstage/config@1.0.8 + - @backstage/plugin-search-common@1.2.5 + +## 1.3.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-search-backend-node@1.2.4-next.2 + ## 1.3.3-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/alpha-api-report.md b/plugins/search-backend-module-elasticsearch/alpha-api-report.md index 91007c20de..c0c30f7f2b 100644 --- a/plugins/search-backend-module-elasticsearch/alpha-api-report.md +++ b/plugins/search-backend-module-elasticsearch/alpha-api-report.md @@ -4,19 +4,21 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; -import { ElasticSearchCustomIndexTemplate } from '@backstage/plugin-search-backend-module-elasticsearch'; import { ElasticSearchQueryTranslator } from '@backstage/plugin-search-backend-module-elasticsearch'; +import { ExtensionPoint } from '@backstage/backend-plugin-api'; // @alpha -export const searchModuleElasticsearchEngine: ( - options?: SearchModuleElasticsearchEngineOptions | undefined, -) => BackendFeature; +const _default: () => BackendFeature; +export default _default; + +// @alpha (undocumented) +export interface ElasticSearchQueryTranslatorExtensionPoint { + // (undocumented) + setTranslator(translator: ElasticSearchQueryTranslator): void; +} // @alpha -export type SearchModuleElasticsearchEngineOptions = { - translator?: ElasticSearchQueryTranslator; - indexTemplate?: ElasticSearchCustomIndexTemplate; -}; +export const elasticsearchTranslatorExtensionPoint: ExtensionPoint; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/search-backend-module-elasticsearch/api-report.md b/plugins/search-backend-module-elasticsearch/api-report.md index b567414b22..408ec288d0 100644 --- a/plugins/search-backend-module-elasticsearch/api-report.md +++ b/plugins/search-backend-module-elasticsearch/api-report.md @@ -15,6 +15,7 @@ import type { ConnectionOptions } from 'tls'; import { IndexableDocument } from '@backstage/plugin-search-common'; import { IndexableResultSet } from '@backstage/plugin-search-common'; import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { Readable } from 'stream'; import { SearchEngine } from '@backstage/plugin-search-common'; import { SearchQuery } from '@backstage/plugin-search-common'; @@ -299,10 +300,11 @@ export interface ElasticSearchNodeOptions { // @public export type ElasticSearchOptions = { - logger: Logger; + logger: Logger | LoggerService; config: Config; aliasPostfix?: string; indexPrefix?: string; + translator?: ElasticSearchQueryTranslator; }; // @public @@ -322,7 +324,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { elasticSearchClientOptions: ElasticSearchClientOptions, aliasPostfix: string, indexPrefix: string, - logger: Logger, + logger: Logger | LoggerService, batchSize: number, highlightOptions?: ElasticSearchHighlightOptions, ); @@ -365,7 +367,7 @@ export type ElasticSearchSearchEngineIndexerOptions = { indexPrefix: string; indexSeparator: string; alias: string; - logger: Logger; + logger: Logger | LoggerService; elasticSearchClientWrapper: ElasticSearchClientWrapper; batchSize: number; }; diff --git a/plugins/search-backend-module-elasticsearch/catalog-info.yaml b/plugins/search-backend-module-elasticsearch/catalog-info.yaml new file mode 100644 index 0000000000..300d1034c9 --- /dev/null +++ b/plugins/search-backend-module-elasticsearch/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-backend-module-elasticsearch + title: '@backstage/plugin-search-backend-module-elasticsearch' + description: A module for the search backend that implements search using ElasticSearch +spec: + lifecycle: production + type: backstage-backend-plugin-module + owner: discoverability-maintainers diff --git a/plugins/search-backend-module-elasticsearch/config.d.ts b/plugins/search-backend-module-elasticsearch/config.d.ts index 16190e7ed0..d7fdd0cd1b 100644 --- a/plugins/search-backend-module-elasticsearch/config.d.ts +++ b/plugins/search-backend-module-elasticsearch/config.d.ts @@ -43,6 +43,33 @@ export interface Config { */ fragmentDelimiter?: string; }; + + /** Elasticsearch specific index template bodies */ + indexTemplates?: Array<{ + name: string; + body: { + /** + * Array of wildcard (*) expressions used to match the names of data streams and indices during creation. + */ + index_patterns: string[]; + + /** + * An ordered list of component template names. + * Component templates are merged in the order specified, + * meaning that the last component template specified has the highest precedence. + */ + composed_of?: string[]; + + /** + * See available properties of template + * https://www.elastic.co/guide/en/elasticsearch/reference/7.15/indices-put-template.html#put-index-template-api-request-body + */ + template?: { + [key: string]: unknown; + }; + }; + }>; + /** Miscellaneous options for the client */ clientOptions?: { ssl?: { @@ -128,7 +155,7 @@ export interface Config { }; /* TODO(kuangp): unsupported until @elastic/elasticsearch@7.14 is released | { - + /** * Bearer authentication token to connect to the cluster. * See: https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-service-token.html diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index f119b1b28a..6b8bb114b2 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", "description": "A module for the search backend that implements search using ElasticSearch", - "version": "1.3.3-next.1", + "version": "1.3.6-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend-module-elasticsearch/src/alpha.ts b/plugins/search-backend-module-elasticsearch/src/alpha.ts index 7e1b95279a..f289847865 100644 --- a/plugins/search-backend-module-elasticsearch/src/alpha.ts +++ b/plugins/search-backend-module-elasticsearch/src/alpha.ts @@ -16,59 +16,66 @@ import { coreServices, createBackendModule, + createExtensionPoint, } from '@backstage/backend-plugin-api'; -import { loggerToWinstonLogger } from '@backstage/backend-common'; import { searchEngineRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; - import { - ElasticSearchCustomIndexTemplate, ElasticSearchQueryTranslator, ElasticSearchSearchEngine, } from '@backstage/plugin-search-backend-module-elasticsearch'; -/** - * @alpha - * Options for {@link searchModuleElasticsearchEngine}. - */ -export type SearchModuleElasticsearchEngineOptions = { - translator?: ElasticSearchQueryTranslator; - indexTemplate?: ElasticSearchCustomIndexTemplate; -}; +/** @alpha */ +export interface ElasticSearchQueryTranslatorExtensionPoint { + setTranslator(translator: ElasticSearchQueryTranslator): void; +} /** + * Extension point used to customize the ElasticSearch query translator. + * * @alpha + */ +export const elasticsearchTranslatorExtensionPoint = + createExtensionPoint({ + id: 'search.elasticsearchEngine.translator', + }); + +/** * Search backend module for the Elasticsearch engine. + * + * @alpha */ -export const searchModuleElasticsearchEngine = createBackendModule( - (options?: SearchModuleElasticsearchEngineOptions) => ({ - moduleId: 'elasticsearchEngine', - pluginId: 'search', - register(env) { - env.registerInit({ - deps: { - searchEngineRegistry: searchEngineRegistryExtensionPoint, - logger: coreServices.logger, - config: coreServices.rootConfig, - }, - async init({ searchEngineRegistry, logger, config }) { - const searchEngine = await ElasticSearchSearchEngine.fromConfig({ - logger: loggerToWinstonLogger(logger), - config: config, - }); +export default createBackendModule({ + moduleId: 'elasticsearchEngine', + pluginId: 'search', + register(env) { + let translator: ElasticSearchQueryTranslator | undefined; - // set custom translator if available - if (options?.translator) { - searchEngine.setTranslator(options.translator); - } + env.registerExtensionPoint(elasticsearchTranslatorExtensionPoint, { + setTranslator(newTranslator) { + if (translator) { + throw new Error( + 'ElasticSearch query translator may only be set once', + ); + } + translator = newTranslator; + }, + }); - // set custom index template if available - if (options?.indexTemplate) { - searchEngine.setIndexTemplate(options.indexTemplate); - } - - searchEngineRegistry.setSearchEngine(searchEngine); - }, - }); - }, - }), -); + env.registerInit({ + deps: { + searchEngineRegistry: searchEngineRegistryExtensionPoint, + logger: coreServices.logger, + config: coreServices.rootConfig, + }, + async init({ searchEngineRegistry, logger, config }) { + searchEngineRegistry.setSearchEngine( + await ElasticSearchSearchEngine.fromConfig({ + logger, + config, + translator, + }), + ); + }, + }); + }, +}); diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 90f6f407d9..9b085b52b0 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -38,6 +38,7 @@ import { AwsCredentialProvider, DefaultAwsCredentialsManager, } from '@backstage/integration-aws-node'; +import { LoggerService } from '@backstage/backend-plugin-api'; export type { ElasticSearchClientOptions }; @@ -73,10 +74,11 @@ export type ElasticSearchQueryTranslator = ( * @public */ export type ElasticSearchOptions = { - logger: Logger; + logger: Logger | LoggerService; config: Config; aliasPostfix?: string; indexPrefix?: string; + translator?: ElasticSearchQueryTranslator; }; /** @@ -126,7 +128,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { private readonly elasticSearchClientOptions: ElasticSearchClientOptions, private readonly aliasPostfix: string, private readonly indexPrefix: string, - private readonly logger: Logger, + private readonly logger: Logger | LoggerService, private readonly batchSize: number, highlightOptions?: ElasticSearchHighlightOptions, ) { @@ -149,6 +151,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { config, aliasPostfix = `search`, indexPrefix = ``, + translator, } = options; const credentialProvider = DefaultAwsCredentialsManager.fromConfig(config); const clientOptions = await this.createElasticSearchClientOptions( @@ -165,7 +168,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { logger.info('Initializing ElasticSearch search engine.'); } - return new ElasticSearchSearchEngine( + const engine = new ElasticSearchSearchEngine( clientOptions, aliasPostfix, indexPrefix, @@ -176,6 +179,18 @@ export class ElasticSearchSearchEngine implements SearchEngine { 'search.elasticsearch.highlightOptions', ), ); + + for (const indexTemplate of this.readIndexTemplateConfig( + config.getConfig('search.elasticsearch'), + )) { + await engine.setIndexTemplate(indexTemplate); + } + + if (translator) { + await engine.setTranslator(translator); + } + + return engine; } /** @@ -221,11 +236,10 @@ export class ElasticSearchSearchEngine implements SearchEngine { .boolQuery() .should(value.map(it => esb.matchQuery(key, it.toString()))); } - this.logger.error( - 'Failed to query, unrecognized filter type', + this.logger.error('Failed to query, unrecognized filter type', { key, value, - ); + }); throw new Error( 'Failed to add filters to query. Unrecognized filter type', ); @@ -518,6 +532,24 @@ export class ElasticSearchSearchEngine implements SearchEngine { : {}), }; } + + private static readIndexTemplateConfig( + config: Config, + ): ElasticSearchCustomIndexTemplate[] { + return ( + config.getOptionalConfigArray('indexTemplates')?.map(templateConfig => { + const bodyConfig = templateConfig.getConfig('body'); + return { + name: templateConfig.getString('name'), + body: { + index_patterns: bodyConfig.getStringArray('index_patterns'), + composed_of: bodyConfig.getOptionalStringArray('composed_of'), + template: bodyConfig.getOptionalConfig('template')?.get(), + }, + }; + }) ?? [] + ); + } } /** diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts index a9d15c421e..b742216d52 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts @@ -19,6 +19,7 @@ import { ElasticSearchClientWrapper } from './ElasticSearchClientWrapper'; import { IndexableDocument } from '@backstage/plugin-search-common'; import { Logger } from 'winston'; import { Readable } from 'stream'; +import { LoggerService } from '@backstage/backend-plugin-api'; /** * Options for instantiate ElasticSearchSearchEngineIndexer @@ -29,7 +30,7 @@ export type ElasticSearchSearchEngineIndexerOptions = { indexPrefix: string; indexSeparator: string; alias: string; - logger: Logger; + logger: Logger | LoggerService; elasticSearchClientWrapper: ElasticSearchClientWrapper; batchSize: number; }; @@ -55,7 +56,7 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { private readonly indexSeparator: string; private readonly alias: string; private readonly removableAlias: string; - private readonly logger: Logger; + private readonly logger: Logger | LoggerService; private readonly sourceStream: Readable; private readonly elasticSearchClientWrapper: ElasticSearchClientWrapper; private configuredBatchSize: number; @@ -186,7 +187,9 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { // If any indices are removable, remove them. Do not bubble up this error, // as doing so would delete the now aliased index. Log instead. if (this.removableIndices.length) { - this.logger.info('Removing stale search indices', this.removableIndices); + this.logger.info('Removing stale search indices', { + removableIndices: this.removableIndices, + }); try { await this.elasticSearchClientWrapper.deleteIndex({ index: this.removableIndices, diff --git a/plugins/search-backend-module-explore/CHANGELOG.md b/plugins/search-backend-module-explore/CHANGELOG.md index fc9fedf290..c1de962a4e 100644 --- a/plugins/search-backend-module-explore/CHANGELOG.md +++ b/plugins/search-backend-module-explore/CHANGELOG.md @@ -1,5 +1,72 @@ # @backstage/plugin-search-backend-module-explore +## 0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## 0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.1 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## 0.1.4 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 29f77f923c71: Ensure that all services are dependency injected into the module instead of taken from options +- 12a8c94eda8d: Add package repository and homepage metadata +- 6694e79ab396: Breaking change for the alpha export moved `schedule` from module options into app-config for the new backend system. You can now pass in a `TaskScheduleDefinitionConfig` through the `search.collators.explore.schedule` configuration key. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-search-backend-node@1.2.4 + - @backstage/backend-tasks@0.5.5 + - @backstage/config@1.0.8 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.5 + +## 0.1.4-next.2 + +### Patch Changes + +- 29f77f923c71: Ensure that all services are dependency injected into the module instead of taken from options +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-search-backend-node@1.2.4-next.2 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-explore/README.md b/plugins/search-backend-module-explore/README.md index e775cf42b6..cf2a01b9f0 100644 --- a/plugins/search-backend-module-explore/README.md +++ b/plugins/search-backend-module-explore/README.md @@ -2,14 +2,21 @@ > DISCLAIMER: The new backend system is in alpha, and so are the search backend module support for the new backend system. We don't recommend you to migrate your backend installations to the new system yet. But if you want to experiment, you can find getting started guides below. -This package exports explore backend modules responsible for extending search. +This package exports a module that extends the search backend to also indexing the tools exposed by the [`explore` service](https://github.com/backstage/backstage/tree/master/plugins/explore-backend). -## Example +## Installation -### Use default schedule +Add the module package as a dependency: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-search-backend-module-explore +``` + +Add the collator to your backend instance, along with the search plugin itself: ```tsx -// packages/backend-next/src/index.ts +// packages/backend/src/index.ts import { createBackend } from '@backstage/backend-defaults'; import { searchPlugin } from '@backstage/plugin-search-backend/alpha'; import { searchModuleExploreCollator } from '@backstage/plugin-search-backend-module-explore/alpha'; @@ -20,29 +27,11 @@ backend.add(searchModuleExploreCollator()); backend.start(); ``` -### Use custom schedule +You may also want to add configuration parameters to your app-config, for example for controlling the scheduled indexing interval. These parameters should be placed under the `search.collators.explore` key. See [the config definition file](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-explore/config.d.ts) for more details. -```tsx -// packages/backend-next/src/index.ts -import { createBackend } from '@backstage/backend-defaults'; -import { searchPlugin } from '@backstage/plugin-search-backend/alpha'; -import { searchModuleExploreCollator } from '@backstage/plugin-search-backend-module-explore/alpha'; +## Using Auth Middleware -const schedule = { - frequency: { minutes: 10 }, - timeout: { minutes: 15 }, - initialDelay: { seconds: 3 }, -}; - -const backend = createBackend(); -backend.add(searchPlugin()); -backend.add(searchModuleExploreCollator({ schedule })); -backend.start(); -``` - -### Using Auth Middleware - -If your Backstage instance uses service-to-service authentication you can pass an optional `tokenManager` to the collator factory. This will ensure that the collator makes authenticated requests to the explore backend. +If your Backstage instance uses service-to-service authentication, the collator will need to have access to a `tokenManager`. This is automatically injected by the collator module when using the new backend system. But if you are using the old backend system, you will want to add it manually to ensure that the collator makes authenticated requests to the explore backend. ```tsx indexBuilder.addCollator({ diff --git a/plugins/search-backend-module-explore/alpha-api-report.md b/plugins/search-backend-module-explore/alpha-api-report.md index 7b0f2658d6..e1ecb85886 100644 --- a/plugins/search-backend-module-explore/alpha-api-report.md +++ b/plugins/search-backend-module-explore/alpha-api-report.md @@ -4,17 +4,10 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; -import { TaskScheduleDefinition } from '@backstage/backend-tasks'; // @alpha -export const searchModuleExploreCollator: ( - options?: SearchModuleExploreCollatorOptions | undefined, -) => BackendFeature; - -// @alpha -export type SearchModuleExploreCollatorOptions = { - schedule?: TaskScheduleDefinition; -}; +const _default: () => BackendFeature; +export default _default; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/search-backend-module-explore/catalog-info.yaml b/plugins/search-backend-module-explore/catalog-info.yaml new file mode 100644 index 0000000000..237e7a0f98 --- /dev/null +++ b/plugins/search-backend-module-explore/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-backend-module-explore + title: '@backstage/plugin-search-backend-module-explore' + description: A module for the search backend that exports explore modules +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: discoverability-maintainers diff --git a/plugins/search-backend-module-explore/config.d.ts b/plugins/search-backend-module-explore/config.d.ts new file mode 100644 index 0000000000..73ff015f20 --- /dev/null +++ b/plugins/search-backend-module-explore/config.d.ts @@ -0,0 +1,33 @@ +/* + * 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 { TaskScheduleDefinitionConfig } from '@backstage/backend-tasks'; + +export interface Config { + search?: { + collators?: { + /** + * Configuration options for `@backstage/plugin-search-backend-module-explore` + */ + explore?: { + /** + * The schedule for how often to run the collation job. + */ + schedule?: TaskScheduleDefinitionConfig; + }; + }; + }; +} diff --git a/plugins/search-backend-module-explore/package.json b/plugins/search-backend-module-explore/package.json index 13c6ce474c..ec11f60795 100644 --- a/plugins/search-backend-module-explore/package.json +++ b/plugins/search-backend-module-explore/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-explore", "description": "A module for the search backend that exports explore modules", - "version": "0.1.4-next.1", + "version": "0.1.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -58,6 +58,8 @@ "msw": "^1.2.1" }, "files": [ - "dist" - ] + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" } diff --git a/plugins/search-backend-module-explore/src/alpha.test.ts b/plugins/search-backend-module-explore/src/alpha.test.ts index 9b133f0fd5..fa6ab8a909 100644 --- a/plugins/search-backend-module-explore/src/alpha.test.ts +++ b/plugins/search-backend-module-explore/src/alpha.test.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { startTestBackend } from '@backstage/backend-test-utils'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; -import { searchModuleExploreCollator } from './alpha'; +import searchModuleExploreCollator from './alpha'; describe('searchModuleExploreCollator', () => { const schedule = { @@ -34,7 +34,18 @@ describe('searchModuleExploreCollator', () => { extensionPoints: [ [searchIndexRegistryExtensionPoint, extensionPointMock], ], - features: [searchModuleExploreCollator({ schedule })], + features: [ + searchModuleExploreCollator(), + mockServices.rootConfig.factory({ + data: { + search: { + explore: { + schedule, + }, + }, + }, + }), + ], }); expect(extensionPointMock.addCollator).toHaveBeenCalledTimes(1); diff --git a/plugins/search-backend-module-explore/src/alpha.ts b/plugins/search-backend-module-explore/src/alpha.ts index fadbcda3a5..e8e3361e1b 100644 --- a/plugins/search-backend-module-explore/src/alpha.ts +++ b/plugins/search-backend-module-explore/src/alpha.ts @@ -23,66 +23,59 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { loggerToWinstonLogger } from '@backstage/backend-common'; import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; import { ToolDocumentCollatorFactory } from '@backstage/plugin-search-backend-module-explore'; - -/** - * Options for {@link searchModuleExploreCollator}. - * - * @alpha - */ -export type SearchModuleExploreCollatorOptions = { - schedule?: TaskScheduleDefinition; -}; +import { readTaskScheduleDefinitionFromConfig } from '@backstage/backend-tasks'; /** * Search backend module for the Explore index. * * @alpha */ -export const searchModuleExploreCollator = createBackendModule( - (options?: SearchModuleExploreCollatorOptions) => ({ - moduleId: 'exploreCollator', - pluginId: 'search', - register(env) { - env.registerInit({ - deps: { - config: coreServices.rootConfig, - logger: coreServices.logger, - discovery: coreServices.discovery, - scheduler: coreServices.scheduler, - tokenManager: coreServices.tokenManager, - indexRegistry: searchIndexRegistryExtensionPoint, - }, - async init({ - config, - logger, - discovery, - scheduler, - indexRegistry, - tokenManager, - }) { - const defaultSchedule = { - frequency: { minutes: 10 }, - timeout: { minutes: 15 }, - initialDelay: { seconds: 3 }, - }; +export default createBackendModule({ + moduleId: 'exploreCollator', + pluginId: 'search', + register(env) { + env.registerInit({ + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, + discovery: coreServices.discovery, + scheduler: coreServices.scheduler, + tokenManager: coreServices.tokenManager, + indexRegistry: searchIndexRegistryExtensionPoint, + }, + async init({ + config, + logger, + discovery, + scheduler, + indexRegistry, + tokenManager, + }) { + const defaultSchedule = { + frequency: { minutes: 10 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 3 }, + }; - indexRegistry.addCollator({ - schedule: scheduler.createScheduledTaskRunner( - options?.schedule ?? defaultSchedule, - ), - factory: ToolDocumentCollatorFactory.fromConfig(config, { - discovery, - logger: loggerToWinstonLogger(logger), - tokenManager, - }), - }); - }, - }); - }, - }), -); + const schedule = config.has('search.collators.explore.schedule') + ? readTaskScheduleDefinitionFromConfig( + config.getConfig('search.collators.explore.schedule'), + ) + : defaultSchedule; + + indexRegistry.addCollator({ + schedule: scheduler.createScheduledTaskRunner(schedule), + factory: ToolDocumentCollatorFactory.fromConfig(config, { + discovery, + logger: loggerToWinstonLogger(logger), + tokenManager, + }), + }); + }, + }); + }, +}); diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index 3bc95aaa2d..ea6c62d47c 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-search-backend-module-pg +## 0.5.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/plugin-search-common@1.2.6-next.1 + +## 0.5.12-next.1 + +### Patch Changes + +- 4ccf9204bc95: Added `indexerBatchSize` option to be able to control the size of the batches being indexed. Also added a debug log entry to list out all the entities in the batch +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## 0.5.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## 0.5.9 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-search-backend-node@1.2.4 + - @backstage/config@1.0.8 + - @backstage/plugin-search-common@1.2.5 + +## 0.5.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-search-backend-node@1.2.4-next.2 + ## 0.5.9-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-pg/alpha-api-report.md b/plugins/search-backend-module-pg/alpha-api-report.md index eaee78e944..b021f6884f 100644 --- a/plugins/search-backend-module-pg/alpha-api-report.md +++ b/plugins/search-backend-module-pg/alpha-api-report.md @@ -6,7 +6,8 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; // @alpha -export const searchModulePostgresEngine: () => BackendFeature; +const _default: () => BackendFeature; +export default _default; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/search-backend-module-pg/catalog-info.yaml b/plugins/search-backend-module-pg/catalog-info.yaml new file mode 100644 index 0000000000..47b9717882 --- /dev/null +++ b/plugins/search-backend-module-pg/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-backend-module-pg + title: '@backstage/plugin-search-backend-module-pg' + description: A module for the search backend that implements search using PostgreSQL +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: discoverability-maintainers diff --git a/plugins/search-backend-module-pg/config.d.ts b/plugins/search-backend-module-pg/config.d.ts index 7367b770bc..3039cd1d28 100644 --- a/plugins/search-backend-module-pg/config.d.ts +++ b/plugins/search-backend-module-pg/config.d.ts @@ -57,6 +57,10 @@ export interface Config { */ fragmentDelimiter?: string; }; + /** + * Batch size to use when indexing + */ + indexerBatchSize?: number; }; }; } diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 63c8061fea..5d0137a60f 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-pg", "description": "A module for the search backend that implements search using PostgreSQL", - "version": "0.5.9-next.1", + "version": "0.5.12-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts index d2cf80e0ca..1a27b9f981 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts @@ -86,6 +86,7 @@ export type PgSearchHighlightOptions = { export class PgSearchEngine implements SearchEngine { private readonly logger?: Logger; private readonly highlightOptions: PgSearchHighlightOptions; + private readonly indexerBatchSize: number; /** * @deprecated This will be marked as private in a future release, please us fromConfig instead @@ -114,6 +115,8 @@ export class PgSearchEngine implements SearchEngine { highlightConfig?.getOptionalString('fragmentDelimiter') ?? ' ... ', }; this.highlightOptions = highlightOptions; + this.indexerBatchSize = + config.getOptionalNumber('search.pg.indexerBatchSize') ?? 1000; this.logger = logger; } @@ -178,7 +181,7 @@ export class PgSearchEngine implements SearchEngine { async getIndexer(type: string) { return new PgSearchEngineIndexer({ - batchSize: 1000, + batchSize: this.indexerBatchSize, type, databaseStore: this.databaseStore, logger: this.logger?.child({ documentType: type }), diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts index 47ea812a92..9fe0e7091c 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts @@ -59,6 +59,11 @@ export class PgSearchEngineIndexer extends BatchSearchEngineIndexer { async index(documents: IndexableDocument[]): Promise { this.numRecords += documents.length; + const refs = [...new Set(documents.map(d => d.authorization?.resourceRef))]; + this.logger.debug( + `Attempting to index the following entities: ${refs.toString()}`, + ); + try { await this.store.insertDocuments(this.tx!, this.type, documents); } catch (e) { diff --git a/plugins/search-backend-module-pg/src/alpha.ts b/plugins/search-backend-module-pg/src/alpha.ts index 71d62e7927..4052b56cb3 100644 --- a/plugins/search-backend-module-pg/src/alpha.ts +++ b/plugins/search-backend-module-pg/src/alpha.ts @@ -24,7 +24,7 @@ import { PgSearchEngine } from './PgSearchEngine'; * @alpha * Search backend module for the Postgres engine. */ -export const searchModulePostgresEngine = createBackendModule({ +export default createBackendModule({ moduleId: 'postgresEngine', pluginId: 'search', register(env) { diff --git a/plugins/search-backend-module-techdocs/CHANGELOG.md b/plugins/search-backend-module-techdocs/CHANGELOG.md index 6fc2307106..8ee45e6bbf 100644 --- a/plugins/search-backend-module-techdocs/CHANGELOG.md +++ b/plugins/search-backend-module-techdocs/CHANGELOG.md @@ -1,5 +1,94 @@ # @backstage/plugin-search-backend-module-techdocs +## 0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-catalog-node@1.4.4-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/catalog-model@1.4.2-next.1 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/plugin-techdocs-node@1.8.0-next.2 + - @backstage/catalog-client@1.4.4-next.1 + - @backstage/plugin-catalog-common@1.0.16-next.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## 0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/plugin-techdocs-node@1.8.0-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/catalog-model@1.4.2-next.0 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-search-backend-node@1.2.7-next.1 + - @backstage/plugin-catalog-node@1.4.4-next.1 + - @backstage/catalog-client@1.4.4-next.0 + - @backstage/plugin-catalog-common@1.0.16-next.0 + - @backstage/plugin-search-common@1.2.6-next.0 + +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-catalog-node@1.4.3-next.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + - @backstage/plugin-techdocs-node@1.7.6-next.0 + +## 0.1.4 + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 29f77f923c71: Ensure that all services are dependency injected into the module instead of taken from options +- 12a8c94eda8d: Add package repository and homepage metadata +- 6694e79ab396: **BREAKING:** Moved `schedule` & `collators` settings from module options into app-config for the new backend system. You can now pass in a `TaskScheduleDefinitionConfig` through the `search.collators.techdocs.schedule` configuration key & configure the `TechDocsCollatorFactory` with the key `search.collators.techdocs`. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/plugin-search-backend-node@1.2.4 + - @backstage/plugin-catalog-node@1.4.1 + - @backstage/backend-tasks@0.5.5 + - @backstage/plugin-techdocs-node@1.7.4 + - @backstage/catalog-client@1.4.3 + - @backstage/catalog-model@1.4.1 + - @backstage/config@1.0.8 + - @backstage/plugin-catalog-common@1.0.15 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-common@1.2.5 + +## 0.1.4-next.2 + +### Patch Changes + +- 29f77f923c71: Ensure that all services are dependency injected into the module instead of taken from options +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-catalog-node@1.4.1-next.2 + - @backstage/plugin-search-backend-node@1.2.4-next.2 + - @backstage/plugin-techdocs-node@1.7.4-next.2 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-techdocs/README.md b/plugins/search-backend-module-techdocs/README.md index 5de310466b..9ffdcb7f55 100644 --- a/plugins/search-backend-module-techdocs/README.md +++ b/plugins/search-backend-module-techdocs/README.md @@ -2,14 +2,21 @@ > DISCLAIMER: The new backend system is in alpha, and so are the search backend module support for the new backend system. We don't recommend you to migrate your backend installations to the new system yet. But if you want to experiment, you can find getting started guides below. -This package exports techdocs backend modules responsible for extending search. +This package exports a module that extends the search backend to also index techdocs. -## Example +## Installation -### Use default schedule +Add the module package as a dependency: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-search-backend-module-techdocs +``` + +Add the collator to your backend instance, along with the search plugin itself: ```tsx -// packages/backend-next/src/index.ts +// packages/backend/src/index.ts import { createBackend } from '@backstage/backend-defaults'; import { searchPlugin } from '@backstage/plugin-search-backend/alpha'; import { searchModuleTechDocsCollator } from '@backstage/plugin-search-backend-module-techdocs/alpha'; @@ -20,22 +27,4 @@ backend.add(searchModuleTechDocsCollator()); backend.start(); ``` -### Use custom schedule - -```tsx -// packages/backend-next/src/index.ts -import { createBackend } from '@backstage/backend-defaults'; -import { searchPlugin } from '@backstage/plugin-search-backend/alpha'; -import { searchModuleTechDocsCollator } from '@backstage/plugin-search-backend-module-techdocs/alpha'; - -const schedule = { - frequency: { minutes: 10 }, - timeout: { minutes: 15 }, - initialDelay: { seconds: 3 }, -}; - -const backend = createBackend(); -backend.add(searchPlugin()); -backend.add(searchModuleTechDocsCollator({ schedule })); -backend.start(); -``` +You may also want to add configuration parameters to your app-config, for example for controlling the scheduled indexing interval. These parameters should be placed under the `search.collators.techdocs` key. See [the config definition file](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-techdocs/config.d.ts) for more details. diff --git a/plugins/search-backend-module-techdocs/alpha-api-report.md b/plugins/search-backend-module-techdocs/alpha-api-report.md index 0eac010b4c..997d4a68db 100644 --- a/plugins/search-backend-module-techdocs/alpha-api-report.md +++ b/plugins/search-backend-module-techdocs/alpha-api-report.md @@ -4,21 +4,10 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; -import { TaskScheduleDefinition } from '@backstage/backend-tasks'; -import { TechDocsCollatorFactoryOptions } from '@backstage/plugin-search-backend-module-techdocs'; // @alpha -export const searchModuleTechDocsCollator: ( - options?: SearchModuleTechDocsCollatorOptions | undefined, -) => BackendFeature; - -// @alpha -export type SearchModuleTechDocsCollatorOptions = Omit< - TechDocsCollatorFactoryOptions, - 'logger' | 'discovery' | 'tokenManager' | 'catalogClient' -> & { - schedule?: TaskScheduleDefinition; -}; +const _default: () => BackendFeature; +export default _default; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/search-backend-module-techdocs/catalog-info.yaml b/plugins/search-backend-module-techdocs/catalog-info.yaml new file mode 100644 index 0000000000..a0bbfc5bb7 --- /dev/null +++ b/plugins/search-backend-module-techdocs/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-backend-module-techdocs + title: '@backstage/plugin-search-backend-module-techdocs' + description: A module for the search backend that exports techdocs modules +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: discoverability-maintainers diff --git a/plugins/search-backend-module-techdocs/config.d.ts b/plugins/search-backend-module-techdocs/config.d.ts new file mode 100644 index 0000000000..630bfc3692 --- /dev/null +++ b/plugins/search-backend-module-techdocs/config.d.ts @@ -0,0 +1,53 @@ +/* + * 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 { TaskScheduleDefinitionConfig } from '@backstage/backend-tasks'; + +export interface Config { + search?: { + collators?: { + /** + * Configuration options for `@backstage/plugin-search-backend-module-techdocs` + */ + techdocs?: { + /** + * The schedule for how often to run the collation job. + */ + schedule?: TaskScheduleDefinitionConfig; + /** + * A templating string with placeholders, to form the final location of + * the entity. + * + * Defaults to `'/docs/:namespace/:kind/:name/:path'`. + */ + locationTemplate?: string; + /** + * An abstract value that controls the concurrency level of the + * collation process. Increasing this value will both increase the + * number of entities fetched at a time from the catalog, as well as how + * many things are being processed concurrently. + * + * Defaults to `10`. + */ + parallelismLimit?: number; + /** + * Defaults to `false`. + */ + legacyPathCasing?: boolean; + }; + }; + }; +} diff --git a/plugins/search-backend-module-techdocs/package.json b/plugins/search-backend-module-techdocs/package.json index ffc9d0ff4f..3af9ae2f70 100644 --- a/plugins/search-backend-module-techdocs/package.json +++ b/plugins/search-backend-module-techdocs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-techdocs", "description": "A module for the search backend that exports techdocs modules", - "version": "0.1.4-next.1", + "version": "0.1.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -65,6 +65,8 @@ "msw": "^1.0.0" }, "files": [ - "dist" - ] + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" } diff --git a/plugins/search-backend-module-techdocs/src/alpha.test.ts b/plugins/search-backend-module-techdocs/src/alpha.test.ts index 011b00b12c..a278614b75 100644 --- a/plugins/search-backend-module-techdocs/src/alpha.test.ts +++ b/plugins/search-backend-module-techdocs/src/alpha.test.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { startTestBackend } from '@backstage/backend-test-utils'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; -import { searchModuleTechDocsCollator } from './alpha'; +import searchModuleTechDocsCollator from './alpha'; describe('searchModuleTechDocsCollator', () => { const schedule = { @@ -34,7 +34,18 @@ describe('searchModuleTechDocsCollator', () => { extensionPoints: [ [searchIndexRegistryExtensionPoint, extensionPointMock], ], - features: [searchModuleTechDocsCollator({ schedule })], + features: [ + searchModuleTechDocsCollator(), + mockServices.rootConfig.factory({ + data: { + search: { + techdocs: { + schedule, + }, + }, + }, + }), + ], }); expect(extensionPointMock.addCollator).toHaveBeenCalledTimes(1); diff --git a/plugins/search-backend-module-techdocs/src/alpha.ts b/plugins/search-backend-module-techdocs/src/alpha.ts index 5d82b6804f..1c4988cc21 100644 --- a/plugins/search-backend-module-techdocs/src/alpha.ts +++ b/plugins/search-backend-module-techdocs/src/alpha.ts @@ -19,79 +19,65 @@ * A module for the search backend that exports TechDocs modules. */ +import { loggerToWinstonLogger } from '@backstage/backend-common'; import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { TaskScheduleDefinition } from '@backstage/backend-tasks'; -import { loggerToWinstonLogger } from '@backstage/backend-common'; -import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; +import { readTaskScheduleDefinitionFromConfig } from '@backstage/backend-tasks'; import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; - -import { - DefaultTechDocsCollatorFactory, - TechDocsCollatorFactoryOptions, -} from '@backstage/plugin-search-backend-module-techdocs'; - -/** - * @alpha - * Options for {@link searchModuleTechDocsCollator}. - */ -export type SearchModuleTechDocsCollatorOptions = Omit< - TechDocsCollatorFactoryOptions, - 'logger' | 'discovery' | 'tokenManager' | 'catalogClient' -> & { - schedule?: TaskScheduleDefinition; -}; +import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-search-backend-module-techdocs'; +import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; /** * @alpha * Search backend module for the TechDocs index. */ -export const searchModuleTechDocsCollator = createBackendModule( - (options?: SearchModuleTechDocsCollatorOptions) => ({ - moduleId: 'techDocsCollator', - pluginId: 'search', - register(env) { - env.registerInit({ - deps: { - config: coreServices.rootConfig, - logger: coreServices.logger, - discovery: coreServices.discovery, - tokenManager: coreServices.tokenManager, - scheduler: coreServices.scheduler, - catalog: catalogServiceRef, - indexRegistry: searchIndexRegistryExtensionPoint, - }, - async init({ - config, - logger, - discovery, - tokenManager, - scheduler, - catalog, - indexRegistry, - }) { - const defaultSchedule = { - frequency: { minutes: 10 }, - timeout: { minutes: 15 }, - initialDelay: { seconds: 3 }, - }; +export default createBackendModule({ + moduleId: 'techDocsCollator', + pluginId: 'search', + register(env) { + env.registerInit({ + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, + discovery: coreServices.discovery, + tokenManager: coreServices.tokenManager, + scheduler: coreServices.scheduler, + catalog: catalogServiceRef, + indexRegistry: searchIndexRegistryExtensionPoint, + }, + async init({ + config, + logger, + discovery, + tokenManager, + scheduler, + catalog, + indexRegistry, + }) { + const defaultSchedule = { + frequency: { minutes: 10 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 3 }, + }; - indexRegistry.addCollator({ - schedule: scheduler.createScheduledTaskRunner( - options?.schedule ?? defaultSchedule, - ), - factory: DefaultTechDocsCollatorFactory.fromConfig(config, { - ...options, - discovery, - tokenManager, - logger: loggerToWinstonLogger(logger), - catalogClient: catalog, - }), - }); - }, - }); - }, - }), -); + const schedule = config.has('search.collators.techdocs.schedule') + ? readTaskScheduleDefinitionFromConfig( + config.getConfig('search.collators.techdocs.schedule'), + ) + : defaultSchedule; + + indexRegistry.addCollator({ + schedule: scheduler.createScheduledTaskRunner(schedule), + factory: DefaultTechDocsCollatorFactory.fromConfig(config, { + discovery, + tokenManager, + logger: loggerToWinstonLogger(logger), + catalogClient: catalog, + }), + }); + }, + }); + }, +}); diff --git a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts index d9329151c6..319b8fb446 100644 --- a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts +++ b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts @@ -174,10 +174,15 @@ describe('DefaultTechDocsCollatorFactory', () => { it('maps a returned entity with a custom locationTemplate', async () => { // Provide an alternate location template. - factory = DefaultTechDocsCollatorFactory.fromConfig(config, { + const _config = new ConfigReader({ + ...config.get(), + search: { + collators: { techdocs: { locationTemplate: '/software/:name' } }, + }, + }); + factory = DefaultTechDocsCollatorFactory.fromConfig(_config, { discovery: mockDiscoveryApi, tokenManager: mockTokenManager, - locationTemplate: '/software/:name', logger, }); collator = await factory.getCollator(); @@ -194,10 +199,17 @@ describe('DefaultTechDocsCollatorFactory', () => { // A parallelismLimit of 1 is a catalog limit of 50 per request. Code // above in the /entities handler ensures valid entities are only // returned on the second page. - factory = DefaultTechDocsCollatorFactory.fromConfig(config, { - ...options, - parallelismLimit: 1, + const _config = new ConfigReader({ + ...config.get(), + search: { + collators: { + techdocs: { + parallelismLimit: 1, + }, + }, + }, }); + factory = DefaultTechDocsCollatorFactory.fromConfig(_config, options); collator = await factory.getCollator(); const pipeline = TestPipeline.fromCollator(collator); @@ -205,6 +217,9 @@ describe('DefaultTechDocsCollatorFactory', () => { // Only 1 entity with TechDocs configured multiplied by 3 pages. expect(documents).toHaveLength(3); + expect(_config.get('search.collators.techdocs.parallelismLimit')).toEqual( + 1, + ); }); describe('with legacyPathCasing configuration', () => { diff --git a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts index e2c6f5913c..051dc93189 100644 --- a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts +++ b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts @@ -104,7 +104,18 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { config.getOptionalBoolean( 'techdocs.legacyUseCaseSensitiveTripletPaths', ) || false; - return new DefaultTechDocsCollatorFactory({ ...options, legacyPathCasing }); + const locationTemplate = config.getOptionalString( + 'search.collators.techdocs.locationTemplate', + ); + const parallelismLimit = config.getOptionalNumber( + 'search.collators.techdocs.parallelismLimit', + ); + return new DefaultTechDocsCollatorFactory({ + ...options, + locationTemplate, + parallelismLimit, + legacyPathCasing, + }); } async getCollator(): Promise { diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index a8b8b930ee..b8d902d729 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,68 @@ # @backstage/plugin-search-backend-node +## 1.2.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-tasks@0.5.8-next.2 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/errors@1.2.1 + - @backstage/plugin-search-common@1.2.6-next.1 + +## 1.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-tasks@0.5.8-next.1 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/errors@1.2.1 + - @backstage/plugin-search-common@1.2.6-next.0 + +## 1.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-tasks@0.5.7-next.0 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-common@1.2.5 + +## 1.2.4 + +### Patch Changes + +- 12a8c94eda8d: Add package repository and homepage metadata +- e3e9bc10298b: Fix Lunr search engine highlight by ignoring invalid metadata positions. +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/backend-tasks@0.5.5 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-common@1.2.5 + +## 1.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-tasks@0.5.5-next.2 + - @backstage/backend-common@0.19.2-next.2 + ## 1.2.4-next.1 ### Patch Changes diff --git a/plugins/search-backend-node/catalog-info.yaml b/plugins/search-backend-node/catalog-info.yaml new file mode 100644 index 0000000000..5f6b36256f --- /dev/null +++ b/plugins/search-backend-node/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-backend-node + title: '@backstage/plugin-search-backend-node' + description: >- + A library for Backstage backend plugins that want to interact with the + search backend plugin +spec: + lifecycle: production + type: backstage-node-library + owner: discoverability-maintainers diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 842c636bba..0a6379d529 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-node", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", - "version": "1.2.4-next.1", + "version": "1.2.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index db52ff9e55..553fd3319d 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,91 @@ # @backstage/plugin-search-backend +## 1.4.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/backend-common@0.19.5-next.2 + - @backstage/plugin-auth-node@0.3.0-next.2 + - @backstage/plugin-permission-node@0.7.14-next.2 + - @backstage/backend-plugin-api@0.6.3-next.2 + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.2 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-search-common@1.2.6-next.1 + +## 1.4.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/backend-common@0.19.5-next.1 + - @backstage/backend-plugin-api@0.6.3-next.1 + - @backstage/plugin-auth-node@0.3.0-next.1 + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/plugin-permission-node@0.7.14-next.1 + - @backstage/plugin-search-backend-node@1.2.7-next.1 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-search-common@1.2.6-next.0 + +## 1.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.3.0-next.0 + - @backstage/backend-common@0.19.4-next.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/backend-plugin-api@0.6.2-next.0 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-permission-node@0.7.13-next.0 + - @backstage/plugin-search-backend-node@1.2.6-next.0 + - @backstage/plugin-search-common@1.2.5 + +## 1.4.0 + +### Minor Changes + +- ebeb77586975: Now performs request validation based on OpenAPI schema through `@backstage/backend-openapi-utils`. Error responses for invalid input, like `"a"` instead of a number, may have changed. + +### Patch Changes + +- 629cbd194a87: Use `coreServices.rootConfig` instead of `coreService.config` +- 12a8c94eda8d: Add package repository and homepage metadata +- 951ab6c9db58: Add missing `configSchema` to package.json +- Updated dependencies + - @backstage/backend-common@0.19.2 + - @backstage/backend-plugin-api@0.6.0 + - @backstage/backend-openapi-utils@0.0.3 + - @backstage/plugin-search-backend-node@1.2.4 + - @backstage/plugin-auth-node@0.2.17 + - @backstage/plugin-permission-node@0.7.11 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + - @backstage/plugin-permission-common@0.7.7 + - @backstage/plugin-search-common@1.2.5 + +## 1.4.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.0-next.2 + - @backstage/backend-common@0.19.2-next.2 + - @backstage/plugin-permission-node@0.7.11-next.2 + - @backstage/plugin-search-backend-node@1.2.4-next.2 + - @backstage/plugin-auth-node@0.2.17-next.2 + ## 1.4.0-next.1 ### Patch Changes diff --git a/plugins/search-backend/alpha-api-report.md b/plugins/search-backend/alpha-api-report.md index 6ee94a0e29..548f4c6608 100644 --- a/plugins/search-backend/alpha-api-report.md +++ b/plugins/search-backend/alpha-api-report.md @@ -6,7 +6,8 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; // @alpha -export const searchPlugin: () => BackendFeature; +const _default: () => BackendFeature; +export default _default; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/search-backend/catalog-info.yaml b/plugins/search-backend/catalog-info.yaml new file mode 100644 index 0000000000..2b8c7ee346 --- /dev/null +++ b/plugins/search-backend/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-backend + title: '@backstage/plugin-search-backend' + description: The Backstage backend plugin that provides your backstage app with search +spec: + lifecycle: production + type: backstage-backend-plugin + owner: discoverability-maintainers diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 167e4d835a..85d4ae8da2 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend", "description": "The Backstage backend plugin that provides your backstage app with search", - "version": "1.4.0-next.1", + "version": "1.4.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend/src/alpha.test.ts b/plugins/search-backend/src/alpha.test.ts index 527f099236..a18ab7e104 100644 --- a/plugins/search-backend/src/alpha.test.ts +++ b/plugins/search-backend/src/alpha.test.ts @@ -16,7 +16,7 @@ import { startTestBackend } from '@backstage/backend-test-utils'; import request from 'supertest'; -import { searchPlugin } from './alpha'; +import searchPlugin from './alpha'; describe('searchPlugin', () => { it('should serve search results on query endpoint', async () => { diff --git a/plugins/search-backend/src/alpha.ts b/plugins/search-backend/src/alpha.ts index 59394ab7a6..8a9d4498bc 100644 --- a/plugins/search-backend/src/alpha.ts +++ b/plugins/search-backend/src/alpha.ts @@ -75,7 +75,7 @@ class SearchEngineRegistry implements SearchEngineRegistryExtensionPoint { * The Search plugin is responsible for starting search indexing processes and return search results. * @alpha */ -export const searchPlugin = createBackendPlugin({ +export default createBackendPlugin({ pluginId: 'search', register(env) { const searchIndexRegistry = new SearchIndexRegistry(); diff --git a/plugins/search-common/CHANGELOG.md b/plugins/search-common/CHANGELOG.md index f4f007ca57..0f01e01dc3 100644 --- a/plugins/search-common/CHANGELOG.md +++ b/plugins/search-common/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-search-common +## 1.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.8-next.1 + - @backstage/types@1.1.0 + +## 1.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.8-next.0 + - @backstage/types@1.1.0 + ## 1.2.5 ### Patch Changes diff --git a/plugins/search-common/catalog-info.yaml b/plugins/search-common/catalog-info.yaml new file mode 100644 index 0000000000..d7e77a8d10 --- /dev/null +++ b/plugins/search-common/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-common + title: '@backstage/plugin-search-common' + description: >- + Common functionalities for Search, to be shared between various + search-enabled plugins +spec: + lifecycle: production + type: backstage-common-library + owner: discoverability-maintainers diff --git a/plugins/search-common/package.json b/plugins/search-common/package.json index d8b7e7bef9..d34549b4de 100644 --- a/plugins/search-common/package.json +++ b/plugins/search-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-common", "description": "Common functionalities for Search, to be shared between various search-enabled plugins", - "version": "1.2.5", + "version": "1.2.6-next.1", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -26,6 +26,7 @@ "files": [ "dist" ], + "sideEffects": false, "scripts": { "build": "backstage-cli package build", "lint": "backstage-cli package lint", diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md index 136a7ad4a4..7056b00fc6 100644 --- a/plugins/search-react/CHANGELOG.md +++ b/plugins/search-react/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-search-react +## 1.7.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.5-next.2 + - @backstage/core-plugin-api@1.6.0-next.2 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-search-common@1.2.6-next.1 + +## 1.7.0-next.1 + +### Minor Changes + +- b78f570f44d3: The SearchPage component can now be configured via app-config.yaml with default query parameters to define how it behaves when it is first loaded or reset. Check out the following example: + + ```yaml + search: + query: + pageLimit: 50 + ``` + + Acceptable values for `pageLimit` are `10`, `25`, `50` or `100`. + +### Patch Changes + +- 45f8a95e1068: Optionally initializes the search context with default settings for search queries only when the config is defined, rather than always overriding it. +- Updated dependencies + - @backstage/core-components@0.13.5-next.1 + - @backstage/core-plugin-api@1.6.0-next.1 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-search-common@1.2.6-next.0 + +## 1.6.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.6.0-next.0 + - @backstage/core-components@0.13.5-next.0 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-search-common@1.2.5 + +## 1.6.4 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.13.4 + - @backstage/core-plugin-api@1.5.3 + - @backstage/theme@0.4.1 + - @backstage/types@1.1.0 + - @backstage/version-bridge@1.0.4 + - @backstage/plugin-search-common@1.2.5 + ## 1.6.4-next.0 ### Patch Changes diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 8afde8c4a5..97c8300c14 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -31,10 +31,12 @@ import { TypographyProps } from '@material-ui/core'; // @public (undocumented) export const AutocompleteFilter: ( props: SearchAutocompleteFilterProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) -export const CheckboxFilter: (props: SearchFilterComponentProps) => JSX.Element; +export const CheckboxFilter: ( + props: SearchFilterComponentProps, +) => React_2.JSX.Element; // @public export const createSearchResultListItemExtension: < @@ -46,7 +48,7 @@ export const createSearchResultListItemExtension: < // @public (undocumented) export const DefaultResultListItem: ( props: DefaultResultListItemProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type DefaultResultListItemProps = { @@ -62,7 +64,7 @@ export type DefaultResultListItemProps = { // @public (undocumented) export const HighlightedSearchResultText: ( props: HighlightedSearchResultTextProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type HighlightedSearchResultTextProps = { @@ -100,7 +102,7 @@ export type SearchAutocompleteComponent =