diff --git a/.changeset/brave-bugs-know.md b/.changeset/brave-bugs-know.md deleted file mode 100644 index 9c0ae9edf4..0000000000 --- a/.changeset/brave-bugs-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Creates a plugin that redirects from the Home page to the Catalog index page to avoid seeing a not found page error when starting the app. diff --git a/.changeset/busy-chairs-itch.md b/.changeset/busy-chairs-itch.md deleted file mode 100644 index ef8b0b8df4..0000000000 --- a/.changeset/busy-chairs-itch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Add missing class for flex: baseline diff --git a/.changeset/cold-donuts-train.md b/.changeset/cold-donuts-train.md deleted file mode 100644 index fe84653601..0000000000 --- a/.changeset/cold-donuts-train.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Fix a potential race condition in EntityListProvider when selecting filters diff --git a/.changeset/cool-games-rescue.md b/.changeset/cool-games-rescue.md deleted file mode 100644 index 3bed707eb7..0000000000 --- a/.changeset/cool-games-rescue.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Fixed the `new-frontend-plugin` template that was incorrectly passing `id` instead of `pluginId` to `createFrontendPlugin` and unnecessarily importing `React`. diff --git a/.changeset/curvy-sites-rhyme.md b/.changeset/curvy-sites-rhyme.md deleted file mode 100644 index 1de9a21e77..0000000000 --- a/.changeset/curvy-sites-rhyme.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -The 'get-catalog-entity' action now throws a ConflictError instead of generic Error if multiple entities are found, so MCP call doesn't fail with 500. diff --git a/.changeset/easy-wings-turn.md b/.changeset/easy-wings-turn.md deleted file mode 100644 index 3ce4e76449..0000000000 --- a/.changeset/easy-wings-turn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-org': patch ---- - -Fixing issue with extra slash in the routing diff --git a/.changeset/fine-hands-think.md b/.changeset/fine-hands-think.md deleted file mode 100644 index 0fa99c904c..0000000000 --- a/.changeset/fine-hands-think.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/frontend-plugin-api': patch ---- - -Improved the types of `createFrontendPlugin` and `createFrontendModule` so that errors due to incompatible options are indicated more clearly. diff --git a/.changeset/fix-select-aria-props.md b/.changeset/fix-select-aria-props.md deleted file mode 100644 index 66bb1de3ad..0000000000 --- a/.changeset/fix-select-aria-props.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Fix Select component to properly attach aria-label and aria-labelledby props to the rendered element for improved accessibility. diff --git a/.changeset/flat-colts-know.md b/.changeset/flat-colts-know.md deleted file mode 100644 index e79ba52ae8..0000000000 --- a/.changeset/flat-colts-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs-backend': minor ---- - -Techdocs: support HumanDuration for cache TTL and timeout configuration diff --git a/.changeset/giant-zebras-peel.md b/.changeset/giant-zebras-peel.md deleted file mode 100644 index c08caaddb7..0000000000 --- a/.changeset/giant-zebras-peel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-test-utils': minor ---- - -The `mockServices.rootConfig()` instance now has an `update` method that can be used to test configuration subscriptions and updates. diff --git a/.changeset/hungry-crews-fetch.md b/.changeset/hungry-crews-fetch.md new file mode 100644 index 0000000000..a9728e9cb0 --- /dev/null +++ b/.changeset/hungry-crews-fetch.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fixed module federation config by only setting `import: false` on shared libraries for remote. diff --git a/.changeset/icy-camels-throw.md b/.changeset/icy-camels-throw.md deleted file mode 100644 index db0521711c..0000000000 --- a/.changeset/icy-camels-throw.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-mcp-actions-backend': patch ---- - -The MCP backend will now convert known Backstage errors into textual responses with `isError: true`. -The error message can be useful for an LLM to understand and maybe give back to the user. -Previously all errors where thrown out to `@modelcontextprotocol/sdk` which causes a generic 500. diff --git a/.changeset/kind-places-reply.md b/.changeset/kind-places-reply.md new file mode 100644 index 0000000000..4b1d361e6e --- /dev/null +++ b/.changeset/kind-places-reply.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-common': patch +--- + +Make SERVICEACCOUNT_CA_PATH public so it can be imported by external modules. diff --git a/.changeset/legal-lemons-attend.md b/.changeset/legal-lemons-attend.md deleted file mode 100644 index 4b497d8620..0000000000 --- a/.changeset/legal-lemons-attend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/integration': minor ---- - -Added support for limiting GithubAppCredentialsMux to specific apps diff --git a/.changeset/lemon-terms-cheer.md b/.changeset/lemon-terms-cheer.md deleted file mode 100644 index c2f93d93fb..0000000000 --- a/.changeset/lemon-terms-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search-backend-module-explore': patch ---- - -Deprecate and mark explore collator as moved diff --git a/.changeset/lucky-glasses-slide.md b/.changeset/lucky-glasses-slide.md deleted file mode 100644 index 9b56901919..0000000000 --- a/.changeset/lucky-glasses-slide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Fixed an issue that could cause conflicts of detected modules in workspaces with multiple apps. diff --git a/.changeset/mean-sites-cheer.md b/.changeset/mean-sites-cheer.md deleted file mode 100644 index a35dfb8c33..0000000000 --- a/.changeset/mean-sites-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-gitlab': patch ---- - -The log message now indicates that the pipeline trigger token was deleted and not pipeline itself. diff --git a/.changeset/olive-moons-burn.md b/.changeset/olive-moons-burn.md deleted file mode 100644 index 625d97c5a7..0000000000 --- a/.changeset/olive-moons-burn.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@backstage/catalog-client': minor -'@backstage/plugin-catalog-react': minor -'@backstage/plugin-catalog-node': minor ---- - -Introduced new `streamEntities` async generator method for the catalog. - -Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. -This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them -all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination -or fetch all entities at once. - -Example usage: - -```ts -const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); -for await (const page of pageStream) { - // Handle page of entities - for (const entity of page) { - console.log(entity); - } -} -``` diff --git a/.changeset/pre.json b/.changeset/pre.json index dcb0a48960..12e4285e7c 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -2,233 +2,208 @@ "mode": "pre", "tag": "next", "initialVersions": { - "example-app": "0.2.112", - "@backstage/app-defaults": "1.6.5", - "example-app-next": "0.0.26", - "app-next-example-plugin": "0.0.26", - "example-backend": "0.0.41", - "@backstage/backend-app-api": "1.2.6", - "@backstage/backend-defaults": "0.12.0", + "example-app": "0.2.113", + "@backstage/app-defaults": "1.7.0", + "example-app-next": "0.0.27", + "app-next-example-plugin": "0.0.27", + "example-backend": "0.0.42", + "@backstage/backend-app-api": "1.2.7", + "@backstage/backend-defaults": "0.12.1", "@backstage/backend-dev-utils": "0.1.5", - "@backstage/backend-dynamic-feature-service": "0.7.3", - "@backstage/backend-openapi-utils": "0.6.0", - "@backstage/backend-plugin-api": "1.4.2", - "@backstage/backend-test-utils": "1.8.0", - "@backstage/catalog-client": "1.11.0", + "@backstage/backend-dynamic-feature-service": "0.7.4", + "@backstage/backend-openapi-utils": "0.6.1", + "@backstage/backend-plugin-api": "1.4.3", + "@backstage/backend-test-utils": "1.9.0", + "@backstage/catalog-client": "1.12.0", "@backstage/catalog-model": "1.7.5", - "@backstage/cli": "0.34.0", + "@backstage/cli": "0.34.2", "@backstage/cli-common": "0.1.15", "@backstage/cli-node": "0.2.14", "@backstage/codemods": "0.1.52", "@backstage/config": "1.3.3", - "@backstage/config-loader": "1.10.2", - "@backstage/core-app-api": "1.18.0", - "@backstage/core-compat-api": "0.5.0", - "@backstage/core-components": "0.17.5", - "@backstage/core-plugin-api": "1.10.9", - "@backstage/create-app": "0.7.2", - "@backstage/dev-utils": "1.1.13", - "e2e-test": "0.2.31", + "@backstage/config-loader": "1.10.3", + "@backstage/core-app-api": "1.19.0", + "@backstage/core-compat-api": "0.5.2", + "@backstage/core-components": "0.18.0", + "@backstage/core-plugin-api": "1.11.0", + "@backstage/create-app": "0.7.4", + "@backstage/dev-utils": "1.1.14", + "e2e-test": "0.2.32", "@backstage/e2e-test-utils": "0.1.1", "@backstage/errors": "1.2.7", "@backstage/eslint-plugin": "0.1.11", - "@backstage/frontend-app-api": "0.12.0", - "@backstage/frontend-defaults": "0.3.0", - "@backstage/frontend-dynamic-feature-loader": "0.1.4", - "@internal/frontend": "0.0.12", - "@backstage/frontend-plugin-api": "0.11.0", - "@backstage/frontend-test-utils": "0.3.5", - "@backstage/integration": "1.17.1", + "@backstage/frontend-app-api": "0.13.0", + "@backstage/frontend-defaults": "0.3.1", + "@backstage/frontend-dynamic-feature-loader": "0.1.5", + "@internal/frontend": "0.0.13", + "@backstage/frontend-plugin-api": "0.12.0", + "@backstage/frontend-test-utils": "0.3.6", + "@backstage/integration": "1.18.0", "@backstage/integration-aws-node": "0.1.17", - "@backstage/integration-react": "1.2.9", + "@backstage/integration-react": "1.2.10", "@internal/opaque": "0.0.1", "@backstage/release-manifests": "0.0.13", - "@backstage/repo-tools": "0.15.1", - "@internal/scaffolder": "0.0.12", - "@techdocs/cli": "1.9.6", - "techdocs-cli-embedded-app": "0.2.111", + "@backstage/repo-tools": "0.15.2", + "@internal/scaffolder": "0.0.13", + "@techdocs/cli": "1.9.8", + "techdocs-cli-embedded-app": "0.2.112", "@backstage/test-utils": "1.7.11", "@backstage/theme": "0.6.8", - "@backstage/types": "1.2.1", - "@backstage/ui": "0.7.0", + "@backstage/types": "1.2.2", + "@backstage/ui": "0.7.1", "@backstage/version-bridge": "1.0.11", "yarn-plugin-backstage": "0.0.7", - "@backstage/plugin-api-docs": "0.12.10", + "@backstage/plugin-api-docs": "0.12.11", "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.10", - "@backstage/plugin-app": "0.2.0", - "@backstage/plugin-app-backend": "0.5.5", - "@backstage/plugin-app-node": "0.1.36", - "@backstage/plugin-app-visualizer": "0.1.22", - "@backstage/plugin-auth-backend": "0.25.3", - "@backstage/plugin-auth-backend-module-atlassian-provider": "0.4.6", - "@backstage/plugin-auth-backend-module-auth0-provider": "0.2.6", - "@backstage/plugin-auth-backend-module-aws-alb-provider": "0.4.6", - "@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.2.11", - "@backstage/plugin-auth-backend-module-bitbucket-provider": "0.3.6", - "@backstage/plugin-auth-backend-module-bitbucket-server-provider": "0.2.6", - "@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.4.6", - "@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.4.6", - "@backstage/plugin-auth-backend-module-github-provider": "0.3.6", - "@backstage/plugin-auth-backend-module-gitlab-provider": "0.3.6", - "@backstage/plugin-auth-backend-module-google-provider": "0.3.6", - "@backstage/plugin-auth-backend-module-guest-provider": "0.2.11", - "@backstage/plugin-auth-backend-module-microsoft-provider": "0.3.6", - "@backstage/plugin-auth-backend-module-oauth2-provider": "0.4.6", - "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.2.11", - "@backstage/plugin-auth-backend-module-oidc-provider": "0.4.6", - "@backstage/plugin-auth-backend-module-okta-provider": "0.2.6", - "@backstage/plugin-auth-backend-module-onelogin-provider": "0.3.6", - "@backstage/plugin-auth-backend-module-pinniped-provider": "0.3.6", - "@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.5.6", - "@backstage/plugin-auth-node": "0.6.6", - "@backstage/plugin-auth-react": "0.1.18", - "@backstage/plugin-bitbucket-cloud-common": "0.3.1", - "@backstage/plugin-catalog": "1.31.2", - "@backstage/plugin-catalog-backend": "3.0.1", - "@backstage/plugin-catalog-backend-module-aws": "0.4.14", - "@backstage/plugin-catalog-backend-module-azure": "0.3.8", - "@backstage/plugin-catalog-backend-module-backstage-openapi": "0.5.5", - "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.5.2", - "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.5.2", - "@backstage/plugin-catalog-backend-module-gcp": "0.3.11", - "@backstage/plugin-catalog-backend-module-gerrit": "0.3.5", - "@backstage/plugin-catalog-backend-module-gitea": "0.1.3", - "@backstage/plugin-catalog-backend-module-github": "0.10.2", - "@backstage/plugin-catalog-backend-module-github-org": "0.3.13", - "@backstage/plugin-catalog-backend-module-gitlab": "0.7.2", - "@backstage/plugin-catalog-backend-module-gitlab-org": "0.2.12", - "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.7.3", - "@backstage/plugin-catalog-backend-module-ldap": "0.11.8", - "@backstage/plugin-catalog-backend-module-logs": "0.1.13", - "@backstage/plugin-catalog-backend-module-msgraph": "0.7.3", - "@backstage/plugin-catalog-backend-module-openapi": "0.2.13", - "@backstage/plugin-catalog-backend-module-puppetdb": "0.2.13", - "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.2.11", - "@backstage/plugin-catalog-backend-module-unprocessed": "0.6.3", + "@backstage/plugin-app": "0.3.0", + "@backstage/plugin-app-backend": "0.5.6", + "@backstage/plugin-app-node": "0.1.37", + "@backstage/plugin-app-visualizer": "0.1.23", + "@backstage/plugin-auth": "0.1.0", + "@backstage/plugin-auth-backend": "0.25.4", + "@backstage/plugin-auth-backend-module-atlassian-provider": "0.4.7", + "@backstage/plugin-auth-backend-module-auth0-provider": "0.2.7", + "@backstage/plugin-auth-backend-module-aws-alb-provider": "0.4.7", + "@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.2.12", + "@backstage/plugin-auth-backend-module-bitbucket-provider": "0.3.7", + "@backstage/plugin-auth-backend-module-bitbucket-server-provider": "0.2.7", + "@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.4.7", + "@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.4.7", + "@backstage/plugin-auth-backend-module-github-provider": "0.3.7", + "@backstage/plugin-auth-backend-module-gitlab-provider": "0.3.7", + "@backstage/plugin-auth-backend-module-google-provider": "0.3.7", + "@backstage/plugin-auth-backend-module-guest-provider": "0.2.12", + "@backstage/plugin-auth-backend-module-microsoft-provider": "0.3.7", + "@backstage/plugin-auth-backend-module-oauth2-provider": "0.4.7", + "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.2.12", + "@backstage/plugin-auth-backend-module-oidc-provider": "0.4.7", + "@backstage/plugin-auth-backend-module-okta-provider": "0.2.7", + "@backstage/plugin-auth-backend-module-onelogin-provider": "0.3.7", + "@backstage/plugin-auth-backend-module-openshift-provider": "0.1.0", + "@backstage/plugin-auth-backend-module-pinniped-provider": "0.3.7", + "@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.5.7", + "@backstage/plugin-auth-node": "0.6.7", + "@backstage/plugin-auth-react": "0.1.19", + "@backstage/plugin-bitbucket-cloud-common": "0.3.2", + "@backstage/plugin-catalog": "1.31.3", + "@backstage/plugin-catalog-backend": "3.1.0", + "@backstage/plugin-catalog-backend-module-aws": "0.4.15", + "@backstage/plugin-catalog-backend-module-azure": "0.3.9", + "@backstage/plugin-catalog-backend-module-backstage-openapi": "0.5.6", + "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.5.3", + "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.5.3", + "@backstage/plugin-catalog-backend-module-gcp": "0.3.12", + "@backstage/plugin-catalog-backend-module-gerrit": "0.3.6", + "@backstage/plugin-catalog-backend-module-gitea": "0.1.4", + "@backstage/plugin-catalog-backend-module-github": "0.11.0", + "@backstage/plugin-catalog-backend-module-github-org": "0.3.14", + "@backstage/plugin-catalog-backend-module-gitlab": "0.7.3", + "@backstage/plugin-catalog-backend-module-gitlab-org": "0.2.13", + "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.7.4", + "@backstage/plugin-catalog-backend-module-ldap": "0.11.9", + "@backstage/plugin-catalog-backend-module-logs": "0.1.14", + "@backstage/plugin-catalog-backend-module-msgraph": "0.8.0", + "@backstage/plugin-catalog-backend-module-openapi": "0.2.14", + "@backstage/plugin-catalog-backend-module-puppetdb": "0.2.14", + "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.2.12", + "@backstage/plugin-catalog-backend-module-unprocessed": "0.6.4", "@backstage/plugin-catalog-common": "1.1.5", - "@backstage/plugin-catalog-graph": "0.4.22", - "@backstage/plugin-catalog-import": "0.13.4", - "@backstage/plugin-catalog-node": "1.18.0", - "@backstage/plugin-catalog-react": "1.20.0", - "@backstage/plugin-catalog-unprocessed-entities": "0.2.20", + "@backstage/plugin-catalog-graph": "0.5.0", + "@backstage/plugin-catalog-import": "0.13.5", + "@backstage/plugin-catalog-node": "1.19.0", + "@backstage/plugin-catalog-react": "1.21.0", + "@backstage/plugin-catalog-unprocessed-entities": "0.2.21", "@backstage/plugin-catalog-unprocessed-entities-common": "0.0.9", - "@backstage/plugin-config-schema": "0.1.71", - "@backstage/plugin-devtools": "0.1.30", - "@backstage/plugin-devtools-backend": "0.5.8", + "@backstage/plugin-config-schema": "0.1.72", + "@backstage/plugin-devtools": "0.1.31", + "@backstage/plugin-devtools-backend": "0.5.9", "@backstage/plugin-devtools-common": "0.1.17", - "@backstage/plugin-events-backend": "0.5.5", - "@backstage/plugin-events-backend-module-aws-sqs": "0.4.14", - "@backstage/plugin-events-backend-module-azure": "0.2.23", - "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.23", - "@backstage/plugin-events-backend-module-bitbucket-server": "0.1.4", - "@backstage/plugin-events-backend-module-gerrit": "0.2.23", - "@backstage/plugin-events-backend-module-github": "0.4.3", - "@backstage/plugin-events-backend-module-gitlab": "0.3.4", - "@backstage/plugin-events-backend-module-google-pubsub": "0.1.3", - "@backstage/plugin-events-backend-module-kafka": "0.1.2", - "@backstage/plugin-events-backend-test-utils": "0.1.47", - "@backstage/plugin-events-node": "0.4.14", - "@internal/plugin-todo-list": "1.0.42", - "@internal/plugin-todo-list-backend": "1.0.42", + "@backstage/plugin-events-backend": "0.5.6", + "@backstage/plugin-events-backend-module-aws-sqs": "0.4.15", + "@backstage/plugin-events-backend-module-azure": "0.2.24", + "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.24", + "@backstage/plugin-events-backend-module-bitbucket-server": "0.1.5", + "@backstage/plugin-events-backend-module-gerrit": "0.2.24", + "@backstage/plugin-events-backend-module-github": "0.4.4", + "@backstage/plugin-events-backend-module-gitlab": "0.3.5", + "@backstage/plugin-events-backend-module-google-pubsub": "0.1.4", + "@backstage/plugin-events-backend-module-kafka": "0.1.3", + "@backstage/plugin-events-backend-test-utils": "0.1.48", + "@backstage/plugin-events-node": "0.4.15", + "@internal/plugin-todo-list": "1.0.43", + "@internal/plugin-todo-list-backend": "1.0.43", "@internal/plugin-todo-list-common": "1.0.26", - "@backstage/plugin-gateway-backend": "1.0.4", - "@backstage/plugin-home": "0.8.11", - "@backstage/plugin-home-react": "0.1.29", - "@backstage/plugin-kubernetes": "0.12.10", - "@backstage/plugin-kubernetes-backend": "0.20.0", - "@backstage/plugin-kubernetes-cluster": "0.0.28", + "@backstage/plugin-gateway-backend": "1.0.5", + "@backstage/plugin-home": "0.8.12", + "@backstage/plugin-home-react": "0.1.30", + "@backstage/plugin-kubernetes": "0.12.11", + "@backstage/plugin-kubernetes-backend": "0.20.2", + "@backstage/plugin-kubernetes-cluster": "0.0.29", "@backstage/plugin-kubernetes-common": "0.9.6", - "@backstage/plugin-kubernetes-node": "0.3.3", - "@backstage/plugin-kubernetes-react": "0.5.10", - "@backstage/plugin-mcp-actions-backend": "0.1.2", - "@backstage/plugin-notifications": "0.5.8", - "@backstage/plugin-notifications-backend": "0.5.9", - "@backstage/plugin-notifications-backend-module-email": "0.3.12", - "@backstage/plugin-notifications-backend-module-slack": "0.1.4", + "@backstage/plugin-kubernetes-node": "0.3.4", + "@backstage/plugin-kubernetes-react": "0.5.11", + "@backstage/plugin-mcp-actions-backend": "0.1.3", + "@backstage/plugin-notifications": "0.5.9", + "@backstage/plugin-notifications-backend": "0.5.10", + "@backstage/plugin-notifications-backend-module-email": "0.3.13", + "@backstage/plugin-notifications-backend-module-slack": "0.1.5", "@backstage/plugin-notifications-common": "0.1.0", - "@backstage/plugin-notifications-node": "0.2.18", - "@backstage/plugin-org": "0.6.42", - "@backstage/plugin-org-react": "0.1.41", - "@backstage/plugin-permission-backend": "0.7.3", - "@backstage/plugin-permission-backend-module-allow-all-policy": "0.2.11", + "@backstage/plugin-notifications-node": "0.2.19", + "@backstage/plugin-org": "0.6.44", + "@backstage/plugin-org-react": "0.1.42", + "@backstage/plugin-permission-backend": "0.7.4", + "@backstage/plugin-permission-backend-module-allow-all-policy": "0.2.12", "@backstage/plugin-permission-common": "0.9.1", - "@backstage/plugin-permission-node": "0.10.3", + "@backstage/plugin-permission-node": "0.10.4", "@backstage/plugin-permission-react": "0.4.36", - "@backstage/plugin-proxy-backend": "0.6.5", - "@backstage/plugin-proxy-node": "0.1.7", - "@backstage/plugin-scaffolder": "1.34.0", - "@backstage/plugin-scaffolder-backend": "2.2.0", - "@backstage/plugin-scaffolder-backend-module-azure": "0.2.12", - "@backstage/plugin-scaffolder-backend-module-bitbucket": "0.3.13", - "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.2.12", - "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.2.12", - "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.3.12", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.3.14", - "@backstage/plugin-scaffolder-backend-module-gcp": "0.2.12", - "@backstage/plugin-scaffolder-backend-module-gerrit": "0.2.12", - "@backstage/plugin-scaffolder-backend-module-gitea": "0.2.12", - "@backstage/plugin-scaffolder-backend-module-github": "0.8.2", - "@backstage/plugin-scaffolder-backend-module-gitlab": "0.9.4", - "@backstage/plugin-scaffolder-backend-module-notifications": "0.1.13", - "@backstage/plugin-scaffolder-backend-module-rails": "0.5.12", - "@backstage/plugin-scaffolder-backend-module-sentry": "0.2.12", - "@backstage/plugin-scaffolder-backend-module-yeoman": "0.4.13", - "@backstage/plugin-scaffolder-common": "1.7.0", - "@backstage/plugin-scaffolder-node": "0.11.0", - "@backstage/plugin-scaffolder-node-test-utils": "0.3.2", - "@backstage/plugin-scaffolder-react": "1.19.0", - "@backstage/plugin-search": "1.4.29", - "@backstage/plugin-search-backend": "2.0.5", - "@backstage/plugin-search-backend-module-catalog": "0.3.7", - "@backstage/plugin-search-backend-module-elasticsearch": "1.7.5", - "@backstage/plugin-search-backend-module-explore": "0.3.5", - "@backstage/plugin-search-backend-module-pg": "0.5.47", - "@backstage/plugin-search-backend-module-stack-overflow-collator": "0.3.12", - "@backstage/plugin-search-backend-module-techdocs": "0.4.5", - "@backstage/plugin-search-backend-node": "1.3.14", + "@backstage/plugin-proxy-backend": "0.6.6", + "@backstage/plugin-proxy-node": "0.1.8", + "@backstage/plugin-scaffolder": "1.34.1", + "@backstage/plugin-scaffolder-backend": "2.2.1", + "@backstage/plugin-scaffolder-backend-module-azure": "0.2.13", + "@backstage/plugin-scaffolder-backend-module-bitbucket": "0.3.14", + "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.2.13", + "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.2.13", + "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.3.13", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.3.15", + "@backstage/plugin-scaffolder-backend-module-gcp": "0.2.13", + "@backstage/plugin-scaffolder-backend-module-gerrit": "0.2.13", + "@backstage/plugin-scaffolder-backend-module-gitea": "0.2.13", + "@backstage/plugin-scaffolder-backend-module-github": "0.9.0", + "@backstage/plugin-scaffolder-backend-module-gitlab": "0.9.5", + "@backstage/plugin-scaffolder-backend-module-notifications": "0.1.14", + "@backstage/plugin-scaffolder-backend-module-rails": "0.5.13", + "@backstage/plugin-scaffolder-backend-module-sentry": "0.2.13", + "@backstage/plugin-scaffolder-backend-module-yeoman": "0.4.14", + "@backstage/plugin-scaffolder-common": "1.7.1", + "@backstage/plugin-scaffolder-node": "0.11.1", + "@backstage/plugin-scaffolder-node-test-utils": "0.3.3", + "@backstage/plugin-scaffolder-react": "1.19.1", + "@backstage/plugin-search": "1.4.30", + "@backstage/plugin-search-backend": "2.0.6", + "@backstage/plugin-search-backend-module-catalog": "0.3.8", + "@backstage/plugin-search-backend-module-elasticsearch": "1.7.6", + "@backstage/plugin-search-backend-module-explore": "0.3.7", + "@backstage/plugin-search-backend-module-pg": "0.5.48", + "@backstage/plugin-search-backend-module-stack-overflow-collator": "0.3.13", + "@backstage/plugin-search-backend-module-techdocs": "0.4.6", + "@backstage/plugin-search-backend-node": "1.3.15", "@backstage/plugin-search-common": "1.2.19", - "@backstage/plugin-search-react": "1.9.3", - "@backstage/plugin-signals": "0.0.22", - "@backstage/plugin-signals-backend": "0.3.7", - "@backstage/plugin-signals-node": "0.1.23", + "@backstage/plugin-search-react": "1.9.4", + "@backstage/plugin-signals": "0.0.23", + "@backstage/plugin-signals-backend": "0.3.8", + "@backstage/plugin-signals-node": "0.1.24", "@backstage/plugin-signals-react": "0.0.15", - "@backstage/plugin-techdocs": "1.14.0", - "@backstage/plugin-techdocs-addons-test-utils": "1.0.52", - "@backstage/plugin-techdocs-backend": "2.0.5", + "@backstage/plugin-techdocs": "1.15.0", + "@backstage/plugin-techdocs-addons-test-utils": "1.1.0", + "@backstage/plugin-techdocs-backend": "2.1.0", "@backstage/plugin-techdocs-common": "0.1.1", - "@backstage/plugin-techdocs-module-addons-contrib": "1.1.27", - "@backstage/plugin-techdocs-node": "1.13.6", - "@backstage/plugin-techdocs-react": "1.3.2", - "@backstage/plugin-user-settings": "0.8.25", - "@backstage/plugin-user-settings-backend": "0.3.5", + "@backstage/plugin-techdocs-module-addons-contrib": "1.1.28", + "@backstage/plugin-techdocs-node": "1.13.7", + "@backstage/plugin-techdocs-react": "1.3.3", + "@backstage/plugin-user-settings": "0.8.26", + "@backstage/plugin-user-settings-backend": "0.3.6", "@backstage/plugin-user-settings-common": "0.0.1" }, - "changesets": [ - "brave-bugs-know", - "cold-donuts-train", - "cool-games-rescue", - "curvy-sites-rhyme", - "easy-wings-turn", - "fine-hands-think", - "flat-colts-know", - "giant-zebras-peel", - "icy-camels-throw", - "legal-lemons-attend", - "lemon-terms-cheer", - "lucky-glasses-slide", - "mean-sites-cheer", - "puny-books-fetch", - "ripe-plants-pump", - "sharp-carrots-spend", - "sixty-pans-prove", - "sixty-places-push", - "slick-lamps-clap", - "slick-worms-drum", - "social-beers-unite", - "sweet-lemons-wonder", - "tricky-buses-lead", - "wet-kiwis-strive", - "whole-dingos-lay", - "yellow-dragons-float" - ] + "changesets": [] } diff --git a/.changeset/puny-books-fetch.md b/.changeset/puny-books-fetch.md deleted file mode 100644 index 641285869f..0000000000 --- a/.changeset/puny-books-fetch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Updated the backend plugin template to use a new pattern for the `TodoListService` that reduces boilerplate. diff --git a/.changeset/ripe-plants-pump.md b/.changeset/ripe-plants-pump.md deleted file mode 100644 index 677c025105..0000000000 --- a/.changeset/ripe-plants-pump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes-backend': patch ---- - -Fix issue with default objects not being loaded properly diff --git a/.changeset/sharp-carrots-spend.md b/.changeset/sharp-carrots-spend.md deleted file mode 100644 index fc846ce13f..0000000000 --- a/.changeset/sharp-carrots-spend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-github': minor ---- - -Added support for discovery by app diff --git a/.changeset/sixty-pans-prove.md b/.changeset/sixty-pans-prove.md deleted file mode 100644 index 0aa2468281..0000000000 --- a/.changeset/sixty-pans-prove.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Reverts a change in CSS layout that shifted the content of the Techdocs too far to the left. diff --git a/.changeset/sixty-places-push.md b/.changeset/sixty-places-push.md deleted file mode 100644 index 523e466158..0000000000 --- a/.changeset/sixty-places-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-compat-api': patch ---- - -Fix for `compatWrapper` creating many wrapping `Providers` when they should not diff --git a/.changeset/slick-lamps-clap.md b/.changeset/slick-lamps-clap.md deleted file mode 100644 index a94b5b81f4..0000000000 --- a/.changeset/slick-lamps-clap.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-msgraph': minor ---- - -**BREAKING**: Encode query filters for requests made to msgraph. If you currently have manually encoded characters in a filter, this is a breaking change and must be updated to avoid requests being double encoded. - -```diff -user: -- filter: department in('MARKETING', 'RESEARCH %26 DEVELOPMENT') -+ filter: department in('MARKETING', 'RESEARCH & DEVELOPMENT') -``` diff --git a/.changeset/slick-worms-drum.md b/.changeset/slick-worms-drum.md deleted file mode 100644 index 4fb170655b..0000000000 --- a/.changeset/slick-worms-drum.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Fixes for rendering initials in the avatar component. diff --git a/.changeset/social-beers-unite.md b/.changeset/social-beers-unite.md deleted file mode 100644 index a8942e6452..0000000000 --- a/.changeset/social-beers-unite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Add missing dependency to `@backstage/cli`, `@backstage/core-plugin-api` and `@backstage/integration-react` diff --git a/.changeset/sweet-lemons-wonder.md b/.changeset/sweet-lemons-wonder.md deleted file mode 100644 index 6e5d4e8ef3..0000000000 --- a/.changeset/sweet-lemons-wonder.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@techdocs/cli': patch ---- - -Fixed an issue where `@techdocs/cli serve` command did not pick up the latest changes to TechDocs. diff --git a/.changeset/tricky-buses-lead.md b/.changeset/tricky-buses-lead.md deleted file mode 100644 index f2bc8ec5dc..0000000000 --- a/.changeset/tricky-buses-lead.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Auto-focus the first menu item in `EntityContextMenu`, and do not render a divider if an empty array is passed to `UNSTABLE_extraContextMenuItems`. diff --git a/.changeset/wet-kiwis-strive.md b/.changeset/wet-kiwis-strive.md deleted file mode 100644 index 70cc1a6367..0000000000 --- a/.changeset/wet-kiwis-strive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-node': patch ---- - -Changes OAuth cookies from domain-scoped to host-only by avoid setting the domain attribute in the default cookie configurer. diff --git a/.changeset/whole-dingos-lay.md b/.changeset/whole-dingos-lay.md deleted file mode 100644 index edefa5c44e..0000000000 --- a/.changeset/whole-dingos-lay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Only run provider orphan cleanup if the engine is started in the first place diff --git a/.changeset/yellow-dragons-float.md b/.changeset/yellow-dragons-float.md deleted file mode 100644 index c1d8341e32..0000000000 --- a/.changeset/yellow-dragons-float.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Support `default*` for older packages as this package is in range for breaking `/alpha` changes diff --git a/.changeset/young-doodles-enter.md b/.changeset/young-doodles-enter.md deleted file mode 100644 index 63e1f33eda..0000000000 --- a/.changeset/young-doodles-enter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -update about card links style for pretty display with other language diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index 56167d6f9f..8f3dcb65ff 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -38,6 +38,7 @@ Bitrise Blackbox bool boolean +breakpoint Brex broadcasted bugfixes @@ -146,6 +147,7 @@ Expedia facto failover Fargate +faqs featureful Figma firehydrant @@ -247,6 +249,8 @@ lockdown lockfile lockfiles lookbehind +lookup +lookups lowercased lunr Luxon @@ -460,6 +464,7 @@ subfolders subheader subheaders subkey +subpage subpath subroutes substring diff --git a/.github/workflows/verify_storybook-noop.yml b/.github/workflows/verify_chromatic-noop.yml similarity index 90% rename from .github/workflows/verify_storybook-noop.yml rename to .github/workflows/verify_chromatic-noop.yml index 98ce6b26a1..600e8f9bdb 100644 --- a/.github/workflows/verify_storybook-noop.yml +++ b/.github/workflows/verify_chromatic-noop.yml @@ -1,11 +1,11 @@ # NO-OP placeholder that always passes for other paths # This is here so that we're able to set the status check as required -name: Storybook Void +name: Chromatic Void on: pull_request: paths-ignore: - - '.github/workflows/verify_storybook.yml' + - '.github/workflows/verify_chromatic.yml' - 'storybook/**' - 'packages/config/src/**' - 'packages/theme/src/**' @@ -25,7 +25,7 @@ jobs: noop: runs-on: ubuntu-latest - name: Storybook + name: Chromatic steps: - name: Harden Runner uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_chromatic.yml similarity index 88% rename from .github/workflows/verify_storybook.yml rename to .github/workflows/verify_chromatic.yml index 5a71caba85..86dbe6477c 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_chromatic.yml @@ -1,9 +1,9 @@ -name: Storybook +name: Chromatic on: # NOTE: If you change these you must update verify_storybook-noop.yml as well pull_request: paths: - - '.github/workflows/verify_storybook.yml' + - '.github/workflows/verify_chromatic.yml' - '.storybook/**' - 'packages/ui/src/**' - 'packages/config/src/**' @@ -26,7 +26,7 @@ jobs: os: [ubuntu-latest] node-version: [20.x] - name: Storybook + name: Chromatic steps: - name: Harden Runner uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 @@ -50,13 +50,14 @@ jobs: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: Build Storybook - run: yarn build-storybook + run: yarn build-storybook:chromatic - - name: Deploy Storybook to Chromatic - uses: chromaui/action@1cfa065cbdab28f6ca3afaeb3d761383076a35aa # v11 + - name: Run Chromatic + uses: chromaui/action@latest with: token: ${{ secrets.GITHUB_TOKEN }} # projectToken intentionally shared to allow collaborators to run Chromatic on forks # https://www.chromatic.com/docs/custom-ci-provider#run-chromatic-on-external-forks-of-open-source-projects projectToken: chpt_dab72dc0f97d55b storybookBuildDir: dist-storybook + onlyChanged: true diff --git a/.storybook/main.ts b/.storybook/main.ts index caf2d744e3..788df83ba9 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -2,17 +2,21 @@ import type { StorybookConfig } from '@storybook/react-vite'; import { join, dirname, posix } from 'path'; -// This set of stories are the ones that we publish to backstage.io. -const backstageCoreStories = [ - 'packages/ui', - 'packages/core-components', - 'packages/app', - 'plugins/org', - 'plugins/search', - 'plugins/search-react', - 'plugins/home', - 'plugins/catalog-react', -]; +const isChromatic = process.env.STORYBOOK_STORY_SET === 'chromatic'; + +// All stories for full development +const allStories = isChromatic + ? ['packages/ui'] + : [ + 'packages/ui', + 'packages/core-components', + 'packages/app', + 'plugins/org', + 'plugins/search', + 'plugins/search-react', + 'plugins/home', + 'plugins/catalog-react', + ]; const rootPath = '../'; const storiesSrcMdx = 'src/**/*.mdx'; @@ -21,7 +25,7 @@ const storiesSrcGlob = 'src/**/*.stories.@(js|jsx|mjs|ts|tsx)'; const getStoriesPath = (element: string, pattern: string) => posix.join(rootPath, element, pattern); -const stories = backstageCoreStories.flatMap(element => [ +const stories = allStories.flatMap(element => [ getStoriesPath(element, storiesSrcMdx), getStoriesPath(element, storiesSrcGlob), ]); @@ -35,15 +39,89 @@ const config: StorybookConfig = { stories, addons: [ getAbsolutePath('@storybook/addon-links'), - getAbsolutePath('@storybook/addon-essentials'), - getAbsolutePath('@storybook/addon-interactions'), getAbsolutePath('@storybook/addon-themes'), - getAbsolutePath('@storybook/addon-storysource'), + getAbsolutePath('@storybook/addon-docs'), + getAbsolutePath('@storybook/addon-a11y'), ], framework: { name: getAbsolutePath('@storybook/react-vite'), options: {}, }, + viteFinal: async (config, { configType }) => { + // Add Node.js polyfills for browser compatibility + // + // When upgrading from Storybook 8 to 9 with the react-vite framework, + // Node.js polyfills are no longer automatically included by Vite. + // This causes "ReferenceError: process is not defined" errors in the browser + // when code tries to access Node.js globals like `process` and `util`. + // + // The @vitest/mocker (included with Storybook 9) expects MSW v2 APIs, + // but we want to keep MSW v1 for the rest of the monorepo to avoid + // breaking changes. This configuration provides the necessary polyfills + // and handles the MSW compatibility issue specifically for Storybook. + // + // These polyfills provide browser-compatible versions of Node.js globals: + // - process: Node.js process object with env + // - global -> globalThis: Maps Node.js global to browser's globalThis + // + // Without these, Backstage components that rely on Node.js APIs will fail + // to load in Storybook's browser environment. + // Different configurations for development vs production + if (configType === 'DEVELOPMENT') { + // Development: Include process polyfill to prevent runtime errors + config.define = { + ...config.define, + global: 'globalThis', + 'process.env': {}, + process: '({ env: {}, browser: true })', + }; + } else if (configType === 'PRODUCTION') { + // Production: Minimal define to avoid esbuild errors + config.define = { + ...config.define, + global: 'globalThis', + 'process.env': {}, + // No process polyfill in production build + }; + } + + config.resolve = { + ...config.resolve, + alias: { + ...config.resolve?.alias, + // Provide Node.js polyfills for browser + process: 'process/browser', + util: 'util', + buffer: 'buffer', + stream: 'stream-browserify', + // Fix MSW v2 imports for @vitest/mocker compatibility + // @vitest/mocker expects MSW v2 APIs but we want to keep MSW v1 for the rest of the monorepo + 'msw/browser': join(__dirname, 'msw-browser-shim.js'), + 'msw/core/http': join(__dirname, 'msw-http-shim.js'), + }, + }; + + // Optimize dependencies for better performance + config.optimizeDeps = { + ...config.optimizeDeps, + include: [ + ...(config.optimizeDeps?.include || []), + 'process/browser', + 'util', + 'buffer', + 'stream-browserify', + ], + // Exclude MSW to prevent optimization conflicts with our shims + exclude: [ + ...(config.optimizeDeps?.exclude || []), + 'msw', + 'msw/browser', + 'msw/core/http', + ], + }; + + return config; + }, }; export default config; diff --git a/.storybook/modes.ts b/.storybook/modes.ts new file mode 100644 index 0000000000..7087fa35eb --- /dev/null +++ b/.storybook/modes.ts @@ -0,0 +1,18 @@ +export const allModes = { + 'light backstage': { + themeMode: 'light', + themeName: 'backstage', + }, + 'dark backstage': { + themeMode: 'dark', + themeName: 'backstage', + }, + 'light spotify': { + themeMode: 'light', + themeName: 'spotify', + }, + 'dark spotify': { + themeMode: 'dark', + themeName: 'spotify', + }, +} as const; diff --git a/.storybook/msw-browser-shim.js b/.storybook/msw-browser-shim.js new file mode 100644 index 0000000000..e0c71e3d3e --- /dev/null +++ b/.storybook/msw-browser-shim.js @@ -0,0 +1,47 @@ +// MSW v2 browser compatibility shim for @vitest/mocker +// This provides MSW v2 exports using MSW v1 APIs to maintain compatibility +// while keeping the rest of the monorepo on MSW v1 + +try { + // Try to import from MSW v1 + const { setupWorker, rest } = require('msw'); + + // Export in MSW v2 format expected by @vitest/mocker + module.exports = { + setupWorker, + // MSW v2 uses 'http' instead of 'rest', but we provide both for compatibility + http: rest, + rest, // Keep rest for any MSW v1 code + }; +} catch (error) { + // Fallback if MSW is not available - provide minimal mock + console.warn( + 'MSW not available, providing minimal mock for @vitest/mocker compatibility', + ); + module.exports = { + setupWorker: () => ({ + start: () => Promise.resolve(), + stop: () => {}, + use: () => {}, + resetHandlers: () => {}, + }), + http: { + get: () => {}, + post: () => {}, + put: () => {}, + delete: () => {}, + patch: () => {}, + head: () => {}, + options: () => {}, + }, + rest: { + get: () => {}, + post: () => {}, + put: () => {}, + delete: () => {}, + patch: () => {}, + head: () => {}, + options: () => {}, + }, + }; +} diff --git a/.storybook/msw-http-shim.js b/.storybook/msw-http-shim.js new file mode 100644 index 0000000000..8653256960 --- /dev/null +++ b/.storybook/msw-http-shim.js @@ -0,0 +1,47 @@ +// MSW v2 http compatibility shim for @vitest/mocker +// This provides MSW v2 http exports using MSW v1 APIs + +try { + // Try to import from MSW v1 + const { rest } = require('msw'); + + // Export MSW v1 'rest' as MSW v2 'http' for @vitest/mocker compatibility + module.exports = { + http: rest, + // Provide individual methods that @vitest/mocker might expect + get: rest.get, + post: rest.post, + put: rest.put, + delete: rest.delete, + patch: rest.patch, + head: rest.head, + options: rest.options, + all: rest.all, + }; +} catch (error) { + // Fallback if MSW is not available + console.warn( + 'MSW not available, providing minimal http mock for @vitest/mocker compatibility', + ); + const noop = () => {}; + module.exports = { + http: { + get: noop, + post: noop, + put: noop, + delete: noop, + patch: noop, + head: noop, + options: noop, + all: noop, + }, + get: noop, + post: noop, + put: noop, + delete: noop, + patch: noop, + head: noop, + options: noop, + all: noop, + }; +} diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index d81c2e9932..8f987ebab1 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -2,9 +2,10 @@ import React, { useEffect } from 'react'; import { TestApiProvider } from '@backstage/test-utils'; import { Content, AlertDisplay } from '@backstage/core-components'; import { apis } from './support/apis'; -import type { Decorator, Preview } from '@storybook/react'; -import { useGlobals } from '@storybook/preview-api'; +import type { Decorator, Preview } from '@storybook/react-vite'; +import { useGlobals } from 'storybook/preview-api'; import { UnifiedThemeProvider, themes } from '@backstage/theme'; +import { allModes } from './modes'; // Default Backstage theme CSS (from packages/ui) import '../packages/ui/src/css/styles.css'; @@ -52,20 +53,24 @@ const preview: Preview = { }, parameters: { layout: 'fullscreen', + backgrounds: { disable: true, }, + controls: { matchers: { color: /(background|color)$/i, date: /Date$/i, }, }, + options: { storySort: { order: ['Backstage UI', 'Plugins', 'Layout', 'Navigation'], }, }, + viewport: { viewports: { initial: { @@ -82,6 +87,20 @@ const preview: Preview = { }, }, }, + + docs: { + codePanel: true, + }, + + chromatic: { + modes: { + 'light backstage': allModes['light backstage'], + // TODO: Enable these modes when we have more Chromatic snapshots. + // 'dark backstage': allModes['dark backstage'], + // 'light spotify': allModes['light spotify'], + // 'dark spotify': allModes['dark spotify'], + }, + }, }, decorators: [ Story => { diff --git a/.yarn/patches/@storybook-react-npm-9.1.5-2331f18b6b.patch b/.yarn/patches/@storybook-react-npm-9.1.5-2331f18b6b.patch new file mode 100644 index 0000000000..b8fb44001f --- /dev/null +++ b/.yarn/patches/@storybook-react-npm-9.1.5-2331f18b6b.patch @@ -0,0 +1,35 @@ +diff --git a/dist/preview.d.ts b/dist/preview.d.ts +index 73525b5fe07240d27733b59362cf2076cd28a6ed..0420e6b170a65b6d0db64fcfcfa07b68cd851942 100644 +--- a/dist/preview.d.ts ++++ b/dist/preview.d.ts +@@ -174,7 +174,7 @@ type UnionToIntersection = ( + declare function __definePreview[]>(input: { + addons: Addons; + } & ProjectAnnotations>): ReactPreview>; +-interface ReactPreview extends Preview { ++interface ReactPreview extends Omit, 'meta'> { + meta, TMetaArgs extends Partial>(meta: { + render?: ArgsStoryFn; + component?: ComponentType; +@@ -187,16 +187,13 @@ interface ReactPreview extends Preview { + }>; + } + type DecoratorsArgs = UnionToIntersection ? TArgs : unknown>; +-interface ReactMeta> extends Meta { ++interface ReactMeta extends Omit>, 'story'> { + story ReactTypes['storyResult']) | (StoryAnnotations & { + render: () => ReactTypes['storyResult']; +- })>(story?: TInput): ReactStory ReactTypes['storyResult'] ? { +- render: TInput; +- } : TInput>; +- story, SetOptional>>>(story?: TInput): ReactStory; ++ })>(story?: TInput): ReactStory; ++ story(story?: TInput): ReactStory; + } +-interface ReactStory> extends Story { ++interface ReactStory { + Component: ComponentType>; + } +- + export { ReactPreview, ReactStory, __definePreview }; +\ No newline at end of file diff --git a/.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch b/.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch new file mode 100644 index 0000000000..996a23c205 --- /dev/null +++ b/.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch @@ -0,0 +1,15 @@ +diff --git a/lib/main.d.ts b/lib/main.d.ts +index 9b54fef0086ea0c99368d8a95f2476e5dcd6d9c1..5c3853901e522c445cdb23b07538aad8a15bafb0 100644 +--- a/lib/main.d.ts ++++ b/lib/main.d.ts +@@ -1,7 +1,7 @@ +-import { ASTNode, Type, AnyType, Field } from "./types"; +-import { NodePath } from "./node-path"; ++import { ASTNode, type Type, AnyType, Field } from "./types"; ++import { type NodePath } from "./node-path"; + import { namedTypes } from "./gen/namedTypes"; +-import { builders } from "./gen/builders"; ++import { type builders } from "./gen/builders"; + import { Visitor } from "./gen/visitor"; + declare const astNodesAreEquivalent: { + (a: any, b: any, problemPath?: any): boolean; diff --git a/app-config.yaml b/app-config.yaml index 60842ee61a..eacf9a96aa 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -209,6 +209,11 @@ scaffolder: defaultCommitMessage: 'Initial commit' auth: + experimentalDynamicClientRegistration: + enabled: true + allowedRedirectUriPatterns: + - cursor://* + ### Add auth.keyStore.provider to more granularly control how to store JWK data when running # the auth-backend. # diff --git a/docs-ui/package.json b/docs-ui/package.json index 71db880859..50bcae8d72 100644 --- a/docs-ui/package.json +++ b/docs-ui/package.json @@ -28,7 +28,7 @@ "@uiw/react-codemirror": "^4.23.7", "html-react-parser": "^5.2.5", "motion": "^12.4.1", - "next": "15.3.4", + "next": "15.4.7", "next-mdx-remote-client": "^2.1.2", "prop-types": "^15.8.1", "react": "19.1.1", diff --git a/docs-ui/src/content/components/table.props.ts b/docs-ui/src/content/components/table.props.ts index 87be472609..1b40055849 100644 --- a/docs-ui/src/content/components/table.props.ts +++ b/docs-ui/src/content/components/table.props.ts @@ -217,18 +217,18 @@ export const cellPropDefs: Record = { }; export const tablePaginationPropDefs: Record = { - pageIndex: { + offset: { type: 'number', - description: 'The current page index.', + description: 'The current offset (starting index) for pagination.', }, pageSize: { type: 'number', description: 'The number of items per page.', }, - setPageIndex: { + setOffset: { type: 'enum', - values: ['(pageIndex: number) => void'], - description: 'Handler that is called when the page index changes.', + values: ['(offset: number) => void'], + description: 'Handler that is called when the offset changes.', }, setPageSize: { type: 'enum', @@ -277,10 +277,7 @@ export const tableUsageSnippet = `import { Cell, ..., TableHeader, TablePaginati `; -export const tableBasicSnippet = `import { Table, TablePagination } from '@backstage/ui'; - -const [pageIndex, setPageIndex] = useState(0); -const [pageSize, setPageSize] = useState(5); +export const tableBasicSnippet = `import { Table, TableHeader, Column, TableBody, Row, Cell, CellProfile, TablePagination, useTable } from '@backstage/ui'; const data = [ { @@ -293,10 +290,13 @@ const data = [ // ... more data ]; -const newData = data4.slice( - pageIndex * pageSize, - (pageIndex + 1) * pageSize, -); +// Uncontrolled pagination (easiest) +const { data: paginatedData, paginationProps } = useTable({ + data, + pagination: { + defaultPageSize: 5, + }, +}); @@ -306,9 +306,9 @@ const newData = data4.slice( Albums - {newData.map(item => ( + {paginatedData?.map(item => ( -
-`; +`; diff --git a/docs-ui/yarn.lock b/docs-ui/yarn.lock index 0a76d117be..0969bf0ffa 100644 --- a/docs-ui/yarn.lock +++ b/docs-ui/yarn.lock @@ -774,10 +774,10 @@ __metadata: languageName: node linkType: hard -"@next/env@npm:15.3.4": - version: 15.3.4 - resolution: "@next/env@npm:15.3.4" - checksum: 10/40ea0bee2eca72dce6102d30ac50029309bf68a281585270e38f920ae47043f240352ad250058d462169a436dea3d8f78a935406ee61088bd4a614524b4a315b +"@next/env@npm:15.4.7": + version: 15.4.7 + resolution: "@next/env@npm:15.4.7" + checksum: 10/8341fa6f1b5aebb6f99d8abad2f3ee2685281aaa51785f8ceff3c9a7f03186645f372003ee833715c0ce05e4e7d960defa4252018977f19564001b872b24714e languageName: node linkType: hard @@ -807,58 +807,58 @@ __metadata: languageName: node linkType: hard -"@next/swc-darwin-arm64@npm:15.3.4": - version: 15.3.4 - resolution: "@next/swc-darwin-arm64@npm:15.3.4" +"@next/swc-darwin-arm64@npm:15.4.7": + version: 15.4.7 + resolution: "@next/swc-darwin-arm64@npm:15.4.7" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@next/swc-darwin-x64@npm:15.3.4": - version: 15.3.4 - resolution: "@next/swc-darwin-x64@npm:15.3.4" +"@next/swc-darwin-x64@npm:15.4.7": + version: 15.4.7 + resolution: "@next/swc-darwin-x64@npm:15.4.7" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@next/swc-linux-arm64-gnu@npm:15.3.4": - version: 15.3.4 - resolution: "@next/swc-linux-arm64-gnu@npm:15.3.4" +"@next/swc-linux-arm64-gnu@npm:15.4.7": + version: 15.4.7 + resolution: "@next/swc-linux-arm64-gnu@npm:15.4.7" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:15.3.4": - version: 15.3.4 - resolution: "@next/swc-linux-arm64-musl@npm:15.3.4" +"@next/swc-linux-arm64-musl@npm:15.4.7": + version: 15.4.7 + resolution: "@next/swc-linux-arm64-musl@npm:15.4.7" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:15.3.4": - version: 15.3.4 - resolution: "@next/swc-linux-x64-gnu@npm:15.3.4" +"@next/swc-linux-x64-gnu@npm:15.4.7": + version: 15.4.7 + resolution: "@next/swc-linux-x64-gnu@npm:15.4.7" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:15.3.4": - version: 15.3.4 - resolution: "@next/swc-linux-x64-musl@npm:15.3.4" +"@next/swc-linux-x64-musl@npm:15.4.7": + version: 15.4.7 + resolution: "@next/swc-linux-x64-musl@npm:15.4.7" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:15.3.4": - version: 15.3.4 - resolution: "@next/swc-win32-arm64-msvc@npm:15.3.4" +"@next/swc-win32-arm64-msvc@npm:15.4.7": + version: 15.4.7 + resolution: "@next/swc-win32-arm64-msvc@npm:15.4.7" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:15.3.4": - version: 15.3.4 - resolution: "@next/swc-win32-x64-msvc@npm:15.3.4" +"@next/swc-win32-x64-msvc@npm:15.4.7": + version: 15.4.7 + resolution: "@next/swc-win32-x64-msvc@npm:15.4.7" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -1122,13 +1122,6 @@ __metadata: languageName: node linkType: hard -"@swc/counter@npm:0.1.3": - version: 0.1.3 - resolution: "@swc/counter@npm:0.1.3" - checksum: 10/df8f9cfba9904d3d60f511664c70d23bb323b3a0803ec9890f60133954173047ba9bdeabce28cd70ba89ccd3fd6c71c7b0bd58be85f611e1ffbe5d5c18616598 - languageName: node - linkType: hard - "@swc/helpers@npm:0.5.15": version: 0.5.15 resolution: "@swc/helpers@npm:0.5.15" @@ -1916,15 +1909,6 @@ __metadata: languageName: node linkType: hard -"busboy@npm:1.6.0": - version: 1.6.0 - resolution: "busboy@npm:1.6.0" - dependencies: - streamsearch: "npm:^1.1.0" - checksum: 10/bee10fa10ea58e7e3e7489ffe4bda6eacd540a17de9f9cd21cc37e297b2dd9fe52b2715a5841afaec82900750d810d01d7edb4b2d456427f449b92b417579763 - languageName: node - linkType: hard - "cacache@npm:^19.0.1": version: 19.0.1 resolution: "cacache@npm:19.0.1" @@ -2360,7 +2344,7 @@ __metadata: html-react-parser: "npm:^5.2.5" lightningcss: "npm:^1.28.2" motion: "npm:^12.4.1" - next: "npm:15.3.4" + next: "npm:15.4.7" next-mdx-remote-client: "npm:^2.1.2" prop-types: "npm:^15.8.1" react: "npm:19.1.1" @@ -5179,29 +5163,27 @@ __metadata: languageName: node linkType: hard -"next@npm:15.3.4": - version: 15.3.4 - resolution: "next@npm:15.3.4" +"next@npm:15.4.7": + version: 15.4.7 + resolution: "next@npm:15.4.7" dependencies: - "@next/env": "npm:15.3.4" - "@next/swc-darwin-arm64": "npm:15.3.4" - "@next/swc-darwin-x64": "npm:15.3.4" - "@next/swc-linux-arm64-gnu": "npm:15.3.4" - "@next/swc-linux-arm64-musl": "npm:15.3.4" - "@next/swc-linux-x64-gnu": "npm:15.3.4" - "@next/swc-linux-x64-musl": "npm:15.3.4" - "@next/swc-win32-arm64-msvc": "npm:15.3.4" - "@next/swc-win32-x64-msvc": "npm:15.3.4" - "@swc/counter": "npm:0.1.3" + "@next/env": "npm:15.4.7" + "@next/swc-darwin-arm64": "npm:15.4.7" + "@next/swc-darwin-x64": "npm:15.4.7" + "@next/swc-linux-arm64-gnu": "npm:15.4.7" + "@next/swc-linux-arm64-musl": "npm:15.4.7" + "@next/swc-linux-x64-gnu": "npm:15.4.7" + "@next/swc-linux-x64-musl": "npm:15.4.7" + "@next/swc-win32-arm64-msvc": "npm:15.4.7" + "@next/swc-win32-x64-msvc": "npm:15.4.7" "@swc/helpers": "npm:0.5.15" - busboy: "npm:1.6.0" caniuse-lite: "npm:^1.0.30001579" postcss: "npm:8.4.31" - sharp: "npm:^0.34.1" + sharp: "npm:^0.34.3" styled-jsx: "npm:5.1.6" peerDependencies: "@opentelemetry/api": ^1.1.0 - "@playwright/test": ^1.41.2 + "@playwright/test": ^1.51.1 babel-plugin-react-compiler: "*" react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 @@ -5236,7 +5218,7 @@ __metadata: optional: true bin: next: dist/bin/next - checksum: 10/bcc781ca5abba6a29408431f81b57598708871796fe4c70d1187cc92ca4905467a6a3cde32840cb7911099d54a93c1543c130e4dea2b59dbf9753f5850a8efdf + checksum: 10/e611751247d5cfff9337a84979918bcd61919d7b66a411c982569336c3dba7e7a8704eead7e082e579807200fa8bc80602a3e2ce4423a8b5387aab3353400918 languageName: node linkType: hard @@ -6059,7 +6041,7 @@ __metadata: languageName: node linkType: hard -"sharp@npm:^0.34.1": +"sharp@npm:^0.34.3": version: 0.34.3 resolution: "sharp@npm:0.34.3" dependencies: @@ -6347,13 +6329,6 @@ __metadata: languageName: node linkType: hard -"streamsearch@npm:^1.1.0": - version: 1.1.0 - resolution: "streamsearch@npm:1.1.0" - checksum: 10/612c2b2a7dbcc859f74597112f80a42cbe4d448d03da790d5b7b39673c1197dd3789e91cd67210353e58857395d32c1e955a9041c4e6d5bae723436b3ed9ed14 - languageName: node - linkType: hard - "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" diff --git a/docs/auth/index.md b/docs/auth/index.md index 9f4c229e7f..bff1f3df26 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -34,6 +34,7 @@ Backstage comes with many common authentication providers in the core library: - [Okta](okta/provider.md) - [OAuth 2 Custom Proxy](oauth2-proxy/provider.md) - [OneLogin](onelogin/provider.md) +- [OpenShift](openshift/provider.md) - [VMware Cloud](vmware-cloud/provider.md) These built-in providers handle the authentication flow for a particular service, including required scopes, callbacks, etc. These providers are each added to a diff --git a/docs/auth/openshift/provider.md b/docs/auth/openshift/provider.md new file mode 100644 index 0000000000..37c5064580 --- /dev/null +++ b/docs/auth/openshift/provider.md @@ -0,0 +1,130 @@ +--- +id: provider +title: OpenShift Authentication Provider +sidebar_label: OpenShift +description: Adding OpenShift OAuth as an authentication provider in Backstage +--- + +The Backstage `core-plugin-api` package comes with a OpenShift authentication +provider that can authenticate users using OpenShift OAuth. + +## Use Case + +This setup enables the [Kubernetes plugin](../../features/kubernetes/index.md) to access OpenShift clusters using the user's permissions, +leveraging OAuth 2.0 _On-Behalf-Of_ flow via the [Kubernetes Client Side Provider](../../features/kubernetes/authentication.md). + +To make this work, the corresponding `User` entities must exist in the Backstage catalog, +and their names must match the OpenShift users. + +Although the OpenShift authentication provider does not support OIDC natively, +you can still configure it for use with the Kubernetes integration by treating it as an OIDC provider +in the `KubernetesAuthProviders` configuration. + +```ts title="packages/app/src/apis.ts" +import { + KubernetesAuthProviders, + kubernetesAuthProvidersApiRef, +} from '@backstage/plugin-kubernetes'; +import { + googleAuthApiRef, + microsoftAuthApiRef, + openshiftAuthApiRef, +} from '@backstage/core-plugin-api'; + +export const apis: AnyApiFactory[] = [ + // ... + createApiFactory({ + api: kubernetesAuthProvidersApiRef, + deps: { + microsoftAuthApi: microsoftAuthApiRef, + googleAuthApi: googleAuthApiRef, + openshiftAuthApi: openshiftAuthApiRef, + }, + factory({ microsoftAuthApi, googleAuthApi, openshiftAuthApi }) { + return new KubernetesAuthProviders({ + microsoftAuthApi, + googleAuthApi, + oidcProviders: { + openshift: { + async getIdToken(_) { + return await openshiftAuthApi.getAccessToken('user:full'); + }, + }, + }, + }); + }, + }), + //... +]; +``` + +:::note Note + +The OpenShift auth API does **not** implement the `OpenIdConnectApi` interface. In other words, it does **not** return an ID token. +Instead, it returns an **access token**, which is used by the Kubernetes integration in place of an ID token. +This is the only functional difference from the standard OIDC-based authentication flow. + +::: + +## Create an OAuth client in OpenShift + +Make sure that an OAuth client exists in the OpenShift cluster. + +To configure the OpenShift integration, create an [`OAuthClient`](https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/authentication_and_authorization/configuring-oauth-clients). + +The redirect URI must be in the following format: `https:///api/auth/openshift/handler/frame`. + +## Configuration + +The provider configuration can then be added to your `app-config.yaml` under the +root `auth` configuration: + +```yaml +auth: + environment: development + providers: + openshift: + development: + clientId: ${AUTH_OPENSHIFT_CLIENT_ID} + clientSecret: ${AUTH_OPENSHIFT_CLIENT_SECRET} + authorizationUrl: ${AUTH_OPENSHIFT_AUTHORIZATION_URL} + tokenUrl: ${AUTH_OPENSHIFT_TOKEN_URL} + openshiftApiServerUrl: ${OPENSHIFT_API_SERVER_URL} + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code + # sessionDuration: 1d + signIn: + resolvers: + - resolver: displayNameMatchingUserEntityName +``` + +The OpenShift provider is a structure with these configuration keys: + +- `clientId`: The client ID of your OpenShift OAuth client, e.g., `my-backstage` +- `clientSecret`: The client secret tied to the OpenShift OAuth client. +- `authorizationUrl`: The OpenShift OAuth client auth endpoint, format: `https:///oauth/authorize`. +- `tokenUrl`: The OpenShift OAuth client token endpoint, format: `https:///oauth/token`. +- `openshiftApiServerUrl`: The OpenShift API server endpoint, format: `https://`. +- `sessionDuration`: (optional): Lifespan of the user session. +- `signIn`: The configuration for the sign-in process, including the **resolvers** + that should be used to match the user from the auth provider with the user + entity in the Backstage catalog (typically a single resolver is sufficient). + +The provider needs to use the scope **user:full**. + +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-openshift-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-openshift-provider')); +/* highlight-add-end */ +``` diff --git a/docs/backend-system/core-services/cache.md b/docs/backend-system/core-services/cache.md index 0e26e8e1f5..ad4d04b50a 100644 --- a/docs/backend-system/core-services/cache.md +++ b/docs/backend-system/core-services/cache.md @@ -7,6 +7,39 @@ description: Documentation for the Cache service This service lets your plugin interact with a cache. It is bound to your plugin too, so that you will only set and get values in your plugin's private namespace. +## Configuration + +The cache service can be configured using the `backend.cache` section in your `app-config.yaml`: + +```yaml +backend: + cache: + store: redis # or 'valkey', 'memcache', 'memory' + connection: redis://localhost:6379 + + # Store-specific configuration (Redis/Valkey only) + redis: + client: + # Optional: Global namespace prefix for all cache keys + namespace: 'my-app' + # Optional: Separator used between namespace and plugin ID (default: ':') + keyPrefixSeparator: ':' + # Other Redis-specific options... + clearBatchSize: 1000 + useUnlink: false +``` + +### Namespace Configuration + +For Redis and Valkey stores, you can configure a global namespace that will be prefixed to all cache keys: + +- **Without namespace**: Cache keys use only the plugin ID (e.g., `catalog:some-key`) +- **With namespace**: Cache keys use the format `namespace:pluginId:key` (e.g., `my-app:catalog:some-key`) + +The `keyPrefixSeparator` controls what character is used between the namespace and plugin ID (defaults to `:`). + +**Note**: Memory and Memcache stores do not support namespace configuration and will always use the plugin ID directly. + ## Using the service The following example shows how to get a cache client in your `example` backend plugin and setting and getting values from the cache. diff --git a/docs/backend-system/core-services/root-logger.md b/docs/backend-system/core-services/root-logger.md index eea846335e..ba2ab05197 100644 --- a/docs/backend-system/core-services/root-logger.md +++ b/docs/backend-system/core-services/root-logger.md @@ -13,6 +13,51 @@ If you want to override the implementation for logging across all of the backend ## Configuring the service +### Configuration parameters + +The Root Logger Service can be configured with the `backend.logger` section of your `app-config.yaml`. + +The following parameters are available: + +- `level` (string, optional): Sets the global log level. Possible values are `debug`, `info`, `warn`, or `error`. Only messages at or above this level will be logged. This can also be set via the `LOG_LEVEL` environment variable, which takes precedence. Defaults to `info`. See [Modifying the log level](#modifying-the-log-level). + +- `meta` (object, optional): Additional metadata to include with every log entry. + +- `overrides` (array, optional): Allows to specify logger overrides for specific plugins or messages. Each override can match on plugin names, message patterns, or any field contained in the log, and set a custom log level for those matches. + +Log level overrides are useful for controlling the volume of logs generated in Backstage. +They allow you to apply a global log level, `info` for example, while setting a stricter level, such as `warn`, for specific verbose plugins. + +The reverse is also possible: you can set a global log level of `warn` while enabling a more detailed level, such as `debug`, for certain logs. + +Example: + +```yaml +backend: + logger: + meta: + env: prod # Every log message will have `env="prod"` + + level: info # Set the global log level to info (the default) + + overrides: + # Set the log level to 'debug' for the catalog plugin logs + - matchers: + plugin: catalog + level: debug + + # Ignore 'info' incoming HTTP requests logs from the rootHttpRouter service + - matchers: + service: rootHttpRouter + type: incomingRequest + level: warn + + # Ignore logs starting with "Task worker starting", unless they're warnings or errors + - matchers: + message: ['/^Task worker starting/'] + level: warn +``` + ### Modifying the log level The available log levels, in order of decreasing verbosity, are: @@ -21,7 +66,6 @@ The available log levels, in order of decreasing verbosity, are: - `info`: General information about the application's operation. This is the default level. - `warn`: Indicates potential issues or situations that may require attention. - `error`: Indicates errors that have occurred but may not necessarily prevent the application from continuing. -- `critical`: Indicates critical errors that require immediate attention and likely prevent the application from functioning correctly. The verbosity of the logging can be controlled by setting the log level. The log level determines the minimum severity level of events that will be output to the console. For example, if the log level is set to `info`, events with a severity level of `debug` will be ignored. diff --git a/docs/conf/index.md b/docs/conf/index.md index 659ffccfe3..029c5b88fd 100644 --- a/docs/conf/index.md +++ b/docs/conf/index.md @@ -16,10 +16,24 @@ allowing for customization. ## Supplying Configuration Configuration is stored in YAML files where the defaults are `app-config.yaml` -and `app-config.local.yaml` for local overrides. Other sets of files can by -loaded by passing `--config ` flags. The configuration files themselves -contain plain YAML, but with support for loading in data and secrets from -various sources using for example `$env` and `$file` keys. +and `app-config.local.yaml` for local overrides. Additionally, it is possible +to define environment based configuration files with `BACKSTAGE_ENV` +environment variable, which will load `app-config..yaml`. + +Loading order of these files is as follows: + +1. `app-config.yaml` +2. `app-config..yaml` +3. `app-config.local.yaml` +4. `app-config..local.yaml` + +Other sets of files can by loaded by passing `--config ` flags. +Read more about the configuration loading order in the +[Configuration Files](./writing.md#configuration-files) section. + +The configuration files themselves contain plain YAML, but with support for +loading in data and secrets from various sources using for example +`$env` and `$file` keys. It is also possible to supply configuration through environment variables, for example `APP_CONFIG_app_baseUrl=https://staging.example.com`. However these diff --git a/docs/contribute/project-structure.md b/docs/contribute/project-structure.md index 5b86b655b2..f58afedee8 100644 --- a/docs/contribute/project-structure.md +++ b/docs/contribute/project-structure.md @@ -221,7 +221,3 @@ future. - [`catalog-info.yaml`](https://github.com/backstage/backstage/tree/master/catalog-info.yaml) - Description of Backstage in the Backstage Entity format. - -- [`lerna.json`](https://github.com/backstage/backstage/tree/master/lerna.json) - - [Lerna](https://github.com/lerna/lerna) monorepo config. We are using - `yarn workspaces`, so this will only be used for executing scripts. diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md index 83f8cff6ea..04b72da445 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -53,7 +53,7 @@ Once the host build is complete, we are ready to build our image. The following `Dockerfile` is included when creating a new app with `@backstage/create-app`: ```dockerfile -FROM node:20-bookworm-slim +FROM node:22-bookworm-slim # Set Python interpreter for `node-gyp` to use ENV PYTHON=/usr/bin/python3 @@ -178,7 +178,7 @@ the repo root: ```dockerfile # Stage 1 - Create yarn install skeleton layer -FROM node:20-bookworm-slim AS packages +FROM node:22-bookworm-slim AS packages WORKDIR /app COPY backstage.json package.json yarn.lock ./ @@ -193,7 +193,7 @@ COPY plugins plugins RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -exec rm -rf {} \+ # Stage 2 - Install dependencies and build packages -FROM node:20-bookworm-slim AS build +FROM node:22-bookworm-slim AS build # Set Python interpreter for `node-gyp` to use ENV PYTHON=/usr/bin/python3 @@ -231,7 +231,7 @@ RUN mkdir packages/backend/dist/skeleton packages/backend/dist/bundle \ && tar xzf packages/backend/dist/bundle.tar.gz -C packages/backend/dist/bundle # Stage 3 - Build the actual backend image and install production dependencies -FROM node:20-bookworm-slim +FROM node:22-bookworm-slim # Set Python interpreter for `node-gyp` to use ENV PYTHON=/usr/bin/python3 diff --git a/docs/features/search/search-engines.md b/docs/features/search/search-engines.md index 1d66a6ae97..07cf11aae5 100644 --- a/docs/features/search/search-engines.md +++ b/docs/features/search/search-engines.md @@ -239,6 +239,26 @@ search: > You can also increase the batch size if you are using a large ES instance. +### Elasticsearch batch key field + +By default, during bulk uploads with the Elasticsearch indexer, each document is assigned an auto-generated `_id` unless a `batchKeyField` is explicitly set. This configuration is optional and most users won’t need to customize it. However, if your use case involves frequent lookups or updates to existing documents, setting `batchKeyField` can be beneficial. It allows you to define a consistent identifier for each document, helping to streamline updates and prevent duplicate entries. Be aware that if the value provided for `batchKeyField` is not unique across documents, Elasticsearch will overwrite any existing document with the same `_id`. + +**Using `batchKeyField` (Custom `_id`)** + +```yaml +search: + elasticsearch: + batchKeyField: document_id +``` + +**Default Behavior (Auto-generated `_id`)** + +```yaml +search: + elasticsearch: + # No batchKeyField specified — Elasticsearch will autogenerate _id +``` + ### Elasticsearch Index Name Customization By default, the Elasticsearch indexer creates index names based on their type, a separator, and the current date as a postfix. You can configure a custom prefix for all indices by adding the following section to your app configuration. diff --git a/docs/features/software-catalog/life-of-an-entity.md b/docs/features/software-catalog/life-of-an-entity.md index 15f30210e8..628b083ed2 100644 --- a/docs/features/software-catalog/life-of-an-entity.md +++ b/docs/features/software-catalog/life-of-an-entity.md @@ -132,6 +132,10 @@ in the registration order, but this does not apply over multiple catalog modules the order of registration depends on the order in which the modules are loaded by the framework. +It's possible to customize the order of the processors by modifying the +`catalog.processors..priority` configuration option. +The default priority is `20`, and lower value means that the processor runs earlier. + ::: Each step has the opportunity to optionally modify the entity, and to optionally diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index cdd93b50ac..d8a2e1d232 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -126,7 +126,7 @@ metadata: The value of this annotation informs of the path to this component's TechDocs within an external entity that owns the TechDocs. In conjunction with [backstage.io/techdocs-entity](#backstageiotechdocs-entity) this allows for deep linking into the TechDocs of -another entity, not just linking to the root of another entities TechDocs. +another entity, not just linking to the root of another entity's TechDocs. ### backstage.io/view-url, backstage.io/edit-url diff --git a/docs/features/techdocs/FAQ.md b/docs/features/techdocs/FAQ.md index 117471f749..9c78e3e123 100644 --- a/docs/features/techdocs/FAQ.md +++ b/docs/features/techdocs/FAQ.md @@ -12,6 +12,8 @@ This page answers frequently asked questions about [TechDocs](README.md). - [What static site generator is TechDocs using?](#what-static-site-generator-is-techdocs-using) - [What is the mkdocs-techdocs-core plugin?](#what-is-the-mkdocs-techdocs-core-plugin) - [Does TechDocs support file formats other than Markdown (e.g. RST, AsciiDoc)?](#does-techdocs-support-file-formats-other-than-markdown-eg-rst-asciidoc-) +- [What should be the value of `backstage.io/techdocs-ref` when using external build and storage?](#what-should-be-the-value-of-backstageiotechdocs-ref-when-using-external-build-and-storage) +- [Is it possible for users to suggest changes or provide feedback on a TechDocs page?](#is-it-possible-for-users-to-suggest-changes-or-provide-feedback-on-a-techdocs-page) #### What static site generator is TechDocs using? @@ -46,6 +48,10 @@ annotation should still be present in entity descriptor file (e.g. `catalog-info.yaml`) for Backstage to know that TechDocs is enabled for the entity. +#### What happens when you navigate to a TechDocs URL for an entity uses the `backstage.io/techdocs-entity` annotation? + +If you navigate to a TechDocs URL in the format `docs/{namespace}/{kind}/{name}` for an entity that has the `backstage.io/techdocs-entity` annotation (instead of the `backstage.io/techdocs-ref` annotation), then Backstage will redirect to the TechDocs page of the entity referenced in the value of that annotation. + #### Is it possible for users to suggest changes or provide feedback on a TechDocs page? This is supported for TechDocs sites whose source code is hosted in either diff --git a/docs/frontend-system/building-plugins/02-testing.md b/docs/frontend-system/building-plugins/02-testing.md index 21449a5bef..ac90185b40 100644 --- a/docs/frontend-system/building-plugins/02-testing.md +++ b/docs/frontend-system/building-plugins/02-testing.md @@ -92,7 +92,7 @@ import { createExtensionTester } from '@backstage/frontend-test-utils'; import { indexPageExtension } from './plugin'; describe('Index page', () => { - it('should render a the index page', async () => { + it('should render the index page', async () => { await renderInTestApp( createExtensionTester(indexPageExtension).reactElement(), ); @@ -147,7 +147,7 @@ import { createExtensionTester } from '@backstage/frontend-test-utils'; import { indexPageExtension, detailsPageExtension } from './plugin'; describe('Index page', () => { - it('should accepts a custom title via config', async () => { + it('should accept a custom title via config', async () => { const tester = createExtensionTester(indexPageExtension, { // Extension configuration for the index page config: { title: 'Custom page' }, diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index cd86aa3343..252884dc54 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -31,7 +31,7 @@ This guide also assumes a basic understanding of working on a Linux based operat [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/) - A GNU-like build environment available at the command line. For example, on Debian/Ubuntu you will want to have the `make` and `build-essential` packages installed. - On macOS, you will want to have run `xcode-select --install` to get the XCode command line build tooling in place. + On macOS, you will want to run `xcode-select --install` to get the XCode command line build tooling in place. - An account with elevated rights to install the dependencies - `curl` or `wget` installed - Node.js [Active LTS Release](../overview/versioning-policy.md#nodejs-releases) installed using one of these @@ -58,9 +58,9 @@ The Backstage app we'll be creating will only have demo data until we set up int ::: -To install the Backstage Standalone app, we will make use of `npx`. `npx` is a tool that comes preinstalled with Node.js and lets you run commands straight from `npm` or other registries. Before we jump in to running the command, let's chat about what it does. +To install the Backstage Standalone app, we will make use of `npx`. `npx` is a tool that comes preinstalled with Node.js and lets you run commands straight from `npm` or other registries. Before we run the command, let's discuss what it does. -This command will create a new directory with a Backstage app inside. The wizard will ask you for the name of the app. This name will be created as sub directory in your current working directory. +This command will create a new directory with a Backstage app inside. The wizard will ask you for the name of the app. This name will be created as subdirectory in your current working directory. ![create app](../assets/getting-started/create-app-output.png) @@ -89,8 +89,8 @@ app - **package.json**: Root package.json for the project. _Note: Be sure that you don't add any npm dependencies here as they probably should be installed in the intended workspace rather than in the root._ -- **packages/**: Lerna leaf packages or "workspaces". Everything here is going - to be a separate package, managed by lerna. +- **packages/**: Yarn workspaces, everything here is going + to be a separate package, managed by Yarn. - **packages/app/**: A fully functioning Backstage frontend app that acts as a good starting point for you to get to know Backstage. - **packages/backend/**: We include a backend that helps power features such as diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index 61b9a5326a..7bfcd1220a 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -184,7 +184,7 @@ To install custom rules in a plugin, we need to use the [`PermissionsRegistrySer import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), ); /* highlight-add-next-line */ - backend.add(import('./extensions/catalogPermissionRules')); + backend.add(import('./modules/catalogPermissionRules')); ``` 5. Now when you run you Backstage instance - `yarn start` - the rule will be added to the catalog plugin. diff --git a/docs/references/glossary.md b/docs/references/glossary.md index c955d9a5ae..ad9e2bbee5 100644 --- a/docs/references/glossary.md +++ b/docs/references/glossary.md @@ -208,7 +208,7 @@ One of the [packages](#package) within a [monorepo](#monorepo). A package may or 1. A single repository for a collection of related software projects, such as all projects belonging to an organization. -2. A project layout that consists of multiple [packages](#package) within a single project, where packages are able to have local dependencies on each other. Often enabled through tooling such as [lerna](https://lerna.js.org/) and [yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/) +2. A project layout that consists of multiple [packages](#package) within a single project, where packages are able to have local dependencies on each other. Often enabled through tooling such as [yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/) ## Name diff --git a/docs/releases/v1.43.0-changelog.md b/docs/releases/v1.43.0-changelog.md new file mode 100644 index 0000000000..6fa500bdd5 --- /dev/null +++ b/docs/releases/v1.43.0-changelog.md @@ -0,0 +1,2119 @@ +# Release v1.43.0 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.43.0](https://backstage.github.io/upgrade-helper/?to=1.43.0) + +## @backstage/app-defaults@1.7.0 + +### Minor Changes + +- 9956704: Add and configure the OpenShift authentication provider to the default APIs. + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-app-api@1.19.0 + +## @backstage/backend-test-utils@1.9.0 + +### Minor Changes + +- 4e2c237: The `mockServices.rootConfig()` instance now has an `update` method that can be used to test configuration subscriptions and updates. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-app-api@1.2.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/catalog-client@1.12.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- 0efcc97: Updated generated schemas + +## @backstage/core-app-api@1.19.0 + +### Minor Changes + +- 3fca906: Add `OpenShiftAuth` helper to create default OAuth flow for OpenShift. + +### Patch Changes + +- 5ae6d9d: feat: support no en languages +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/types@1.2.2 + +## @backstage/core-components@0.18.0 + +### Minor Changes + +- b9a87f4: Add optional `distance` property to `DependencyEdge` to reflect the distance to a root. + +### Patch Changes + +- 1ad3d94: Dependency graph can now be opened in full screen mode +- e409bec: Fixes for rendering initials in the avatar component. +- ae7d426: update about card links style for pretty display with other language +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + +## @backstage/core-plugin-api@1.11.0 + +### Minor Changes + +- 5114627: Make `openshiftAuthApiRef` available in `@backstage/core-plugin-api`. + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + +## @backstage/frontend-app-api@0.13.0 + +### Minor Changes + +- 6516c3d: The `createSpecializedApp` no longer throws when encountering many common errors when starting up the app. It will instead return them through the `errors` property so that they can be handled more gracefully in the app. + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/types@1.2.2 + - @backstage/frontend-defaults@0.3.1 + - @backstage/core-app-api@1.19.0 + +## @backstage/frontend-plugin-api@0.12.0 + +### Minor Changes + +- 894d514: Make `openshiftApiRef` available to the new frontend system. + +### Patch Changes + +- 2fb8b04: Improved the types of `createFrontendPlugin` and `createFrontendModule` so that errors due to incompatible options are indicated more clearly. +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + +## @backstage/integration@1.18.0 + +### Minor Changes + +- 03bdc68: Added support for limiting GithubAppCredentialsMux to specific apps + +### Patch Changes + +- 56897d7: Fixes issue with Github credentials provider which fails to match organization name if using allowedInstallationOwners + +## @backstage/plugin-app@0.3.0 + +### Minor Changes + +- 99790db: Add implementation of OpenShift authentication provider. + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/integration-react@1.2.10 + +## @backstage/plugin-auth@0.1.0 + +### Minor Changes + +- 54ddfef: Initial publish of the `auth` frontend package + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-components@0.18.0 + +## @backstage/plugin-auth-backend-module-openshift-provider@0.1.0 + +### Minor Changes + +- 5a84253: Add new `auth-backend-module-openshift-provider`. This authentication provider enables Backstage to sign in with OpenShift. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend@3.1.0 + +### Minor Changes + +- 9b40a55: Add support for specifying an entity `spec.type` in `catalog.rules` and `catalog.locations.rules` within the catalog configuration. + + For example, this enables allowing all `Template` entities with the type `website`: + + ```diff + catalog: + rules: + - allow: + - Component + - API + - Resource + - System + - Domain + - Location + + - allow: + + - kind: Template + + spec.type: website + locations: + - type: url + pattern: https://github.com/org/*\/blob/master/*.yaml + ``` + +### Patch Changes + +- 37b4eaf: The 'get-catalog-entity' action now throws a ConflictError instead of generic Error if multiple entities are found, so MCP call doesn't fail with 500. + +- 2bbd24f: Order catalog processors by priority. + + This change enables the ordering of catalog processors by their priority, + allowing for more control over the catalog processing sequence. + The default priority is set to 20, and processors can be assigned a custom + priority to influence their execution order. Lower number indicates higher priority. + The priority can be set by implementing the `getPriority` method in the processor class + or by adding a `catalog.processors..priority` configuration + in the `app-config.yaml` file. The configuration takes precedence over the method. + +- e934a27: Updating `catalog:get-catalog-entity` action to be `readOnly` and non destructive + +- 0efcc97: Updated generated schemas + +- 2204f5b: Prevent deadlock in catalog deferred stitching + +- 58874c4: Add support to disable catalog providers and processors via configuration + +- a4c82ad: Only run provider orphan cleanup if the engine is started in the first place + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + +## @backstage/plugin-catalog-backend-module-github@0.11.0 + +### Minor Changes + +- 03bdc68: Added support for discovery by app + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-msgraph@0.8.0 + +### Minor Changes + +- 577f0ed: **BREAKING**: Encode query filters for requests made to msgraph. If you currently have manually encoded characters in a filter, this is a breaking change and must be updated to avoid requests being double encoded. + + ```diff + user: + - filter: department in('MARKETING', 'RESEARCH %26 DEVELOPMENT') + + filter: department in('MARKETING', 'RESEARCH & DEVELOPMENT') + ``` + +### Patch Changes + +- 7597781: Ensure that msgraph parent group stays same in case the group has multiple parents +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-graph@0.5.0 + +### Minor Changes + +- ae6b606: Support custom relations by using an API to define known relations and which to show by default + Fixes "simplified" bug (#30121) which caused graphs not to be simplified + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + +## @backstage/plugin-catalog-node@1.19.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- 2bbd24f: Order catalog processors by priority. + + This change enables the ordering of catalog processors by their priority, + allowing for more control over the catalog processing sequence. + The default priority is set to 20, and processors can be assigned a custom + priority to influence their execution order. Lower number indicates higher priority. + The priority can be set by implementing the `getPriority` method in the processor class + or by adding a `catalog.processors..priority` configuration + in the `app-config.yaml` file. The configuration takes precedence over the method. + +- Updated dependencies + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + +## @backstage/plugin-catalog-react@1.21.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- 0174799: Fix a potential race condition in EntityListProvider when selecting filters +- 4316c11: Catalog table columns support i18n +- 79ff318: Removed the deprecation warning when not passing an explicit type to `EntityCardBlueprint`. Omitting the type is now intended, allowing the layout to pick the default type instead, typically `content`. +- ad0f58d: Support `default*` for older packages as this package is in range for breaking `/alpha` changes +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/frontend-test-utils@0.3.6 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/integration-react@1.2.10 + +## @backstage/plugin-scaffolder-backend-module-github@0.9.0 + +### Minor Changes + +- f0f06b4: Adding a new scaffolder action `github:issues:create` following the reference of `github:issues:label` with `dryRun` testing possibility + + It can be used like this + + steps: + - id: create-simple-issue + name: Create Simple Issue + action: github:issues:create + input: + repoUrl: ${{ parameters.repoUrl }} + title: "[${{ parameters.projectName }}] Simple Bug Report" + body: | + ## Bug Description + This is a simple bug report created by the scaffolder template. + + ### Steps to Reproduce + 1. Run the application + 2. Navigate to the main page + 3. Click on the problematic button + + ### Expected Behavior + The button should work correctly. + + ### Actual Behavior + The button does not respond to clicks. + output: + links: + - title: Simple Issue + url: ${{ steps['create-simple-issue'].output.issueUrl }} + +### Patch Changes + +- aee107b: Add `auto_init` option to `github:repo:create` action to create repository with an initial commit containing a README.md file + + This initial commit is created by GitHub itself and the commit is signed, so the repository will not be empty after creation. + + ```diff + - action: github:repo:create + id: init-new-repo + input: + repoUrl: 'github.com?repo=repo&owner=owner' + description: This is the description + visibility: private + + autoInit: true + + ``` + +- 6393b78: Add block creations field in github branch protection scaffolder actions + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-techdocs@1.15.0 + +### Minor Changes + +- a0b604c: Adding redirect handling for TechDocs URLs that reference entities that now reference an external entity for TechDocs. Including tests and documentation. + +### Patch Changes + +- 313cec7: Updated dependency `dompurify` to `^3.2.4`. +- 8d18d23: TechDocs page titles have been improved, especially for deeply nested pages. +- 1dfee19: Reverts a change in CSS layout that shifted the content of the Techdocs too far to the left. +- 4ce5831: Support Techdocs redirect with dompurify 3.2.6+ +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-auth-react@0.1.19 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/integration@1.18.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-search-react@1.9.4 + - @backstage/integration-react@1.2.10 + +## @backstage/plugin-techdocs-addons-test-utils@1.1.0 + +### Minor Changes + +- 72543e9: Adding catalogApiRef to test-utils to support catalog API usage by TechDocs reader page. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.15.0 + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-app-api@1.19.0 + - @backstage/plugin-search-react@1.9.4 + - @backstage/integration-react@1.2.10 + +## @backstage/plugin-techdocs-backend@2.1.0 + +### Minor Changes + +- 063cdc5: Techdocs: support HumanDuration for cache TTL and timeout configuration +- a0b604c: Adding new entity that specifies an external entity in the techdocs-entity annotation and updates to documentation regarding TechDocs redirects. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-module-techdocs@0.4.6 + - @backstage/plugin-techdocs-node@1.13.7 + +## @backstage/backend-app-api@1.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/backend-defaults@0.12.1 + +### Patch Changes + +- 33bd4d0: Deduplicate discovered features discovered with discoveryFeatureLoader + +- 4eda590: Fixed cache namespace and key prefix separator configuration to properly use configured values instead of hardcoded plugin ID. The cache manager now correctly combines the configured namespace with plugin IDs using the configured separator for Redis and Valkey. Memcache and memory store continue to use plugin ID as namespace. + +- f244e61: Add `backend.logger` config options to configure the `RootLoggerService`. + + Read more about the new configuration options in the + [Root Logger Service](https://backstage.io/docs/backend-system/core-services/root-logger/) + documentation. + +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-app-api@1.2.7 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + +## @backstage/backend-dynamic-feature-service@0.7.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/config-loader@1.10.3 + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-backend@0.5.6 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-app-node@0.1.37 + - @backstage/plugin-permission-node@0.10.4 + - @backstage/plugin-scaffolder-node@0.11.1 + - @backstage/plugin-search-backend-node@1.3.15 + +## @backstage/backend-openapi-utils@0.6.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/backend-plugin-api@1.4.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-node@0.10.4 + +## @backstage/cli@0.34.2 + +### Patch Changes + +- e6f45dc: Updated the WebPack configuration to use `contenthash`. This fixes an issue were builds would sometimes generate output files with the same name but different content across builds, leading to breakages when loading the frontend app. + +- fffd434: Disallow import fallback of critical shared dependencies in module federation. + +- 080f252: Fixed the `new-frontend-plugin` template that was incorrectly passing `id` instead of `pluginId` to `createFrontendPlugin` and unnecessarily importing `React`. + +- e0db9b8: Modify the `backstage.json` also for custom patterns if it extends the default pattern. + + Examples: + + - `@backstage/*` (default pattern) + - `@{backstage,backstage-community}/*` + - `@{extra1,backstage,extra2}/*` + +- 275bda8: Fixed an issue that could cause conflicts of detected modules in workspaces with multiple apps. + +- e1adce4: Updated the backend plugin template to use a new pattern for the `TodoListService` that reduces boilerplate. + +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + +## @backstage/config-loader@1.10.3 + +### Patch Changes + +- a73f495: Allow using `BACKSTAGE_ENV` for loading environment specific config files +- Updated dependencies + - @backstage/types@1.2.2 + +## @backstage/core-compat-api@0.5.2 + +### Patch Changes + +- dc01d6f: Fix for `compatWrapper` creating many wrapping `Providers` when they should not +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + +## @backstage/create-app@0.7.4 + +### Patch Changes + +- b2d9fc1: Creates a plugin that redirects from the Home page to the Catalog index page to avoid seeing a not found page error when starting the app. +- 020d484: Bumped create-app version. +- 02dbe8e: Add missing dependency to `@backstage/cli`, `@backstage/core-plugin-api` and `@backstage/integration-react` + +## @backstage/dev-utils@1.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/app-defaults@1.7.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-app-api@1.19.0 + - @backstage/integration-react@1.2.10 + +## @backstage/frontend-defaults@0.3.1 + +### Patch Changes + +- 6516c3d: The default app now leverages the new error reporting functionality from `@backstage/frontend-app-api`. If there are critical errors during startup, an error screen that shows a summary of all errors will now be shown, rather than leaving the screen blank. Other errors will be logged as warnings in the console. +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-app@0.3.0 + - @backstage/core-components@0.18.0 + - @backstage/frontend-app-api@0.13.0 + +## @backstage/frontend-dynamic-feature-loader@0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + +## @backstage/frontend-test-utils@0.3.6 + +### Patch Changes + +- 6516c3d: Internal update to use and throw app errors. +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-app@0.3.0 + - @backstage/frontend-app-api@0.13.0 + - @backstage/types@1.2.2 + +## @backstage/integration-react@1.2.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/integration@1.18.0 + +## @backstage/repo-tools@0.15.2 + +### Patch Changes + +- 133ac7a: Fixed knip-reports command failing when workspace path contains spaces and process termination issues by replacing `execFile` with `spawn` and removing `shell` option. +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/backend-plugin-api@1.4.3 + +## @techdocs/cli@1.9.8 + +### Patch Changes + +- db63208: Fixed an issue where `@techdocs/cli serve` command did not pick up the latest changes to TechDocs. +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-techdocs-node@1.13.7 + +## @backstage/types@1.2.2 + +### Patch Changes + +- a95cebd: Internal refactoring for better type support + +## @backstage/ui@0.7.1 + +### Patch Changes + +- 7307930: Add missing class for flex: baseline +- 89da341: Fix Select component to properly attach aria-label and aria-labelledby props to the rendered element for improved accessibility. +- 0ffa4c7: Removed the need to mock `window.matchMedia` in tests, falling back to default breakpoint values instead. + +## @backstage/plugin-api-docs@0.12.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + +## @backstage/plugin-app-backend@0.5.6 + +### Patch Changes + +- afd368e: Internal update to not expose the old `createRouter`. +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-app-node@0.1.37 + +## @backstage/plugin-app-node@0.1.37 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-app-visualizer@0.1.23 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + +## @backstage/plugin-auth-backend@0.25.4 + +### Patch Changes + +- 1d47bf3: Implementing Dynamic Client Registration with the OIDC server. You can enable this by setting `auth.experimentalDynamicClientRegistration.enabled` in `app-config.yaml`. This is highly experimental, but feedback welcome. +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-auth0-provider@0.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.25.4 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-bitbucket-server-provider@0.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-github-provider@0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-google-provider@0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-guest-provider@0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.25.4 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-okta-provider@0.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.5.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-node@0.6.7 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- 3aff9e1: Changes OAuth cookies from domain-scoped to host-only by avoid setting the domain attribute in the default cookie configurer. +- Updated dependencies + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-react@0.1.19 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + +## @backstage/plugin-bitbucket-cloud-common@0.3.2 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + +## @backstage/plugin-catalog@1.31.3 + +### Patch Changes + +- 4316c11: Catalog table columns support i18n +- ce1239e: Auto-focus the first menu item in `EntityContextMenu`, and do not render a divider if an empty array is passed to `UNSTABLE_extraContextMenuItems`. +- 85c5e04: Fix incorrect `defaultTarget` on `createComponentRouteRef`. +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-search-react@1.9.4 + - @backstage/integration-react@1.2.10 + - @backstage/plugin-scaffolder-common@1.7.1 + +## @backstage/plugin-catalog-backend-module-aws@0.4.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-azure@0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-bitbucket-cloud-common@0.3.2 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-gcp@0.3.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/plugin-catalog-backend-module-github@0.11.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-gitlab@0.7.3 + +### Patch Changes + +- ea80e76: When possible, requests a more limited set of results from the Gitlab projects API, which can reduce the amount of network traffic required to sync with Gitlab. +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-gitlab@0.7.3 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-ldap@0.11.9 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-logs@0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.14 + +### Patch Changes + +- afd368e: **BREAKING ALPHA**: The module has been moved from the `/alpha` export to the root of the package. +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-common@1.7.1 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.6.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-import@0.13.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/integration@1.18.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + - @backstage/integration-react@1.2.10 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.21 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + +## @backstage/plugin-config-schema@0.1.72 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-devtools@0.1.31 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + +## @backstage/plugin-devtools-backend@0.5.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/config-loader@1.10.3 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + +## @backstage/plugin-events-backend@0.5.6 + +### Patch Changes + +- 0efcc97: Updated generated schemas +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-aws-sqs@0.4.15 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-azure@0.2.24 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.24 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-bitbucket-server@0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-gerrit@0.2.24 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-github@0.4.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-gitlab@0.3.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-google-pubsub@0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-kafka@0.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-test-utils@0.1.48 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + +## @backstage/plugin-events-node@0.4.15 + +### Patch Changes + +- 0efcc97: Updated generated schemas +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-gateway-backend@1.0.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-home@0.8.12 + +### Patch Changes + +- 929c55a: Fixed race condition in CustomHomepageGrid by waiting for storage to load before rendering custom layout to prevent + rendering of the default content. +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + - @backstage/core-app-api@1.19.0 + - @backstage/plugin-home-react@0.1.30 + +## @backstage/plugin-home-react@0.1.30 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + +## @backstage/plugin-kubernetes@0.12.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-kubernetes-react@0.5.11 + +## @backstage/plugin-kubernetes-backend@0.20.2 + +### Patch Changes + +- dd7b6d2: Fix a bug where `getDefault` in the `kubernetesFetcherExtensionPoint` had the wrong `this` value +- 80cf8c9: Fix issue with default objects not being loaded properly +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-kubernetes-node@0.3.4 + - @backstage/plugin-permission-node@0.10.4 + +## @backstage/plugin-kubernetes-cluster@0.0.29 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/plugin-kubernetes-react@0.5.11 + +## @backstage/plugin-kubernetes-node@0.3.4 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-kubernetes-react@0.5.11 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-mcp-actions-backend@0.1.3 + +### Patch Changes + +- 1d47bf3: Proxy `/.well-known/oauth-authorization-server` to `/.well-known/openid-configuration` on `auth-backend` when `auth.experimentalDynamicClientRegistration.enabled` is enabled. +- 7f2a4a0: Updating docs +- d08b0c9: The MCP backend will now convert known Backstage errors into textual responses with `isError: true`. + The error message can be useful for an LLM to understand and maybe give back to the user. + Previously all errors where thrown out to `@modelcontextprotocol/sdk` which causes a generic 500. +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-notifications@0.5.9 + +### Patch Changes + +- 4815b12: Fixed missing app context when rendering the notifications view +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + +## @backstage/plugin-notifications-backend@0.5.10 + +### Patch Changes + +- a95cebd: Internal refactoring for better type support + +- 7e7ed57: A new extension point was added that can be used to modify how the users receiving notifications + are resolved. The interface passed to the extension point should only return complete user entity references + based on the notification target references and the excluded entity references. Note that the inputs are lists + of entity references that can be any entity kind, not just user entities. + + Using this extension point will override the default behavior of resolving users with the + `DefaultNotificationRecipientResolver`. + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-node@0.2.19 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-signals-node@0.1.24 + +## @backstage/plugin-notifications-backend-module-email@0.3.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-node@0.2.19 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-notifications-backend-module-slack@0.1.5 + +### Patch Changes + +- a95cebd: Internal refactoring for better type support +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-node@0.2.19 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-notifications-node@0.2.19 + +### Patch Changes + +- 7e7ed57: A new extension point was added that can be used to modify how the users receiving notifications + are resolved. The interface passed to the extension point should only return complete user entity references + based on the notification target references and the excluded entity references. Note that the inputs are lists + of entity references that can be any entity kind, not just user entities. + + Using this extension point will override the default behavior of resolving users with the + `DefaultNotificationRecipientResolver`. + +- Updated dependencies + - @backstage/catalog-client@1.12.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-signals-node@0.1.24 + +## @backstage/plugin-org@0.6.44 + +### Patch Changes + +- 22b69f2: Fixing issue with extra slash in the routing +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + +## @backstage/plugin-org-react@0.1.42 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + +## @backstage/plugin-permission-backend@0.7.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + +## @backstage/plugin-permission-node@0.10.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-proxy-backend@0.6.6 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-proxy-node@0.1.8 + +## @backstage/plugin-proxy-node@0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-scaffolder@1.34.1 + +### Patch Changes + +- 0d415ae: Render a TechDocs link on the Scaffolder Template List page when templates include either `backstage.io/techdocs-ref` or `backstage.io/techdocs-entity` annotations, using the shared `buildTechDocsURL` helper. Also adds tests to verify both annotations and optional `backstage.io/techdocs-entity-path` are respected. +- 7151260: Prevent the MultiEntityPicker from removing options present in form state when new options are selected +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/integration@1.18.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-scaffolder-react@1.19.1 + - @backstage/integration-react@1.2.10 + - @backstage/plugin-scaffolder-common@1.7.1 + +## @backstage/plugin-scaffolder-backend@2.2.1 + +### Patch Changes + +- a57185f: Added support for executing actions from the `ActionsRegistry` in the `scaffolder-backend` +- c3405db: Fixed a regression that prevented uploads greater than 100KB. Uploads up to 10MB are supported again. +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-scaffolder-backend-module-github@0.9.0 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.9.5 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-bitbucket-cloud-common@0.3.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12 + - @backstage/plugin-permission-node@0.10.4 + - @backstage/plugin-scaffolder-backend-module-azure@0.2.13 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.14 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.13 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.13 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.2.13 + - @backstage/plugin-scaffolder-backend-module-gitea@0.2.13 + - @backstage/plugin-scaffolder-common@1.7.1 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-azure@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.14 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.13 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.13 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-bitbucket-cloud-common@0.3.2 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.3.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.9.5 + +### Patch Changes + +- a84ddea: The log message now indicates that the pipeline trigger token was deleted and not pipeline itself. +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-node@0.2.19 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-rails@0.5.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.4.14 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + - @backstage/plugin-scaffolder-node-test-utils@0.3.3 + +## @backstage/plugin-scaffolder-common@1.7.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-scaffolder-node@0.11.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-common@1.7.1 + +## @backstage/plugin-scaffolder-node-test-utils@0.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@1.9.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-react@1.19.1 + +### Patch Changes + +- 58fc108: Fix scaffolder task log stream not having a minimum height +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-common@1.7.1 + +## @backstage/plugin-search@1.4.30 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-search-react@1.9.4 + +## @backstage/plugin-search-backend@2.0.6 + +### Patch Changes + +- 0efcc97: Updated generated schemas +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + - @backstage/plugin-search-backend-node@1.3.15 + +## @backstage/plugin-search-backend-module-catalog@0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + +## @backstage/plugin-search-backend-module-elasticsearch@1.7.6 + +### Patch Changes + +- cde70ca: Added support for batchKeyField in the Elasticsearch indexer to allow consistent document IDs during bulk uploads. +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + +## @backstage/plugin-search-backend-module-explore@0.3.7 + +### Patch Changes + +- 9a93520: Deprecate and mark explore collator as moved +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + +## @backstage/plugin-search-backend-module-pg@0.5.48 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.3.13 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + +## @backstage/plugin-search-backend-module-techdocs@0.4.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + - @backstage/plugin-techdocs-node@1.13.7 + +## @backstage/plugin-search-backend-node@1.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-search-react@1.9.4 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-signals@0.0.23 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + +## @backstage/plugin-signals-backend@0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-signals-node@0.1.24 + +## @backstage/plugin-signals-node@0.1.24 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/integration@1.18.0 + - @backstage/core-components@0.18.0 + - @backstage/integration-react@1.2.10 + +## @backstage/plugin-techdocs-node@1.13.7 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-techdocs-react@1.3.3 + +### Patch Changes + +- a0b604c: Update to documentation regarding TechDocs redirects. +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + +## @backstage/plugin-user-settings@0.8.26 + +### Patch Changes + +- 320a9ac: Add the OpenShift authenticator provider to the default `user-settings` providers page. +- b713b54: Tool-tip text correction for the Theme selection in settings page +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/core-app-api@1.19.0 + +## @backstage/plugin-user-settings-backend@0.3.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-signals-node@0.1.24 + +## example-app@0.2.113 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.34.2 + - @backstage/ui@0.7.1 + - @backstage/plugin-techdocs@1.15.0 + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-notifications@0.5.9 + - @backstage/app-defaults@1.7.0 + - @backstage/plugin-catalog-graph@0.5.0 + - @backstage/plugin-org@0.6.44 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/plugin-auth-react@0.1.19 + - @backstage/plugin-home@0.8.12 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-scaffolder@1.34.1 + - @backstage/plugin-user-settings@0.8.26 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-components@0.18.0 + - @backstage/frontend-app-api@0.13.0 + - @backstage/plugin-scaffolder-react@1.19.1 + - @backstage/core-app-api@1.19.0 + - @backstage/plugin-api-docs@0.12.11 + - @backstage/plugin-catalog-import@0.13.5 + - @backstage/plugin-catalog-unprocessed-entities@0.2.21 + - @backstage/plugin-devtools@0.1.31 + - @backstage/plugin-kubernetes@0.12.11 + - @backstage/plugin-search@1.4.30 + - @backstage/plugin-search-react@1.9.4 + - @backstage/plugin-signals@0.0.23 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28 + - @backstage/integration-react@1.2.10 + - @backstage/plugin-kubernetes-cluster@0.0.29 + +## example-app-next@0.0.27 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.34.2 + - @backstage/ui@0.7.1 + - @backstage/plugin-techdocs@1.15.0 + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-notifications@0.5.9 + - @backstage/app-defaults@1.7.0 + - @backstage/plugin-catalog-graph@0.5.0 + - @backstage/plugin-org@0.6.44 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-auth-react@0.1.19 + - @backstage/plugin-auth@0.1.0 + - @backstage/plugin-home@0.8.12 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-scaffolder@1.34.1 + - @backstage/plugin-app@0.3.0 + - @backstage/plugin-user-settings@0.8.26 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-components@0.18.0 + - @backstage/frontend-app-api@0.13.0 + - @backstage/frontend-defaults@0.3.1 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-scaffolder-react@1.19.1 + - @backstage/core-app-api@1.19.0 + - @backstage/plugin-api-docs@0.12.11 + - @backstage/plugin-app-visualizer@0.1.23 + - @backstage/plugin-catalog-import@0.13.5 + - @backstage/plugin-catalog-unprocessed-entities@0.2.21 + - @backstage/plugin-kubernetes@0.12.11 + - @backstage/plugin-search@1.4.30 + - @backstage/plugin-search-react@1.9.4 + - @backstage/plugin-signals@0.0.23 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28 + - @backstage/integration-react@1.2.10 + - @backstage/plugin-kubernetes-cluster@0.0.29 + +## app-next-example-plugin@0.0.27 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-components@0.18.0 + +## example-backend@0.0.42 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-scaffolder-backend-module-github@0.9.0 + - @backstage/plugin-scaffolder-backend@2.2.1 + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-mcp-actions-backend@0.1.3 + - @backstage/plugin-auth-backend@0.25.4 + - @backstage/plugin-techdocs-backend@2.1.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-app-backend@0.5.6 + - @backstage/plugin-kubernetes-backend@0.20.2 + - @backstage/plugin-events-backend@0.5.6 + - @backstage/plugin-search-backend@2.0.6 + - @backstage/plugin-search-backend-module-explore@0.3.7 + - @backstage/plugin-notifications-backend@0.5.10 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-auth-backend-module-github-provider@0.3.7 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.12 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.6 + - @backstage/plugin-catalog-backend-module-openapi@0.2.14 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.4 + - @backstage/plugin-devtools-backend@0.5.9 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.4 + - @backstage/plugin-permission-backend@0.7.4 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.12 + - @backstage/plugin-permission-node@0.10.4 + - @backstage/plugin-proxy-backend@0.6.6 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.14 + - @backstage/plugin-search-backend-module-catalog@0.3.8 + - @backstage/plugin-search-backend-module-techdocs@0.4.6 + - @backstage/plugin-search-backend-node@1.3.15 + - @backstage/plugin-signals-backend@0.3.8 + +## e2e-test@0.2.32 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.7.4 + +## @internal/frontend@0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/types@1.2.2 + +## @internal/scaffolder@0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-scaffolder-react@1.19.1 + +## techdocs-cli-embedded-app@0.2.112 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.34.2 + - @backstage/plugin-techdocs@1.15.0 + - @backstage/app-defaults@1.7.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-components@0.18.0 + - @backstage/core-app-api@1.19.0 + - @backstage/integration-react@1.2.10 + +## @internal/plugin-todo-list@1.0.43 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + +## @internal/plugin-todo-list-backend@1.0.43 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 diff --git a/docs/releases/v1.43.0-next.2-changelog.md b/docs/releases/v1.43.0-next.2-changelog.md new file mode 100644 index 0000000000..ef7ddb1c1f --- /dev/null +++ b/docs/releases/v1.43.0-next.2-changelog.md @@ -0,0 +1,695 @@ +# Release v1.43.0-next.2 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.43.0-next.2](https://backstage.github.io/upgrade-helper/?to=1.43.0-next.2) + +## @backstage/catalog-client@1.12.0-next.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +## @backstage/plugin-auth@0.1.0-next.0 + +### Minor Changes + +- 54ddfef: Initial publish of the `auth` frontend package + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.17.6-next.1 + +## @backstage/plugin-catalog-node@1.19.0-next.1 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + +## @backstage/plugin-catalog-react@1.21.0-next.2 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/backend-defaults@0.12.1-next.1 + +### Patch Changes + +- 4eda590: Fixed cache namespace and key prefix separator configuration to properly use configured values instead of hardcoded plugin ID. The cache manager now correctly combines the configured namespace with plugin IDs using the configured separator for Redis and Valkey. Memcache and memory store continue to use plugin ID as namespace. +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/integration-aws-node@0.1.17 + +## @backstage/backend-dynamic-feature-service@0.7.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/plugin-app-node@0.1.37-next.1 + +## @backstage/cli@0.34.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/integration@1.18.0-next.0 + +## @backstage/config-loader@1.10.3-next.0 + +### Patch Changes + +- a73f495: Allow using `BACKSTAGE_ENV` for loading environment specific config files + +## @backstage/core-compat-api@0.5.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + +## @backstage/core-components@0.17.6-next.1 + +### Patch Changes + +- 1ad3d94: Dependency graph can now be opened in full screen mode +- ae7d426: update about card links style for pretty display with other language + +## @backstage/create-app@0.7.4-next.2 + +### Patch Changes + +- Bumped create-app version. + +## @backstage/dev-utils@1.1.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + +## @backstage/repo-tools@0.15.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + +## @backstage/ui@0.7.1-next.0 + +### Patch Changes + +- 7307930: Add missing class for flex: baseline +- 89da341: Fix Select component to properly attach aria-label and aria-labelledby props to the rendered element for improved accessibility. + +## @backstage/plugin-api-docs@0.12.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-app-backend@0.5.6-next.1 + +### Patch Changes + +- afd368e: Internal update to not expose the old `createRouter`. +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-app-node@0.1.37-next.1 + +## @backstage/plugin-app-node@0.1.37-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + +## @backstage/plugin-auth-backend@0.25.4-next.1 + +### Patch Changes + +- 1d47bf3: Implementing Dynamic Client Registration with the OIDC server. You can enable this by setting `auth.experimentalDynamicClientRegistration.enabled` in `app-config.yaml`. This is highly experimental, but feedback welcome. +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-auth-node@0.6.7-next.1 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + +## @backstage/plugin-auth-react@0.1.19-next.1 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/core-components@0.17.6-next.1 + +## @backstage/plugin-catalog@1.31.3-next.2 + +### Patch Changes + +- 85c5e04: Fix incorrect `defaultTarget` on `createComponentRouteRef`. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-catalog-backend@3.0.2-next.1 + +### Patch Changes + +- 2204f5b: Prevent deadlock in catalog deferred stitching +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-aws@0.4.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/integration-aws-node@0.1.17 + +## @backstage/plugin-catalog-backend-module-azure@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-gcp@0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-github@0.11.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-catalog-backend-module-github@0.11.0-next.1 + +## @backstage/plugin-catalog-backend-module-gitlab@0.7.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-catalog-backend-module-gitlab@0.7.3-next.1 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-ldap@0.11.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-msgraph@0.8.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.14-next.1 + +### Patch Changes + +- afd368e: **BREAKING ALPHA**: The module has been moved from the `/alpha` export to the root of the package. +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.6.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-graph@0.4.23-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-catalog-import@0.13.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-devtools-backend@0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/backend-defaults@0.12.1-next.1 + +## @backstage/plugin-home@0.8.12-next.2 + +### Patch Changes + +- 929c55a: Fixed race condition in CustomHomepageGrid by waiting for storage to load before rendering custom layout to prevent + rendering of the default content. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-kubernetes@0.12.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-kubernetes-backend@0.20.2-next.2 + +### Patch Changes + +- dd7b6d2: Fix a bug where `getDefault` in the `kubernetesFetcherExtensionPoint` had the wrong `this` value +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration-aws-node@0.1.17 + +## @backstage/plugin-kubernetes-cluster@0.0.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + +## @backstage/plugin-mcp-actions-backend@0.1.3-next.1 + +### Patch Changes + +- 1d47bf3: Proxy `/.well-known/oauth-authorization-server` to `/.well-known/openid-configuration` on `auth-backend` when `auth.experimentalDynamicClientRegistration.enabled` is enabled. +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-notifications-backend@0.5.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-notifications-node@0.2.19-next.1 + +## @backstage/plugin-notifications-backend-module-email@0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration-aws-node@0.1.17 + - @backstage/plugin-notifications-node@0.2.19-next.1 + +## @backstage/plugin-notifications-backend-module-slack@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-notifications-node@0.2.19-next.1 + +## @backstage/plugin-notifications-node@0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + +## @backstage/plugin-org@0.6.44-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-org-react@0.1.42-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + +## @backstage/plugin-scaffolder@1.34.1-next.2 + +### Patch Changes + +- 0d415ae: Render a TechDocs link on the Scaffolder Template List page when templates include either `backstage.io/techdocs-ref` or `backstage.io/techdocs-entity` annotations, using the shared `buildTechDocsURL` helper. Also adds tests to verify both annotations and optional `backstage.io/techdocs-entity-path` are respected. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-scaffolder-backend@2.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12-next.1 + - @backstage/plugin-scaffolder-backend-module-github@0.8.3-next.1 + +## @backstage/plugin-scaffolder-backend-module-github@0.8.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-scaffolder-react@1.19.1-next.2 + +### Patch Changes + +- 58fc108: Fix scaffolder task log stream not having a minimum height +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + +## @backstage/plugin-search@1.4.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-search-backend-module-catalog@0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-search-backend-module-techdocs@0.4.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-techdocs@1.14.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.53-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + +## @backstage/plugin-techdocs-backend@2.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.4.6-next.1 + +## @backstage/plugin-user-settings@0.8.26-next.2 + +### Patch Changes + +- b713b54: Tool-tip text correction for the Theme selection in settings page +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## example-app@0.2.113-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.7.1-next.0 + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/plugin-home@0.8.12-next.2 + - @backstage/plugin-scaffolder@1.34.1-next.2 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-user-settings@0.8.26-next.2 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/cli@0.34.2-next.2 + - @backstage/plugin-catalog-graph@0.4.23-next.2 + - @backstage/plugin-catalog-import@0.13.5-next.2 + - @backstage/plugin-org@0.6.44-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + - @backstage/plugin-api-docs@0.12.11-next.2 + - @backstage/plugin-kubernetes@0.12.11-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.29-next.2 + - @backstage/plugin-search@1.4.30-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28-next.0 + +## example-app-next@0.0.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.7.1-next.0 + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/plugin-auth@0.1.0-next.0 + - @backstage/plugin-home@0.8.12-next.2 + - @backstage/plugin-scaffolder@1.34.1-next.2 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-user-settings@0.8.26-next.2 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/cli@0.34.2-next.2 + - @backstage/plugin-catalog-graph@0.4.23-next.2 + - @backstage/plugin-catalog-import@0.13.5-next.2 + - @backstage/plugin-org@0.6.44-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + - @backstage/core-compat-api@0.5.2-next.2 + - @backstage/plugin-api-docs@0.12.11-next.2 + - @backstage/plugin-kubernetes@0.12.11-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.29-next.2 + - @backstage/plugin-search@1.4.30-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28-next.0 diff --git a/docs/releases/v1.43.0.md b/docs/releases/v1.43.0.md new file mode 100644 index 0000000000..449023d167 --- /dev/null +++ b/docs/releases/v1.43.0.md @@ -0,0 +1,90 @@ +--- +id: v1.43.0 +title: v1.43.0 +description: Backstage Release v1.43.0 +--- + +These are the release notes for the v1.43.0 release of [Backstage](https://backstage.io/). + +A huge thanks to the whole team of maintainers and contributors as well as the amazing Backstage Community for the hard work in getting this release developed and done. + +## Highlights + +### **BREAKING:** MSgraph catalog module encoding queries + +The catalog MSgraph module now properly encodes filter queries before they are sent to the remote system. This fixes some confusion in how to properly write queries–and thus simplifies operations for users. However, it does mean that if you use the `@backstage/plugin-catalog-backend-module-msgraph` module, you will have to update your `filter` parameters to be just plain text without any manual encoding. Example: + +```diff + user: +- filter: department in('MARKETING', 'R %26 D') ++ filter: department in('MARKETING', 'R & D') +``` + +Contributed by [@stephenglass](https://github.com/stephenglass) in [#30567](https://github.com/backstage/backstage/pull/30567) + +### OpenShift auth + +Backstage now supports OpenShift as an auth provider! This was a long standing PR that we are very excited to have merged into the project. Go [check out the docs](https://backstage.io/docs/auth/openshift/provider) for it! + +Contributed by [@ioboi](https://github.com/ioboi) in [#29470](https://github.com/backstage/backstage/pull/29470) + +### Improved app error display for new frontend system + +If the frontend app encounters one or more critical errors during startup they are now displayed on an error page that summarizes the errors and their source. This is in contrast to before this change, where errors would result in a blank page with the first error logged to the console. + +Some more errors have also been moved to be warnings, which will be logged in the console regardless of whether the app starts up or not. + +### Actions Registry integration with the Scaffolder + +Actions which are registered in the Actions Registry are now also available in Scaffolder templates. You will be able to browse the actions and see the schema like the original Scaffolder Templating Actions, as well as being able to use `action: acme:my-action` inside template steps to call these distributed actions. + +### _Experimental_: Dynamic Client Registration support for MCP Actions Backend + +We’ve shipped experimental support for allowing the `auth-backend` to issue tokens on behalf of users for use in MCP Clients such as Claude and Cursor. This is highly experimental, and tokens are only valid for 1 hour currently. We’re eager to get some feedback on this feature, so if you would like to try it out you can check out the [mcp-actions docs](https://github.com/backstage/backstage/blob/master/plugins/mcp-actions-backend/README.md#experimental-dynamic-client-registration) and leave us some feedback in a [GitHub issue](https://github.com/backstage/backstage/issues/new), or on our [Community Discord](https://discord.gg/backstage-687207715902193673)! + +### Introducing `BACKSTAGE_ENV` + +The Backstage configuration system now pays attention to the `BACKSTAGE_ENV` environment variable, and uses it to locate config files to load without you having to manually specify `--config` flags at startup. This makes it even smoother to leverage e.g. `app-config.production.yaml` files in your production environment. + +Check out [the updated documentation](https://backstage.io/docs/conf/) for more information. + +Contributed by [@drodil](https://github.com/drodil) in [#30722](https://github.com/backstage/backstage/pull/30722) + +### Backend logger configuration + +It is now possible to configure log level, meta, and log level overrides through static configuration. This allows you to raise or lower the log level for individual plugins or services. But it also allows you to match on any log field using either exact matches or regular expressions. Read more about the new configuration options in the [Root Logger Service](https://backstage.io/docs/backend-system/core-services/root-logger/) documentation. + +Contributed by [@tcardonne](https://github.com/tcardonne) in [#30727](https://github.com/backstage/backstage/pull/30727) + +### Toggling and priorities for catalog features + +You are now able to enable/disable individual processors and providers in your catalog, directly through your app config. You can also affect the order in which processors run, by assigning them priorities. + +Contributed by [@drodil](https://github.com/drodil) in [#31034](https://github.com/backstage/backstage/pull/31034) and [#30969](https://github.com/backstage/backstage/pull/30969) + +### Moved the Explore search module + +Just a quick note, that the search collator module for the `explore` plugin was moved to the community repository, where it fits snugly next to the plugin itself. If you are using the yarn plugin, these references should be updated automatically for you. But if you do not, just update your dependencies on `@backstage/plugin-search-backend-module-explore`, to `@backstage-community/plugin-search-backend-module-explore`. + +## Security Fixes + +This release does not contain any security fixes. However, it is worth calling out that OAuth cookies issued by the auth backend have changed from domain-scoped to host-only. + +Contributed by [@JessicaJHee](https://github.com/JessicaJHee) in [#30922](https://github.com/backstage/backstage/pull/30922) + +## 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.43.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/lerna.json b/lerna.json deleted file mode 100644 index 4621689664..0000000000 --- a/lerna.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "packages": ["packages/*", "plugins/*"], - "npmClient": "yarn", - "useWorkspaces": true, - "version": "0.0.0" -} diff --git a/microsite/data/plugins/kwirthmetrics.yaml b/microsite/data/plugins/kwirthmetrics.yaml new file mode 100644 index 0000000000..9a8c069eda --- /dev/null +++ b/microsite/data/plugins/kwirthmetrics.yaml @@ -0,0 +1,10 @@ +--- +title: KwirthMetrics for Backstage +author: Julio Fernandez +authorUrl: https://github.com/jfvilas +category: Monitoring +description: Shows live-streaming charts of Kubernetes resources metrics like CPU, memory, I/O... (you can group and merge data). +documentation: https://github.com/jfvilas/plugin-kwirth-metrics +iconUrl: https://raw.githubusercontent.com/jfvilas/plugin-kwirth-metrics/master/src/assets/kwirthmetrics-logo.png +npmPackageName: '@jfvilas/plugin-kwirth-metrics' +addedDate: '2025-09-14' diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index d7538cd12d..d801d41ca3 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -297,6 +297,11 @@ const config: Config = { specPath: '../plugins/search-backend/src/schema/openapi.yaml', outputDir: '../docs/features/search/api', } satisfies OpenApiPlugin.Options, + scaffolder: { + ...defaultOpenApiOptions, + specPath: '../plugins/scaffolder-backend/src/schema/openapi.yaml', + outputDir: '../docs/features/software-templates/api', + } satisfies OpenApiPlugin.Options, }, }, ], diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 5a408d5d1f..72608f4bac 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -14,6 +14,9 @@ const catalogSidebar = tryToLoadCustomSidebar( const searchSidebar = tryToLoadCustomSidebar( '../docs/features/search/api/sidebar.ts', ); +const scaffolderSidebar = tryToLoadCustomSidebar( + '../docs/features/software-templates/api/sidebar.ts', +); export default { docs: { @@ -229,6 +232,22 @@ export default { 'features/software-templates/dry-run-testing', 'features/software-templates/experimental', 'features/software-templates/templating-extensions', + { + type: 'category', + label: 'API', + link: + scaffolderSidebar.length > 0 + ? { + type: 'generated-index', + title: 'Scaffolder API', + slug: '/category/scaffolder-api', + } + : { + type: 'doc', + id: 'openapi/generated-docs/404', + }, + items: scaffolderSidebar, + }, ], }, { diff --git a/microsite/src/components/pluginsSearch/pluginsSearch.tsx b/microsite/src/components/pluginsSearch/pluginsSearch.tsx new file mode 100644 index 0000000000..68f9a2831a --- /dev/null +++ b/microsite/src/components/pluginsSearch/pluginsSearch.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +type Props = { + searchTerm: string; + onSearchTermChange: (newTerm: string) => void; +}; + +export const PluginsSearch = (props: Props) => { + const { searchTerm, onSearchTermChange } = props; + return ( + onSearchTermChange((e.target as HTMLInputElement).value)} + value={searchTerm} + placeholder="Search plugins..." + className="DocSearch-Input search" + /> + ); +}; diff --git a/microsite/src/pages/plugins/index.tsx b/microsite/src/pages/plugins/index.tsx index 2abdbabf4b..1a87f9c0af 100644 --- a/microsite/src/pages/plugins/index.tsx +++ b/microsite/src/pages/plugins/index.tsx @@ -5,10 +5,11 @@ import { truncateDescription } from '@site/src/util/truncateDescription'; import { ChipCategory } from '@site/src/util/types'; import Layout from '@theme/Layout'; import clsx from 'clsx'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { IPluginData, PluginCard } from './_pluginCard'; import pluginsStyles from './plugins.module.scss'; +import { PluginsSearch } from '@site/src/components/pluginsSearch/pluginsSearch'; interface IPluginsList { corePlugins: IPluginData[]; @@ -55,6 +56,7 @@ const Plugins = () => { const [selectedCategories, setSelectedCategories] = useState([]); const [showCoreFeatures, setShowCoreFeatures] = useState(true); const [showOtherPlugins, setShowOtherPlugins] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); const handleChipClick = (categoryName: string) => { const isSelected = @@ -97,6 +99,34 @@ const Plugins = () => { } }; + const matchesSearch = (pluginData: IPluginData, term: string) => { + if (!term) return true; + const lowerTerm = term.toLowerCase(); + return ( + pluginData.title.toLowerCase().includes(lowerTerm) || + pluginData.description.toLowerCase().includes(lowerTerm) || + pluginData.category.toLowerCase().includes(lowerTerm) || + (pluginData.author && pluginData.author.toLowerCase().includes(lowerTerm)) + ); + }; + + const matchesCategory = (pluginData: IPluginData, categories: string[]) => { + if (categories.length === 0) return true; + return categories.includes(pluginData.category); + }; + + const corePlugins = useMemo(() => { + return plugins.corePlugins + .filter(pluginData => matchesCategory(pluginData, selectedCategories)) + .filter(pluginData => matchesSearch(pluginData, searchTerm)); + }, [selectedCategories, searchTerm]); + + const otherPlugins = useMemo(() => { + return plugins.otherPlugins + .filter(pluginData => matchesCategory(pluginData, selectedCategories)) + .filter(pluginData => matchesSearch(pluginData, searchTerm)); + }, [selectedCategories, searchTerm]); + return (
{ categories={categories} handleChipClick={handleChipClick} /> +
- {showCoreFeatures && ( + {corePlugins.length === 0 && otherPlugins.length === 0 && ( +
+

No plugins found

+

+ We couldn't find any plugins matching your criteria. Please try + adjusting your search or filter settings. +

+
+ )} + + {showCoreFeatures && corePlugins.length > 0 && (
-

Core Features

+

Core Features ({corePlugins.length})

- {plugins.corePlugins - .filter( - pluginData => - !selectedCategories.length || - selectedCategories.includes(pluginData.category), - ) - .map(pluginData => ( - - ))} + {corePlugins.map(pluginData => ( + + ))}
)} - {showOtherPlugins && ( + {showOtherPlugins && otherPlugins.length > 0 && (
-

All Plugins

+

All Plugins ({otherPlugins.length})

Friendly reminder: While we love the variety and contributions of our open source plugins, they haven't been fully vetted by the @@ -158,18 +193,9 @@ const Plugins = () => { your due diligence before installing. Happy exploring!

- {plugins.otherPlugins - .filter( - pluginData => - !selectedCategories.length || - selectedCategories.includes(pluginData.category), - ) - .map(pluginData => ( - - ))} + {otherPlugins.map(pluginData => ( + + ))}
)} diff --git a/microsite/src/pages/plugins/plugins.module.scss b/microsite/src/pages/plugins/plugins.module.scss index fb17e90625..a17e4bc5c0 100644 --- a/microsite/src/pages/plugins/plugins.module.scss +++ b/microsite/src/pages/plugins/plugins.module.scss @@ -76,6 +76,20 @@ height: 1rem; } + :global(.search) { + float: right; + margin-bottom: 1rem; + margin-right: 1rem; + font-size: calc(0.875rem * var(--ifm-button-size-multiplier)); + font-weight: var(--ifm-button-font-weight); + background-color: transparent; + border: var(--ifm-button-border-width) solid var(--ifm-color-primary); + border-radius: var(--ifm-button-border-radius); + color: var(--ifm-color-primary); + width: 220px; + height: 2.2rem; + } + :global(.dropdown) { float: right; margin-bottom: 1rem; diff --git a/package.json b/package.json index d4ebc22bc1..69e30f5cc9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.43.0-next.1", + "version": "1.43.0", "backstage": { "cli": { "new": { @@ -24,10 +24,11 @@ }, "scripts": { "build-storybook": "storybook build --output-dir dist-storybook", + "build-storybook:chromatic": "STORYBOOK_STORY_SET=chromatic storybook build --stats-json --output-dir dist-storybook", "build:all": "backstage-cli repo build --all", - "build:api-docs": "LANG=en_EN yarn build:api-reports --docs --exclude 'plugins/@(adr|adr-backend|adr-common|airbrake|airbrake-backend|allure|analytics-module-ga|analytics-module-ga4|analytics-module-newrelic-browser|apache-airflow|api-docs|api-docs-module-protoc-gen-doc|apollo-explorer|app-visualizer|azure-devops|azure-devops-backend|azure-devops-common|azure-sites|azure-sites-backend|azure-sites-common|badges|badges-backend|bazaar|bazaar-backend|bitbucket-cloud-common|bitrise|catalog-graph|catalog-graphql|catalog-import|catalog-unprocessed-entities|cicd-statistics|cicd-statistics-module-gitlab|circleci|cloudbuild|code-climate|code-coverage|code-coverage-backend|codescene|config-schema|cost-insights|cost-insights-common|dynatrace|entity-feedback|entity-feedback-backend|entity-feedback-common|entity-validation|example-todo-list|example-todo-list-backend|example-todo-list-common|firehydrant|fossa|gcalendar|gcp-projects|git-release-manager|github-actions|github-deployments|github-issues|github-pull-requests-board|gitops-profiles|gocd|graphiql|graphql-backend|graphql-voyager|ilert|jenkins|jenkins-backend|jenkins-common|kafka|kafka-backend|lighthouse|lighthouse-backend|lighthouse-common|linguist|linguist-backend|linguist-common|microsoft-calendar|newrelic|newrelic-dashboard|nomad|nomad-backend|octopus-deploy|opencost|pagerduty|periskop|periskop-backend|playlist|playlist-backend|playlist-common|proxy-backend|puppetdb|rollbar|rollbar-backend|sentry|shortcuts|splunk-on-call|stack-overflow|stack-overflow-backend|stackstorm|tech-radar|tech-radar-2|todo|todo-backend|xcmetrics)'", + "build:api-docs": "LANG=en_EN yarn build:api-reports --docs --exclude 'plugins/@(api-docs|api-docs-module-protoc-gen-doc|app-visualizer|catalog-graph|catalog-import|catalog-unprocessed-entities|config-schema|example-todo-list|example-todo-list-backend)'", "build:api-reports": "yarn build:api-reports:only --tsc", - "build:api-reports:only": "LANG=en_US.UTF-8 NODE_OPTIONS=--max-old-space-size=8192 backstage-repo-tools api-reports --sql-reports --allow-warnings 'packages/backend-app-api,packages/core-components,plugins/+(catalog|catalog-import|git-release-manager|jenkins|kubernetes)' -o ae-undocumented,ae-wrong-input-file-type --validate-release-tags", + "build:api-reports:only": "LANG=en_US.UTF-8 NODE_OPTIONS=--max-old-space-size=8192 backstage-repo-tools api-reports --sql-reports --allow-warnings 'packages/backend-app-api,packages/core-components,plugins/+(catalog|catalog-import|kubernetes)' -o ae-undocumented,ae-wrong-input-file-type --validate-release-tags", "build:backend": "yarn workspace example-backend build", "build:knip-reports": "backstage-repo-tools knip-reports", "build:plugins-report": "node ./scripts/build-plugins-report", @@ -101,15 +102,20 @@ "@changesets/assemble-release-plan@^6.0.0": "patch:@changesets/assemble-release-plan@npm%3A6.0.0#./.yarn/patches/@changesets-assemble-release-plan-npm-6.0.0-f7b3005037.patch", "@material-ui/pickers@^3.2.10": "patch:@material-ui/pickers@npm%3A3.3.11#./.yarn/patches/@material-ui-pickers-npm-3.3.11-1c8f68ea20.patch", "@material-ui/pickers@^3.3.10": "patch:@material-ui/pickers@npm%3A3.3.11#./.yarn/patches/@material-ui-pickers-npm-3.3.11-1c8f68ea20.patch", + "@storybook/react@npm:9.1.5": "patch:@storybook/react@npm%3A9.1.5#~/.yarn/patches/@storybook-react-npm-9.1.5-2331f18b6b.patch", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@yarnpkg/plugin-npm@npm:^3.1.0": "patch:@yarnpkg/plugin-npm@npm%3A3.1.0#~/.yarn/patches/@yarnpkg-plugin-npm-npm-3.1.0-6533d0f5a1.patch", "ast-types@0.14.2": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch", "ast-types@^0.14.1": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch", + "ast-types@npm:0.14.2": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", + "ast-types@npm:^0.13.4": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", + "ast-types@npm:^0.16.1": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", "csstype@npm:^3.0.2": "3.0.9", "csstype@npm:^3.1.2": "3.0.9", "csstype@npm:^3.1.3": "3.0.9", - "jest-haste-map@^29.7.0": "patch:jest-haste-map@npm%3A29.7.0#./.yarn/patches/jest-haste-map-npm-29.7.0-e3be419eff.patch" + "jest-haste-map@^29.7.0": "patch:jest-haste-map@npm%3A29.7.0#./.yarn/patches/jest-haste-map-npm-29.7.0-e3be419eff.patch", + "recast@npm:0.23.9>ast-types": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch" }, "dependencies": { "@backstage/errors": "workspace:^", @@ -129,13 +135,11 @@ "@octokit/rest": "^19.0.3", "@playwright/test": "^1.32.3", "@spotify/eslint-plugin": "^15.0.0", - "@storybook/addon-essentials": "^8.6.12", - "@storybook/addon-interactions": "^8.6.12", - "@storybook/addon-links": "^8.6.12", - "@storybook/addon-storysource": "^8.6.12", - "@storybook/addon-themes": "^8.6.12", - "@storybook/react": "^8.6.12", - "@storybook/react-vite": "^8.6.12", + "@storybook/addon-a11y": "^9.1.5", + "@storybook/addon-docs": "^9.1.5", + "@storybook/addon-links": "^9.1.5", + "@storybook/addon-themes": "^9.1.5", + "@storybook/react-vite": "^9.1.5", "@techdocs/cli": "workspace:*", "@types/cacheable-request": "^8.3.6", "@types/memjs": "^1.3.3", @@ -160,10 +164,10 @@ "shx": "^0.4.0", "sloc": "^0.3.1", "sort-package-json": "^2.8.0", - "storybook": "^8.6.12", + "storybook": "^9.1.5", "typedoc": "^0.28.0", "typescript": "~5.7.0", - "vite": "^7.1.2" + "vite": "^7.1.5" }, "packageManager": "yarn@4.8.1", "engines": { diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index f22c96ceca..02f126bcf5 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/app-defaults +## 1.7.0 + +### Minor Changes + +- 9956704: Add and configure the OpenShift authentication provider to the default APIs. + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-app-api@1.19.0 + ## 1.6.6-next.0 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 9f972eccbf..37973fbf0f 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/app-defaults", - "version": "1.6.6-next.0", + "version": "1.7.0", "description": "Provides the default wiring of a Backstage App", "backstage": { "role": "web-library" diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index 63ace7b4d5..97989dff91 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -35,6 +35,7 @@ import { FetchMiddlewares, VMwareCloudAuth, FrontendHostDiscovery, + OpenShiftAuth, } from '@backstage/core-app-api'; import { @@ -58,6 +59,7 @@ import { bitbucketServerAuthApiRef, atlassianAuthApiRef, vmwareCloudAuthApiRef, + openshiftAuthApiRef, } from '@backstage/core-plugin-api'; import { permissionApiRef, @@ -275,6 +277,22 @@ export const apis = [ }); }, }), + createApiFactory({ + api: openshiftAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => { + return OpenShiftAuth.create({ + configApi, + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }); + }, + }), createApiFactory({ api: permissionApiRef, deps: { diff --git a/packages/app-next-example-plugin/CHANGELOG.md b/packages/app-next-example-plugin/CHANGELOG.md index 5748f8f75b..7949e120fa 100644 --- a/packages/app-next-example-plugin/CHANGELOG.md +++ b/packages/app-next-example-plugin/CHANGELOG.md @@ -1,5 +1,13 @@ # app-next-example-plugin +## 0.0.27 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-components@0.18.0 + ## 0.0.27-next.0 ### Patch Changes diff --git a/packages/app-next-example-plugin/knip-report.md b/packages/app-next-example-plugin/knip-report.md index e01cd18c93..0f2b9c5ebc 100644 --- a/packages/app-next-example-plugin/knip-report.md +++ b/packages/app-next-example-plugin/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :---------- | :----------- | :------- | -| cross-fetch | package.json | error | -| msw | package.json | error | +| cross-fetch | packages/app-next-example-plugin/package.json | error | +| msw | packages/app-next-example-plugin/package.json | error | diff --git a/packages/app-next-example-plugin/package.json b/packages/app-next-example-plugin/package.json index 7878658b5d..3b76202029 100644 --- a/packages/app-next-example-plugin/package.json +++ b/packages/app-next-example-plugin/package.json @@ -1,6 +1,6 @@ { "name": "app-next-example-plugin", - "version": "0.0.27-next.0", + "version": "0.0.27", "description": "Backstage internal example plugin", "backstage": { "role": "frontend-plugin", diff --git a/packages/app-next/CHANGELOG.md b/packages/app-next/CHANGELOG.md index 5fc353c09d..7e66d0d041 100644 --- a/packages/app-next/CHANGELOG.md +++ b/packages/app-next/CHANGELOG.md @@ -1,5 +1,73 @@ # example-app-next +## 0.0.27 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.34.2 + - @backstage/ui@0.7.1 + - @backstage/plugin-techdocs@1.15.0 + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-notifications@0.5.9 + - @backstage/app-defaults@1.7.0 + - @backstage/plugin-catalog-graph@0.5.0 + - @backstage/plugin-org@0.6.44 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-auth-react@0.1.19 + - @backstage/plugin-auth@0.1.0 + - @backstage/plugin-home@0.8.12 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-scaffolder@1.34.1 + - @backstage/plugin-app@0.3.0 + - @backstage/plugin-user-settings@0.8.26 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-components@0.18.0 + - @backstage/frontend-app-api@0.13.0 + - @backstage/frontend-defaults@0.3.1 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-scaffolder-react@1.19.1 + - @backstage/core-app-api@1.19.0 + - @backstage/plugin-api-docs@0.12.11 + - @backstage/plugin-app-visualizer@0.1.23 + - @backstage/plugin-catalog-import@0.13.5 + - @backstage/plugin-catalog-unprocessed-entities@0.2.21 + - @backstage/plugin-kubernetes@0.12.11 + - @backstage/plugin-search@1.4.30 + - @backstage/plugin-search-react@1.9.4 + - @backstage/plugin-signals@0.0.23 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28 + - @backstage/integration-react@1.2.10 + - @backstage/plugin-kubernetes-cluster@0.0.29 + +## 0.0.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.7.1-next.0 + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/plugin-auth@0.1.0-next.0 + - @backstage/plugin-home@0.8.12-next.2 + - @backstage/plugin-scaffolder@1.34.1-next.2 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-user-settings@0.8.26-next.2 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/cli@0.34.2-next.2 + - @backstage/plugin-catalog-graph@0.4.23-next.2 + - @backstage/plugin-catalog-import@0.13.5-next.2 + - @backstage/plugin-org@0.6.44-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + - @backstage/core-compat-api@0.5.2-next.2 + - @backstage/plugin-api-docs@0.12.11-next.2 + - @backstage/plugin-kubernetes@0.12.11-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.29-next.2 + - @backstage/plugin-search@1.4.30-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28-next.0 + ## 0.0.27-next.1 ### Patch Changes diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index f83e4d4ba0..35380c961f 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -4,7 +4,6 @@ app: routes: bindings: catalog.viewTechDoc: techdocs.docRoot - catalog.createComponent: catalog-import.importPage org.catalogIndex: catalog.catalogIndex pluginOverrides: diff --git a/packages/app-next/knip-report.md b/packages/app-next/knip-report.md index de7de83ffc..8513269386 100644 --- a/packages/app-next/knip-report.md +++ b/packages/app-next/knip-report.md @@ -4,40 +4,40 @@ | Name | Location | Severity | | :----------------------------------------------- | :----------- | :------- | -| @backstage/plugin-techdocs-module-addons-contrib | package.json | error | -| @backstage/plugin-catalog-unprocessed-entities | package.json | error | -| @backstage/plugin-kubernetes-cluster | package.json | error | -| @backstage/plugin-permission-react | package.json | error | -| @backstage/plugin-scaffolder-react | package.json | error | -| @backstage/plugin-catalog-common | package.json | error | -| @backstage/plugin-techdocs-react | package.json | error | -| @backstage/plugin-catalog-graph | package.json | error | -| @backstage/plugin-search-common | package.json | error | -| @backstage/plugin-search-react | package.json | error | -| @backstage/integration-react | package.json | error | -| @backstage/plugin-auth-react | package.json | error | -| @backstage/plugin-scaffolder | package.json | error | -| @backstage/core-plugin-api | package.json | error | -| @backstage/plugin-api-docs | package.json | error | -| @backstage/plugin-catalog | package.json | error | -| @backstage/plugin-signals | package.json | error | -| @backstage/app-defaults | package.json | error | -| @backstage/plugin-app | package.json | error | -| @backstage/plugin-org | package.json | error | -| @backstage/config | package.json | error | -| @material-ui/lab | package.json | error | -| zen-observable | package.json | error | -| @octokit/rest | package.json | error | -| react-use | package.json | error | -| history | package.json | error | +| @backstage/plugin-techdocs-module-addons-contrib | packages/app-next/package.json | error | +| @backstage/plugin-catalog-unprocessed-entities | packages/app-next/package.json | error | +| @backstage/plugin-kubernetes-cluster | packages/app-next/package.json | error | +| @backstage/plugin-permission-react | packages/app-next/package.json | error | +| @backstage/plugin-scaffolder-react | packages/app-next/package.json | error | +| @backstage/plugin-catalog-common | packages/app-next/package.json | error | +| @backstage/plugin-techdocs-react | packages/app-next/package.json | error | +| @backstage/plugin-catalog-graph | packages/app-next/package.json | error | +| @backstage/plugin-search-common | packages/app-next/package.json | error | +| @backstage/plugin-search-react | packages/app-next/package.json | error | +| @backstage/integration-react | packages/app-next/package.json | error | +| @backstage/plugin-auth-react | packages/app-next/package.json | error | +| @backstage/plugin-scaffolder | packages/app-next/package.json | error | +| @backstage/core-plugin-api | packages/app-next/package.json | error | +| @backstage/plugin-api-docs | packages/app-next/package.json | error | +| @backstage/plugin-catalog | packages/app-next/package.json | error | +| @backstage/plugin-signals | packages/app-next/package.json | error | +| @backstage/app-defaults | packages/app-next/package.json | error | +| @backstage/plugin-app | packages/app-next/package.json | error | +| @backstage/plugin-org | packages/app-next/package.json | error | +| @backstage/config | packages/app-next/package.json | error | +| @material-ui/lab | packages/app-next/package.json | error | +| zen-observable | packages/app-next/package.json | error | +| @octokit/rest | packages/app-next/package.json | error | +| react-use | packages/app-next/package.json | error | +| history | packages/app-next/package.json | error | ## Unused devDependencies (5) | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @testing-library/user-event | package.json | error | -| @types/zen-observable | package.json | error | -| @testing-library/dom | package.json | error | -| @types/jquery | package.json | error | -| cross-env | package.json | error | +| @testing-library/user-event | packages/app-next/package.json | error | +| @types/zen-observable | packages/app-next/package.json | error | +| @testing-library/dom | packages/app-next/package.json | error | +| @types/jquery | packages/app-next/package.json | error | +| cross-env | packages/app-next/package.json | error | diff --git a/packages/app-next/package.json b/packages/app-next/package.json index 0a39471e34..f6f9f52e8d 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -1,6 +1,6 @@ { "name": "example-app-next", - "version": "0.0.27-next.1", + "version": "0.0.27", "backstage": { "role": "frontend" }, @@ -49,6 +49,7 @@ "@backstage/plugin-api-docs": "workspace:^", "@backstage/plugin-app": "workspace:^", "@backstage/plugin-app-visualizer": "workspace:^", + "@backstage/plugin-auth": "workspace:^", "@backstage/plugin-auth-react": "workspace:^", "@backstage/plugin-catalog": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 93dd1db16a..533f4ab84c 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,66 @@ # example-app +## 0.2.113 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.34.2 + - @backstage/ui@0.7.1 + - @backstage/plugin-techdocs@1.15.0 + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-notifications@0.5.9 + - @backstage/app-defaults@1.7.0 + - @backstage/plugin-catalog-graph@0.5.0 + - @backstage/plugin-org@0.6.44 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/plugin-auth-react@0.1.19 + - @backstage/plugin-home@0.8.12 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-scaffolder@1.34.1 + - @backstage/plugin-user-settings@0.8.26 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-components@0.18.0 + - @backstage/frontend-app-api@0.13.0 + - @backstage/plugin-scaffolder-react@1.19.1 + - @backstage/core-app-api@1.19.0 + - @backstage/plugin-api-docs@0.12.11 + - @backstage/plugin-catalog-import@0.13.5 + - @backstage/plugin-catalog-unprocessed-entities@0.2.21 + - @backstage/plugin-devtools@0.1.31 + - @backstage/plugin-kubernetes@0.12.11 + - @backstage/plugin-search@1.4.30 + - @backstage/plugin-search-react@1.9.4 + - @backstage/plugin-signals@0.0.23 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28 + - @backstage/integration-react@1.2.10 + - @backstage/plugin-kubernetes-cluster@0.0.29 + +## 0.2.113-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.7.1-next.0 + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/plugin-home@0.8.12-next.2 + - @backstage/plugin-scaffolder@1.34.1-next.2 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-user-settings@0.8.26-next.2 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/cli@0.34.2-next.2 + - @backstage/plugin-catalog-graph@0.4.23-next.2 + - @backstage/plugin-catalog-import@0.13.5-next.2 + - @backstage/plugin-org@0.6.44-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + - @backstage/plugin-api-docs@0.12.11-next.2 + - @backstage/plugin-kubernetes@0.12.11-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.29-next.2 + - @backstage/plugin-search@1.4.30-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28-next.0 + ## 0.2.113-next.1 ### Patch Changes diff --git a/packages/app/knip-report.md b/packages/app/knip-report.md index b05d232915..753f119c52 100644 --- a/packages/app/knip-report.md +++ b/packages/app/knip-report.md @@ -4,26 +4,26 @@ | Name | Location | Severity | | :------------------------------ | :----------- | :------- | -| @backstage/plugin-search-common | package.json | error | -| @backstage/plugin-auth-react | package.json | error | -| @backstage/frontend-app-api | package.json | error | -| @material-ui/lab | package.json | error | -| zen-observable | package.json | error | -| @octokit/rest | package.json | error | -| react-router | package.json | error | -| history | package.json | error | +| @backstage/plugin-search-common | packages/app/package.json | error | +| @backstage/plugin-auth-react | packages/app/package.json | error | +| @backstage/frontend-app-api | packages/app/package.json | error | +| @material-ui/lab | packages/app/package.json | error | +| zen-observable | packages/app/package.json | error | +| @octokit/rest | packages/app/package.json | error | +| react-router | packages/app/package.json | error | +| history | packages/app/package.json | error | ## Unused devDependencies (3) | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @testing-library/user-event | package.json | error | -| @types/zen-observable | package.json | error | -| @types/jquery | package.json | error | +| @testing-library/user-event | packages/app/package.json | error | +| @types/zen-observable | packages/app/package.json | error | +| @types/jquery | packages/app/package.json | error | ## Unlisted dependencies (1) | Name | Location | Severity | | :---------- | :------------------------------------------------------- | :------- | -| @rjsf/utils | src/components/scaffolder/customScaffolderExtensions.tsx | error | +| @rjsf/utils | packages/app/src/components/scaffolder/customScaffolderExtensions.tsx | error | diff --git a/packages/app/package.json b/packages/app/package.json index 580c401a1b..90ffcd0b3b 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.113-next.1", + "version": "0.2.113", "backstage": { "role": "frontend" }, @@ -94,7 +94,7 @@ "@types/react": "*", "@types/react-dom": "*", "@types/zen-observable": "^0.8.0", - "axios": "^1.11.0", + "axios": "^1.12.0", "cross-env": "^7.0.0", "msw": "^1.0.0" }, diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 5ac9052d2a..ad525420a7 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -14,18 +14,6 @@ * limitations under the License. */ -import { - RELATION_API_CONSUMED_BY, - RELATION_API_PROVIDED_BY, - RELATION_CONSUMES_API, - RELATION_DEPENDENCY_OF, - RELATION_DEPENDS_ON, - RELATION_HAS_PART, - RELATION_OWNED_BY, - RELATION_OWNER_OF, - RELATION_PART_OF, - RELATION_PROVIDES_API, -} from '@backstage/catalog-model'; import { createApp } from '@backstage/app-defaults'; import { AppRouter, FeatureFlagged, FlatRoutes } from '@backstage/core-app-api'; import { @@ -158,18 +146,6 @@ const routes = ( } diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index 66f1460210..372477cd63 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -23,6 +23,7 @@ import { oneloginAuthApiRef, bitbucketAuthApiRef, bitbucketServerAuthApiRef, + openshiftAuthApiRef, } from '@backstage/core-plugin-api'; export const providers = [ @@ -74,4 +75,10 @@ export const providers = [ message: 'Sign In using Bitbucket Server', apiRef: bitbucketServerAuthApiRef, }, + { + id: 'openshift-auth-provider', + title: 'OpenShift', + message: 'Sign In using OpenShift', + apiRef: openshiftAuthApiRef, + }, ]; diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md index a0d274a400..d965783fac 100644 --- a/packages/backend-app-api/CHANGELOG.md +++ b/packages/backend-app-api/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/backend-app-api +## 1.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + ## 1.2.7-next.0 ### Patch Changes diff --git a/packages/backend-app-api/knip-report.md b/packages/backend-app-api/knip-report.md index 13b4ec495e..d056972992 100644 --- a/packages/backend-app-api/knip-report.md +++ b/packages/backend-app-api/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/backend-defaults | package.json | error | +| @backstage/backend-defaults | packages/backend-app-api/package.json | error | diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index f20cd2e901..9407f28707 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-app-api", - "version": "1.2.7-next.0", + "version": "1.2.7", "description": "Core API used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index 0375e54232..1adfd0f80e 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/backend-defaults +## 0.12.1 + +### Patch Changes + +- 33bd4d0: Deduplicate discovered features discovered with discoveryFeatureLoader +- 4eda590: Fixed cache namespace and key prefix separator configuration to properly use configured values instead of hardcoded plugin ID. The cache manager now correctly combines the configured namespace with plugin IDs using the configured separator for Redis and Valkey. Memcache and memory store continue to use plugin ID as namespace. +- f244e61: Add `backend.logger` config options to configure the `RootLoggerService`. + + Read more about the new configuration options in the + [Root Logger Service](https://backstage.io/docs/backend-system/core-services/root-logger/) + documentation. + +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-app-api@1.2.7 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + +## 0.12.1-next.1 + +### Patch Changes + +- 4eda590: Fixed cache namespace and key prefix separator configuration to properly use configured values instead of hardcoded plugin ID. The cache manager now correctly combines the configured namespace with plugin IDs using the configured separator for Redis and Valkey. Memcache and memory store continue to use plugin ID as namespace. +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/integration-aws-node@0.1.17 + ## 0.12.1-next.0 ### Patch Changes diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts index 4fd3aca193..2982557f32 100644 --- a/packages/backend-defaults/config.d.ts +++ b/packages/backend-defaults/config.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { HumanDuration } from '@backstage/types'; +import { HumanDuration, JsonObject } from '@backstage/types'; export interface Config { app: { @@ -985,6 +985,66 @@ export interface Config { headers?: { [name: string]: string }; }; + /** + * Options to configure the default RootLoggerService. + */ + logger?: { + /** + * Configures the global log level for messages. + * + * This can also be configured using the LOG_LEVEL environment variable, which + * takes precedence over this configuration. + * + * Defaults to 'info'. + */ + level?: 'debug' | 'info' | 'warn' | 'error'; + + /** + * Additional metadata to include with every log entry. + */ + meta?: JsonObject; + + /** + * List of logger overrides. + * + * Can be used to configure a different level for logs matching certain criterias. + * For example, it can be used to ignore 'info' logs of given plugins. + * + * @example + * + * ```yaml + * logger: + * level: info + * overrides: + * # For catalog and auth plugins, messages less important than 'warn' will be ignored. + * - matchers: + * plugin: [catalog, auth] + * level: warn + * # Ignore all messages that starts with 'Forget' + * - matchers: + * message: '/^Forget/' + * level: warn + * ``` + */ + overrides?: Array<{ + /** + * Conditions that must be met to override the log level. + * + * A matcher can be: + * + * - A string (exact match or regex pattern delimited by slashes, e.g. `/pattern/`) + * - A non-string value (compared by strict equality) + * - An array of matchers (returns true if any matcher matches) + */ + matchers: JsonObject; + + /** + * Log level to use for matched entries. + */ + level: 'debug' | 'info' | 'warn' | 'error'; + }>; + }; + /** * Rate limiting options. Defining this as `true` will enable rate limiting with default values. */ diff --git a/packages/backend-defaults/knip-report.md b/packages/backend-defaults/knip-report.md index 169963316a..8951112aaa 100644 --- a/packages/backend-defaults/knip-report.md +++ b/packages/backend-defaults/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :------------- | :----------- | :------- | -| better-sqlite3 | package.json | error | -| mysql2 | package.json | error | +| better-sqlite3 | packages/backend-defaults/package.json | error | +| mysql2 | packages/backend-defaults/package.json | error | ## Referenced optional peerDependencies (1) | Name | Location | Severity | | :-------------------------------- | :----------- | :------- | -| @google-cloud/cloud-sql-connector | package.json | error | +| @google-cloud/cloud-sql-connector | packages/backend-defaults/package.json | error | diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 09bb4dc85d..013e32db59 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-defaults", - "version": "0.12.1-next.0", + "version": "0.12.1", "description": "Backend defaults used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/backend-defaults/report-rootLogger.api.md b/packages/backend-defaults/report-rootLogger.api.md index b353039c03..99dc263358 100644 --- a/packages/backend-defaults/report-rootLogger.api.md +++ b/packages/backend-defaults/report-rootLogger.api.md @@ -3,8 +3,10 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { config } from 'winston'; import { Format } from 'logform'; import { JsonObject } from '@backstage/types'; +import { JsonPrimitive } from '@backstage/types'; import { LoggerService } from '@backstage/backend-plugin-api'; import { RootLoggerService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; @@ -31,14 +33,31 @@ export class WinstonLogger implements RootLoggerService { error(message: string, meta?: JsonObject): void; // (undocumented) info(message: string, meta?: JsonObject): void; + static logLevelFilter(defaultLogLevel: keyof config.NpmConfigSetLevels): { + format: Format; + setOverrides: (overrides: WinstonLoggerLevelOverride[]) => void; + }; static redacter(): { format: Format; add: (redactions: Iterable) => void; }; // (undocumented) + setLevelOverrides(overrides: WinstonLoggerLevelOverride[]): void; + // (undocumented) warn(message: string, meta?: JsonObject): void; } +// @public (undocumented) +export type WinstonLoggerLevelOverride = { + matchers: WinstonLoggerLevelOverrideMatchers; + level: string; +}; + +// @public (undocumented) +export type WinstonLoggerLevelOverrideMatchers = { + [key: string]: JsonPrimitive | JsonPrimitive[] | undefined; +}; + // @public (undocumented) export interface WinstonLoggerOptions { // (undocumented) diff --git a/packages/backend-defaults/src/PackageDiscoveryService.ts b/packages/backend-defaults/src/PackageDiscoveryService.ts index fe0ab931a7..abf015baf3 100644 --- a/packages/backend-defaults/src/PackageDiscoveryService.ts +++ b/packages/backend-defaults/src/PackageDiscoveryService.ts @@ -175,6 +175,6 @@ export class PackageDiscoveryService { } } - return { features }; + return { features: Array.from(new Set(features)) }; } } diff --git a/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts index 4e9475b66e..a308caa222 100644 --- a/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts @@ -333,4 +333,147 @@ describe('CacheManager store options', () => { keyPrefixSeparator: '!', }); }); + + it('correctly applies namespace configuration to redis and valkey stores', () => { + const testCases = [ + { store: 'redis', namespace: 'test1', separator: ':' }, + { store: 'valkey', namespace: 'test2', separator: '!' }, + ]; + + testCases.forEach(({ store, namespace, separator }) => { + const manager = CacheManager.fromConfig( + mockServices.rootConfig({ + data: { + backend: { + cache: { + store, + connection: 'redis://localhost:6379', + [store]: { + client: { + namespace, + keyPrefixSeparator: separator, + }, + }, + }, + }, + }, + }), + ); + + manager.forPlugin('testPlugin'); + + if (store === 'redis') { + // eslint-disable-next-line jest/no-conditional-expect + expect(KeyvRedis).toHaveBeenCalledWith('redis://localhost:6379', { + namespace, + keyPrefixSeparator: separator, + }); + } else if (store === 'valkey') { + // eslint-disable-next-line jest/no-conditional-expect + expect(KeyvValkey).toHaveBeenCalledWith('redis://localhost:6379', { + namespace, + keyPrefixSeparator: separator, + }); + } + }); + }); + + it('falls back to pluginId when no namespace is configured', () => { + const manager = CacheManager.fromConfig( + mockServices.rootConfig({ + data: { + backend: { + cache: { + store: 'redis', + connection: 'redis://localhost:6379', + }, + }, + }, + }), + ); + + manager.forPlugin('testPlugin'); + + expect(KeyvRedis).toHaveBeenCalledWith('redis://localhost:6379', { + keyPrefixSeparator: ':', + }); + }); + + describe('Namespace construction', () => { + it('returns pluginId when no store options are provided', () => { + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + undefined, + ); + expect(result).toBe('testPlugin'); + }); + + it('returns pluginId when store options have no namespace', () => { + const storeOptions = { + client: { + keyPrefixSeparator: ':', + }, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('testPlugin'); + }); + + it('combines namespace and pluginId with default separator', () => { + const storeOptions = { + client: { + namespace: 'my-app', + keyPrefixSeparator: ':', + }, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('my-app:testPlugin'); + }); + + it('combines namespace and pluginId with custom separator', () => { + const storeOptions = { + client: { + namespace: 'my-app', + keyPrefixSeparator: '-', + }, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('my-app-testPlugin'); + }); + + it('uses default separator when keyPrefixSeparator is not provided', () => { + const storeOptions = { + client: { + namespace: 'my-app', + }, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('my-app:testPlugin'); + }); + + it('handles empty namespace by falling back to pluginId', () => { + const storeOptions = { + client: { + namespace: '', + keyPrefixSeparator: ':', + }, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('testPlugin'); + }); + }); }); diff --git a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts index 1c26083525..06b2b0c57b 100644 --- a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts @@ -213,6 +213,26 @@ export class CacheManager { return redisOptions; } + /** + * Construct the full namespace based on the options and pluginId. + * + * @param pluginId - The plugin ID to namespace + * @param storeOptions - Optional cache store configuration options + * @returns The constructed namespace string combining the configured namespace with pluginId + */ + private static constructNamespace( + pluginId: string, + storeOptions: RedisCacheStoreOptions | undefined, + ): string { + const prefix = storeOptions?.client?.namespace + ? `${storeOptions.client.namespace}${ + storeOptions.client.keyPrefixSeparator ?? ':' + }` + : ''; + + return `${prefix}${pluginId}`; + } + /** @internal */ constructor( store: string, @@ -286,7 +306,7 @@ export class CacheManager { }); } return new Keyv({ - namespace: pluginId, + namespace: CacheManager.constructNamespace(pluginId, this.storeOptions), ttl: defaultTtl, store: stores[pluginId], emitErrors: false, @@ -326,7 +346,7 @@ export class CacheManager { }); } return new Keyv({ - namespace: pluginId, + namespace: CacheManager.constructNamespace(pluginId, this.storeOptions), ttl: defaultTtl, store: stores[pluginId], emitErrors: false, diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.test.ts b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.test.ts index 7208c22d39..7dba10cccf 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.test.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.test.ts @@ -118,4 +118,82 @@ describe('WinstonLogger', () => { add([null as any, undefined as any, 'valid-secret']); }).not.toThrow(); }); + + it('should filter logs below the default log level', () => { + const mockTransport = new Transport({ + log: jest.fn(), + logv: jest.fn(), + }); + + const logger = WinstonLogger.create({ + level: 'warn', + format: format.json(), + transports: [mockTransport], + }); + + logger.debug('debug log'); + + expect(mockTransport.log).not.toHaveBeenCalled(); + }); + + it('should not filter logs below the default log level with an override', () => { + const mockTransport = new Transport({ + log: jest.fn(), + logv: jest.fn(), + }); + + const logger = WinstonLogger.create({ + level: 'warn', + format: format.json(), + transports: [mockTransport], + }); + + logger.setLevelOverrides([ + { + matchers: { + plugin: 'catalog', + }, + level: 'debug', + }, + ]); + + logger.debug('debug log', { plugin: 'catalog' }); + + expect(mockTransport.log).toHaveBeenCalledWith( + expect.objectContaining({ + [MESSAGE]: JSON.stringify({ + level: 'debug', + message: 'debug log', + plugin: 'catalog', + }), + }), + expect.any(Function), + ); + }); + + it('should filter logs above the default log level with an override', () => { + const mockTransport = new Transport({ + log: jest.fn(), + logv: jest.fn(), + }); + + const logger = WinstonLogger.create({ + level: 'debug', + format: format.json(), + transports: [mockTransport], + }); + + logger.setLevelOverrides([ + { + matchers: { + plugin: 'catalog', + }, + level: 'error', + }, + ]); + + logger.info('info log', { plugin: 'catalog' }); + + expect(mockTransport.log).not.toHaveBeenCalled(); + }); }); diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts index 1b6b9db900..c48cef2f1f 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts @@ -26,9 +26,12 @@ import { createLogger, transports, transport as Transport, + config as winstonConfig, } from 'winston'; import { MESSAGE } from 'triple-beam'; import { escapeRegExp } from '../../lib/escapeRegExp'; +import { winstonLevels, WinstonLoggerLevelOverride } from './types'; +import { createLogMatcher } from './utils'; /** * @public @@ -48,20 +51,27 @@ export interface WinstonLoggerOptions { export class WinstonLogger implements RootLoggerService { #winston: Logger; #addRedactions?: (redactions: Iterable) => void; + #setLevelOverrides?: (overrides: WinstonLoggerLevelOverride[]) => void; /** * Creates a {@link WinstonLogger} instance. */ static create(options: WinstonLoggerOptions): WinstonLogger { + const defaultLogLevel = process.env.LOG_LEVEL || options.level || 'info'; + const redacter = WinstonLogger.redacter(); + const logLevelFilter = WinstonLogger.logLevelFilter(defaultLogLevel); + const defaultFormatter = process.env.NODE_ENV === 'production' ? format.json() : WinstonLogger.colorFormat(); let logger = createLogger({ - level: process.env.LOG_LEVEL || options.level || 'info', + // Lowest level possible as we let the logLevelFilter do the filtering + level: 'silly', format: format.combine( + logLevelFilter.format, options.format ?? defaultFormatter, redacter.format, ), @@ -72,7 +82,7 @@ export class WinstonLogger implements RootLoggerService { logger = logger.child(options.meta); } - return new WinstonLogger(logger, redacter.add); + return new WinstonLogger(logger, redacter.add, logLevelFilter.setOverrides); } /** @@ -169,12 +179,61 @@ export class WinstonLogger implements RootLoggerService { ); } + /** + * Formatter that filters log levels using overrides, falling back to the default level when no criteria match. + */ + static logLevelFilter( + defaultLogLevel: keyof winstonConfig.NpmConfigSetLevels, + ): { + format: Format; + setOverrides: (overrides: WinstonLoggerLevelOverride[]) => void; + } { + const overrides: { + predicate: (log: TransformableInfo) => boolean; + level: string; + }[] = []; + + return { + format: format(log => { + for (const override of overrides) { + if (override.predicate(log)) { + // Discard the log if the log level is below the override + // eg, if the override level is 'warn' (1) and the log is 'debug' (5) + if (winstonLevels[log.level] > winstonLevels[override.level]) { + return false; + } + + return log; + } + } + + // Ignore logs that are below the global level + // eg, if the global level is 'warn' (1) and the log level is 'debug' (5) + if (winstonLevels[log.level] > winstonLevels[defaultLogLevel]) { + return false; + } + + return log; + })(), + setOverrides: newOverrides => { + const newOverridesPredicates = newOverrides.map(o => ({ + predicate: createLogMatcher(o.matchers), + level: o.level, + })); + // Replace the content while preserving the reference to support live config updates + overrides.splice(0, overrides.length, ...newOverridesPredicates); + }, + }; + } + private constructor( winston: Logger, addRedactions?: (redactions: Iterable) => void, + setLevelOverrides?: (overrides: WinstonLoggerLevelOverride[]) => void, ) { this.#winston = winston; this.#addRedactions = addRedactions; + this.#setLevelOverrides = setLevelOverrides; } error(message: string, meta?: JsonObject): void { @@ -200,4 +259,8 @@ export class WinstonLogger implements RootLoggerService { addRedactions(redactions: Iterable) { this.#addRedactions?.(redactions); } + + setLevelOverrides(overrides: WinstonLoggerLevelOverride[]) { + this.#setLevelOverrides?.(overrides); + } } diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/config.test.ts b/packages/backend-defaults/src/entrypoints/rootLogger/config.test.ts new file mode 100644 index 0000000000..e32cc13247 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLogger/config.test.ts @@ -0,0 +1,75 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { getRootLoggerConfig } from './config'; + +describe('getRootLoggerConfig', () => { + it('should load the configuration without throwing', () => { + const config = { + backend: { + logger: { + level: 'info', + meta: { + env: 'prod', + }, + overrides: [ + { + matchers: { + plugin: 'catalog', + }, + level: 'warn', + }, + ], + }, + }, + }; + + expect(() => + getRootLoggerConfig( + mockServices.rootConfig({ + data: config, + }), + ), + ).not.toThrow(); + }); + + it('should throw if an override is using an invalid level', () => { + const config = { + backend: { + logger: { + level: 'info', + meta: { + env: 'prod', + }, + overrides: [ + { + matchers: { + plugin: 'catalog', + }, + level: 'invalid', + }, + ], + }, + }, + }; + + expect(() => + getRootLoggerConfig(mockServices.rootConfig({ data: config })), + ).toThrow( + "Invalid config at backend.logger.overrides[0].level, 'invalid' is not a valid logging level, must be one of 'error', 'warn', 'info' or 'debug'.", + ); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/config.ts b/packages/backend-defaults/src/entrypoints/rootLogger/config.ts new file mode 100644 index 0000000000..11687d72f4 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLogger/config.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { RootConfigService } from '@backstage/backend-plugin-api'; +import { JsonObject } from '@backstage/types'; +import { + RootLoggerConfig, + winstonLevels, + WinstonLoggerLevelOverrideMatchers, +} from './types'; + +export const getRootLoggerConfig = ( + config: RootConfigService, +): RootLoggerConfig => { + const level = config.getOptionalString('backend.logger.level'); + const meta = config + .getOptionalConfig('backend.logger.meta') + ?.get(); + + const overridesConfig = config.getOptionalConfigArray( + 'backend.logger.overrides', + ); + const overrides = overridesConfig?.map((override, i) => { + const overrideLevel = override.getString('level'); + if (winstonLevels[overrideLevel] === undefined) { + throw new Error( + `Invalid config at backend.logger.overrides[${i}].level, '${overrideLevel}' is not a valid logging level, must be one of 'error', 'warn', 'info' or 'debug'.`, + ); + } + + const matchers = override + .getConfig('matchers') + .get(); + + return { + matchers, + level: overrideLevel, + }; + }); + + return { + meta, + level, + overrides, + }; +}; diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/index.ts b/packages/backend-defaults/src/entrypoints/rootLogger/index.ts index 96ed402520..676c1f5398 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/index.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/index.ts @@ -16,3 +16,7 @@ export { rootLoggerServiceFactory } from './rootLoggerServiceFactory'; export { WinstonLogger, type WinstonLoggerOptions } from './WinstonLogger'; +export { + type WinstonLoggerLevelOverride, + type WinstonLoggerLevelOverrideMatchers, +} from './types'; diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.test.ts new file mode 100644 index 0000000000..4b807aafc8 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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, + ServiceFactoryTester, +} from '@backstage/backend-test-utils'; +import { rootLoggerServiceFactory } from './rootLoggerServiceFactory'; + +import { WinstonLogger } from './WinstonLogger'; + +describe('rootLoggerServiceFactory', () => { + beforeEach(() => { + jest.spyOn(WinstonLogger, 'create'); + }); + + it('should create WinstonLogger with defaults', async () => { + await ServiceFactoryTester.from(rootLoggerServiceFactory, { + dependencies: [mockServices.rootConfig.factory()], + }).getSubject(); + + expect(WinstonLogger.create).toHaveBeenCalledWith({ + level: 'info', + meta: { + service: 'backstage', + }, + format: expect.anything(), + transports: expect.anything(), + }); + }); + + it('should create WinstonLogger from config', async () => { + await ServiceFactoryTester.from(rootLoggerServiceFactory, { + dependencies: [ + mockServices.rootConfig.factory({ + data: { + backend: { + logger: { + meta: { + env: 'test', + }, + level: 'warn', + }, + }, + }, + }), + ], + }).getSubject(); + + expect(WinstonLogger.create).toHaveBeenCalledWith({ + level: 'warn', + meta: { + service: 'backstage', + env: 'test', + }, + format: expect.anything(), + transports: expect.anything(), + }); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.ts index 5a4427cf5f..abd81e575b 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.ts @@ -15,12 +15,13 @@ */ import { - createServiceFactory, coreServices, + createServiceFactory, } from '@backstage/backend-plugin-api'; -import { transports, format } from 'winston'; -import { WinstonLogger } from '../rootLogger/WinstonLogger'; +import { format, transports } from 'winston'; import { createConfigSecretEnumerator } from '../rootConfig/createConfigSecretEnumerator'; +import { WinstonLogger } from '../rootLogger/WinstonLogger'; +import { getRootLoggerConfig } from './config'; /** * Root-level logging. @@ -37,11 +38,14 @@ export const rootLoggerServiceFactory = createServiceFactory({ config: coreServices.rootConfig, }, async factory({ config }) { + const rootLoggerConfig = getRootLoggerConfig(config); + const logger = WinstonLogger.create({ meta: { service: 'backstage', + ...rootLoggerConfig.meta, }, - level: process.env.LOG_LEVEL || 'info', + level: process.env.LOG_LEVEL || rootLoggerConfig.level || 'info', format: process.env.NODE_ENV === 'production' ? format.json() @@ -53,6 +57,11 @@ export const rootLoggerServiceFactory = createServiceFactory({ logger.addRedactions(secretEnumerator(config)); config.subscribe?.(() => logger.addRedactions(secretEnumerator(config))); + logger.setLevelOverrides(rootLoggerConfig.overrides ?? []); + config.subscribe?.(() => + logger.setLevelOverrides(getRootLoggerConfig(config).overrides ?? []), + ); + return logger; }, }); diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/types.ts b/packages/backend-defaults/src/entrypoints/rootLogger/types.ts new file mode 100644 index 0000000000..f8277d8075 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLogger/types.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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, JsonPrimitive } from '@backstage/types'; +import { config as winstonConfig } from 'winston'; + +/** + * @public + */ +export type WinstonLoggerLevelOverrideMatchers = { + [key: string]: JsonPrimitive | JsonPrimitive[] | undefined; +}; + +/** + * @public + */ +export type WinstonLoggerLevelOverride = { + matchers: WinstonLoggerLevelOverrideMatchers; + level: string; +}; + +export type RootLoggerConfig = { + level?: string; + meta?: JsonObject; + overrides?: WinstonLoggerLevelOverride[]; +}; + +export const winstonLevels = winstonConfig.npm.levels; diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/utils.test.ts b/packages/backend-defaults/src/entrypoints/rootLogger/utils.test.ts new file mode 100644 index 0000000000..ede3de600b --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLogger/utils.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createLogMatcher } from './utils'; + +describe('createLogMatcher', () => { + const log = { + level: 'info', + message: 'This is a simple log from the catalog plugin', + plugin: 'catalog', + status: 200, + action: 'read', + }; + + it('should match with a simple matcher', () => { + const matcher = createLogMatcher({ plugin: 'catalog' }); + expect(matcher(log)).toEqual(true); + }); + + it('should not match with a simple matcher', () => { + const matcher = createLogMatcher({ plugin: 'search' }); + expect(matcher(log)).toEqual(false); + }); + + it('should match with an AND matcher', () => { + const matcher = createLogMatcher({ + plugin: 'catalog', + action: 'read', + }); + expect(matcher(log)).toEqual(true); + }); + + it('should not match log with an AND matcher', () => { + const matcher = createLogMatcher({ + plugin: 'catalog', + action: 'write', + }); + expect(matcher(log)).toEqual(false); + }); + + it('should match with an OR matcher', () => { + const matcher = createLogMatcher({ + plugin: ['auth', 'catalog'], + }); + expect(matcher(log)).toEqual(true); + }); + + it('should not match log with an OR matcher', () => { + const matcher = createLogMatcher({ + plugin: ['auth', 'search'], + }); + expect(matcher(log)).toEqual(false); + }); + + it('should match log with a regex matcher', () => { + const matcher = createLogMatcher({ + message: '/This is a simple log/', + }); + expect(matcher(log)).toEqual(true); + }); + + it('should not match log with a regex matcher', () => { + const matcher = createLogMatcher({ + message: '/^simple log/', + }); + expect(matcher(log)).toEqual(false); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/utils.ts b/packages/backend-defaults/src/entrypoints/rootLogger/utils.ts new file mode 100644 index 0000000000..adf38fc4da --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLogger/utils.ts @@ -0,0 +1,94 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TransformableInfo } from 'logform'; +import { WinstonLoggerLevelOverrideMatchers } from './types'; + +/** Parse a slash-delimited regex like `/pattern/flags` into a RegExp, or null if not a regex-string */ +const parseRegex = (s: string): RegExp | null => { + if (!s.startsWith('/')) return null; + const lastSlash = s.lastIndexOf('/'); + if (lastSlash <= 0) return null; + + const pattern = s.slice(1, lastSlash); + const flags = s.slice(lastSlash + 1); + + try { + return new RegExp(pattern, flags); + } catch { + return null; // fall back to treating it as a plain string + } +}; + +/** + * Create a predicate function that determines whether a log field matches a given matcher. + * + * The matcher can be: + * - A string (exact match or regex pattern delimited by slashes, e.g. `/pattern/`) + * - A non-string value (compared by strict equality) + * - An array of matchers (returns true if any matcher matches) + * + * @param matcher - The matcher or array of matchers to compare against the log field. + * @returns A function that takes a log field and returns `true` if it matches the matcher, otherwise `false`. + */ +const createLogFieldMatcher = ( + matcher: WinstonLoggerLevelOverrideMatchers[0], +): ((logField: unknown) => boolean) => { + // Array of matchers: create predicates for each element and OR them together + if (Array.isArray(matcher)) { + const fns = matcher.map(m => createLogFieldMatcher(m)); + return (logField: unknown) => fns.some(fn => fn(logField)); + } + + // Non-string matcher: strict equality + if (typeof matcher !== 'string') { + return (logField: unknown) => logField === matcher; + } + + // String matcher: maybe a slash-delimited regex (/pattern/flags) + const regex = parseRegex(matcher); + if (regex) { + return (logField: unknown) => + typeof logField === 'string' && regex.test(logField); + } + + // Plain string matcher: strict equality + return (logField: unknown) => logField === matcher; +}; + +/** + * Create a predicate function that determines whether a log entry matches + * all specified override matchers. + * + * Iterates over each key-matcher pair in the provided `matchers` object, + * retrieves the corresponding field from the `log` object, and checks if + * the field matches the matcher using `isLogFieldMatching`. Returns `true` + * only if all matchers are satisfied. + * + * @param matchers - An object where each key corresponds to a log field and each value is a matcher to test against that field. + * @returns A function that takes a log entry and returns `true` if it matches all specified matchers, otherwise `false`. + */ +export const createLogMatcher = ( + matchers: WinstonLoggerLevelOverrideMatchers, +): ((log: TransformableInfo) => boolean) => { + const logFieldMatchers = Object.entries(matchers).map(([key, m]) => { + const fn = createLogFieldMatcher(m); + return [key, fn] as const; + }); + + return (log: TransformableInfo) => + logFieldMatchers.every(([key, fn]) => fn(log[key])); +}; diff --git a/packages/backend-dynamic-feature-service/CHANGELOG.md b/packages/backend-dynamic-feature-service/CHANGELOG.md index 6bd6a7a781..e3f857785d 100644 --- a/packages/backend-dynamic-feature-service/CHANGELOG.md +++ b/packages/backend-dynamic-feature-service/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/backend-dynamic-feature-service +## 0.7.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/config-loader@1.10.3 + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-backend@0.5.6 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-app-node@0.1.37 + - @backstage/plugin-permission-node@0.10.4 + - @backstage/plugin-scaffolder-node@0.11.1 + - @backstage/plugin-search-backend-node@1.3.15 + +## 0.7.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/plugin-app-node@0.1.37-next.1 + ## 0.7.4-next.0 ### Patch Changes diff --git a/packages/backend-dynamic-feature-service/knip-report.md b/packages/backend-dynamic-feature-service/knip-report.md index 55e3fd302b..01af03f52e 100644 --- a/packages/backend-dynamic-feature-service/knip-report.md +++ b/packages/backend-dynamic-feature-service/knip-report.md @@ -4,14 +4,14 @@ | Name | Location | Severity | | :------------------------------------ | :----------- | :------- | -| @backstage/plugin-search-backend-node | package.json | error | -| @backstage/plugin-permission-common | package.json | error | -| @backstage/plugin-catalog-backend | package.json | error | -| @backstage/plugin-permission-node | package.json | error | -| @backstage/plugin-scaffolder-node | package.json | error | -| @backstage/plugin-events-backend | package.json | error | -| @backstage/plugin-search-common | package.json | error | -| @backstage/plugin-events-node | package.json | error | -| @backstage/plugin-auth-node | package.json | error | -| express-promise-router | package.json | error | +| @backstage/plugin-search-backend-node | packages/backend-dynamic-feature-service/package.json | error | +| @backstage/plugin-permission-common | packages/backend-dynamic-feature-service/package.json | error | +| @backstage/plugin-catalog-backend | packages/backend-dynamic-feature-service/package.json | error | +| @backstage/plugin-permission-node | packages/backend-dynamic-feature-service/package.json | error | +| @backstage/plugin-scaffolder-node | packages/backend-dynamic-feature-service/package.json | error | +| @backstage/plugin-events-backend | packages/backend-dynamic-feature-service/package.json | error | +| @backstage/plugin-search-common | packages/backend-dynamic-feature-service/package.json | error | +| @backstage/plugin-events-node | packages/backend-dynamic-feature-service/package.json | error | +| @backstage/plugin-auth-node | packages/backend-dynamic-feature-service/package.json | error | +| express-promise-router | packages/backend-dynamic-feature-service/package.json | error | diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index ff8fc29639..04577cbf43 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-dynamic-feature-service", - "version": "0.7.4-next.0", + "version": "0.7.4", "description": "Backstage dynamic feature service", "backstage": { "role": "node-library" diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md index b86bc1b616..87bea0405e 100644 --- a/packages/backend-openapi-utils/CHANGELOG.md +++ b/packages/backend-openapi-utils/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/backend-openapi-utils +## 0.6.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.6.1-next.0 ### Patch Changes diff --git a/packages/backend-openapi-utils/knip-report.md b/packages/backend-openapi-utils/knip-report.md index cf540a8d1d..d2f286fde1 100644 --- a/packages/backend-openapi-utils/knip-report.md +++ b/packages/backend-openapi-utils/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---------------------------- | :----------- | :------- | -| @backstage/backend-plugin-api | package.json | error | +| @backstage/backend-plugin-api | packages/backend-openapi-utils/package.json | error | diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json index 02d9dd1407..31d7aaf486 100644 --- a/packages/backend-openapi-utils/package.json +++ b/packages/backend-openapi-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-openapi-utils", - "version": "0.6.1-next.0", + "version": "0.6.1", "description": "OpenAPI typescript support.", "backstage": { "role": "node-library" diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md index ffc1a6eb6a..914d45f68b 100644 --- a/packages/backend-plugin-api/CHANGELOG.md +++ b/packages/backend-plugin-api/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/backend-plugin-api +## 1.4.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-node@0.10.4 + ## 1.4.3-next.0 ### Patch Changes diff --git a/packages/backend-plugin-api/knip-report.md b/packages/backend-plugin-api/knip-report.md index 89988530e6..869066c9f6 100644 --- a/packages/backend-plugin-api/knip-report.md +++ b/packages/backend-plugin-api/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/plugin-auth-node | package.json | error | +| @backstage/plugin-auth-node | packages/backend-plugin-api/package.json | error | diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index 46142aceda..baebdcd506 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-plugin-api", - "version": "1.4.3-next.0", + "version": "1.4.3", "description": "Core API used by Backstage backend plugins", "backstage": { "role": "node-library" diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index ad3915e3fb..cc82ef5593 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/backend-test-utils +## 1.9.0 + +### Minor Changes + +- 4e2c237: The `mockServices.rootConfig()` instance now has an `update` method that can be used to test configuration subscriptions and updates. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-app-api@1.2.7 + - @backstage/backend-plugin-api@1.4.3 + ## 1.9.0-next.1 ### Patch Changes diff --git a/packages/backend-test-utils/knip-report.md b/packages/backend-test-utils/knip-report.md index 6ba3e32cd6..860e474541 100644 --- a/packages/backend-test-utils/knip-report.md +++ b/packages/backend-test-utils/knip-report.md @@ -1,13 +1,20 @@ # Knip report -## Unused dependencies (6) +## Unused dependencies (7) | Name | Location | Severity | | :------------------------------- | :----------- | :------- | -| @types/express-serve-static-core | package.json | error | -| @backstage/plugin-auth-node | package.json | error | -| better-sqlite3 | package.json | error | -| @types/qs | package.json | error | -| mysql2 | package.json | error | -| pg | package.json | error | +| @types/express-serve-static-core | packages/backend-test-utils/package.json | error | +| @backstage/plugin-auth-node | packages/backend-test-utils/package.json | error | +| better-sqlite3 | packages/backend-test-utils/package.json | error | +| @types/qs | packages/backend-test-utils/package.json | error | +| lodash | packages/backend-test-utils/package.json | error | +| mysql2 | packages/backend-test-utils/package.json | error | +| pg | packages/backend-test-utils/package.json | error | + +## Unused devDependencies (1) + +| Name | Location | Severity | +| :------------ | :----------- | :------- | +| @types/lodash | packages/backend-test-utils/package.json | error | diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 6a6d24cf57..868c98a8f2 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-test-utils", - "version": "1.9.0-next.1", + "version": "1.9.0", "description": "Test helpers library for Backstage backends", "backstage": { "role": "node-library" diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 15b61dfb4e..398465966e 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,44 @@ # example-backend +## 0.0.42 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-scaffolder-backend-module-github@0.9.0 + - @backstage/plugin-scaffolder-backend@2.2.1 + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-mcp-actions-backend@0.1.3 + - @backstage/plugin-auth-backend@0.25.4 + - @backstage/plugin-techdocs-backend@2.1.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-app-backend@0.5.6 + - @backstage/plugin-kubernetes-backend@0.20.2 + - @backstage/plugin-events-backend@0.5.6 + - @backstage/plugin-search-backend@2.0.6 + - @backstage/plugin-search-backend-module-explore@0.3.7 + - @backstage/plugin-notifications-backend@0.5.10 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-auth-backend-module-github-provider@0.3.7 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.12 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.6 + - @backstage/plugin-catalog-backend-module-openapi@0.2.14 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.4 + - @backstage/plugin-devtools-backend@0.5.9 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.4 + - @backstage/plugin-permission-backend@0.7.4 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.12 + - @backstage/plugin-permission-node@0.10.4 + - @backstage/plugin-proxy-backend@0.6.6 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.14 + - @backstage/plugin-search-backend-module-catalog@0.3.8 + - @backstage/plugin-search-backend-module-techdocs@0.4.6 + - @backstage/plugin-search-backend-node@1.3.15 + - @backstage/plugin-signals-backend@0.3.8 + ## 0.0.42-next.1 ### Patch Changes diff --git a/packages/backend/knip-report.md b/packages/backend/knip-report.md index d24322fd2b..2410758b6d 100644 --- a/packages/backend/knip-report.md +++ b/packages/backend/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :----------------------------------------------- | :----------- | :------- | -| @backstage/plugin-catalog-backend-module-openapi | package.json | error | -| @opentelemetry/auto-instrumentations-node | package.json | error | -| @backstage/plugin-search-backend-node | package.json | error | -| @backstage/plugin-permission-common | package.json | error | -| @opentelemetry/exporter-prometheus | package.json | error | -| @backstage/plugin-permission-node | package.json | error | -| @opentelemetry/sdk-node | package.json | error | -| example-app | package.json | error | +| @backstage/plugin-catalog-backend-module-openapi | packages/backend/package.json | error | +| @opentelemetry/auto-instrumentations-node | packages/backend/package.json | error | +| @backstage/plugin-search-backend-node | packages/backend/package.json | error | +| @backstage/plugin-permission-common | packages/backend/package.json | error | +| @opentelemetry/exporter-prometheus | packages/backend/package.json | error | +| @backstage/plugin-permission-node | packages/backend/package.json | error | +| @opentelemetry/sdk-node | packages/backend/package.json | error | +| example-app | packages/backend/package.json | error | diff --git a/packages/backend/package.json b/packages/backend/package.json index a60b6d3940..8bca4f3b22 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.0.42-next.1", + "version": "0.0.42", "backstage": { "role": "backend" }, @@ -37,6 +37,7 @@ "@backstage/plugin-auth-backend": "workspace:^", "@backstage/plugin-auth-backend-module-github-provider": "workspace:^", "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^", + "@backstage/plugin-auth-backend-module-openshift-provider": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-catalog-backend": "workspace:^", "@backstage/plugin-catalog-backend-module-backstage-openapi": "workspace:^", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index f46e947b0d..e32d3148b3 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -33,6 +33,7 @@ const searchLoader = createBackendFeatureLoader({ backend.add(import('@backstage/plugin-auth-backend')); backend.add(import('./authModuleGithubProvider')); backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); +backend.add(import('@backstage/plugin-auth-backend-module-openshift-provider')); backend.add(import('@backstage/plugin-app-backend')); backend.add(import('@backstage/plugin-catalog-backend-module-unprocessed')); backend.add( diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 52e2ed04a4..a7d254702d 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,55 @@ # @backstage/catalog-client +## 1.12.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- 0efcc97: Updated generated schemas + +## 1.12.0-next.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + ## 1.11.0 ### Minor Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index a87ed29302..d3c13d9e6f 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-client", - "version": "1.11.0", + "version": "1.12.0", "description": "An isomorphic client for the catalog backend", "backstage": { "role": "common-library" diff --git a/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts b/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts index be1fa78117..a2a751ae2f 100644 --- a/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts +++ b/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts @@ -236,115 +236,14 @@ export class DefaultApiClient { } /** - * Get all entities matching a given filter. - * @param fields - By default the full entities are returned, but you can pass in a `fields` query -parameter which selects what parts of the entity data to retain. This makes the -response smaller and faster to transfer, and may allow the catalog to perform -more efficient queries. - -The query parameter value is a comma separated list of simplified JSON paths -like above. Each path corresponds to the key of either a value, or of a subtree -root that you want to keep in the output. The rest is pruned away. For example, -specifying `?fields=metadata.name,metadata.annotations,spec` retains only the -`name` and `annotations` fields of the `metadata` of each entity (it'll be an -object with at most two keys), keeps the entire `spec` unchanged, and cuts out -all other roots such as `relations`. - -Some more real world usable examples: - -- Return only enough data to form the full ref of each entity: - - `/entities/by-query?fields=kind,metadata.namespace,metadata.name` - - * @param limit - Number of records to return in the response. - * @param filter - You can pass in one or more filter sets that get matched against each entity. -Each filter set is a number of conditions that all have to match for the -condition to be true (conditions effectively have an AND between them). At least -one filter set has to be true for the entity to be part of the result set -(filter sets effectively have an OR between them). - -Example: - -```text -/entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type - - Return entities that match - - Filter set 1: - Condition 1: kind = user - AND - Condition 2: metadata.namespace = default - - OR - - Filter set 2: - Condition 1: kind = group - AND - Condition 2: spec.type exists -``` - -Each condition is either on the form `<key>`, or on the form `<key>=<value>`. -The first form asserts on the existence of a certain key (with any value), and -the second asserts that the key exists and has a certain value. All checks are -always case _insensitive_. - -In all cases, the key is a simplified JSON path in a given piece of entity data. -Each part of the path is a key of an object, and the traversal also descends -through arrays. There are two special forms: - -- Array items that are simple value types (such as strings) match on a key-value - pair where the key is the item as a string, and the value is the string `true` -- Relations can be matched on a `relations.<type>=<targetRef>` form - -Let's look at a simplified example to illustrate the concept: - -```json -{ - "a": { - "b": ["c", { "d": 1 }], - "e": 7 - } -} -``` - -This would match any one of the following conditions: - -- `a` -- `a.b` -- `a.b.c` -- `a.b.c=true` -- `a.b.d` -- `a.b.d=1` -- `a.e` -- `a.e=7` - -Some more real world usable examples: - -- Return all orphaned entities: - - `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - -- Return all users and groups: - - `/entities/by-query?filter=kind=user&filter=kind=group` - -- Return all service components: - - `/entities/by-query?filter=kind=component,spec.type=service` - -- Return all entities with the `java` tag: - - `/entities/by-query?filter=metadata.tags.java` - -- Return all users who are members of the `ops` group (note that the full - [reference](references.md) of the group is used): - - `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` - - * @param offset - Number of records to skip in the query page. - * @param after - Pointer to the previous page of results. - * @param order - - */ + * Get all entities matching a given filter. + * @param fields - By default the full entities are returned, but you can pass in a `fields` query parameter which selects what parts of the entity data to retain. This makes the response smaller and faster to transfer, and may allow the catalog to perform more efficient queries. The query parameter value is a comma separated list of simplified JSON paths like above. Each path corresponds to the key of either a value, or of a subtree root that you want to keep in the output. The rest is pruned away. For example, specifying `?fields=metadata.name,metadata.annotations,spec` retains only the `name` and `annotations` fields of the `metadata` of each entity (it'll be an object with at most two keys), keeps the entire `spec` unchanged, and cuts out all other roots such as `relations`. Some more real world usable examples: - Return only enough data to form the full ref of each entity: `/entities/by-query?fields=kind,metadata.namespace,metadata.name` + * @param limit - Number of records to return in the response. + * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let's look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` + * @param offset - Number of records to skip in the query page. + * @param after - Pointer to the previous page of results. + * @param order - + */ public async getEntities( // @ts-ignore request: GetEntities, @@ -368,148 +267,16 @@ Some more real world usable examples: } /** - * Search for entities by a given query. - * @param fields - By default the full entities are returned, but you can pass in a `fields` query -parameter which selects what parts of the entity data to retain. This makes the -response smaller and faster to transfer, and may allow the catalog to perform -more efficient queries. - -The query parameter value is a comma separated list of simplified JSON paths -like above. Each path corresponds to the key of either a value, or of a subtree -root that you want to keep in the output. The rest is pruned away. For example, -specifying `?fields=metadata.name,metadata.annotations,spec` retains only the -`name` and `annotations` fields of the `metadata` of each entity (it'll be an -object with at most two keys), keeps the entire `spec` unchanged, and cuts out -all other roots such as `relations`. - -Some more real world usable examples: - -- Return only enough data to form the full ref of each entity: - - `/entities/by-query?fields=kind,metadata.namespace,metadata.name` - - * @param limit - Number of records to return in the response. - * @param offset - Number of records to skip in the query page. - * @param orderField - By default the entities are returned ordered by their internal uid. You can -customize the `orderField` query parameters to affect that ordering. - -For example, to return entities by their name: - -`/entities/by-query?orderField=metadata.name,asc` - -Each parameter can be followed by `asc` for ascending lexicographical order or -`desc` for descending (reverse) lexicographical order. - - * @param cursor - You may pass the `cursor` query parameters to perform cursor based pagination -through the set of entities. The value of `cursor` will be returned in the response, under the `pageInfo` property: - -```json - "pageInfo": { - "nextCursor": "a-cursor", - "prevCursor": "another-cursor" - } -``` - -If `nextCursor` exists, it can be used to retrieve the next batch of entities. Following the same approach, -if `prevCursor` exists, it can be used to retrieve the previous batch of entities. - -- [`filter`](#filtering), for selecting only a subset of all entities -- [`fields`](#field-selection), for selecting only parts of the full data - structure of each entity -- `limit` for limiting the number of entities returned (20 is the default) -- [`orderField`](#ordering), for deciding the order of the entities -- `fullTextFilter` - **NOTE**: [`filter`, `orderField`, `fullTextFilter`] and `cursor` are mutually exclusive. This means that, - it isn't possible to change any of [`filter`, `orderField`, `fullTextFilter`] when passing `cursor` as query parameters, - as changing any of these properties will affect pagination. If any of `filter`, `orderField`, `fullTextFilter` is specified together with `cursor`, only the latter is taken into consideration. - - * @param filter - You can pass in one or more filter sets that get matched against each entity. -Each filter set is a number of conditions that all have to match for the -condition to be true (conditions effectively have an AND between them). At least -one filter set has to be true for the entity to be part of the result set -(filter sets effectively have an OR between them). - -Example: - -```text -/entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type - - Return entities that match - - Filter set 1: - Condition 1: kind = user - AND - Condition 2: metadata.namespace = default - - OR - - Filter set 2: - Condition 1: kind = group - AND - Condition 2: spec.type exists -``` - -Each condition is either on the form `<key>`, or on the form `<key>=<value>`. -The first form asserts on the existence of a certain key (with any value), and -the second asserts that the key exists and has a certain value. All checks are -always case _insensitive_. - -In all cases, the key is a simplified JSON path in a given piece of entity data. -Each part of the path is a key of an object, and the traversal also descends -through arrays. There are two special forms: - -- Array items that are simple value types (such as strings) match on a key-value - pair where the key is the item as a string, and the value is the string `true` -- Relations can be matched on a `relations.<type>=<targetRef>` form - -Let's look at a simplified example to illustrate the concept: - -```json -{ - "a": { - "b": ["c", { "d": 1 }], - "e": 7 - } -} -``` - -This would match any one of the following conditions: - -- `a` -- `a.b` -- `a.b.c` -- `a.b.c=true` -- `a.b.d` -- `a.b.d=1` -- `a.e` -- `a.e=7` - -Some more real world usable examples: - -- Return all orphaned entities: - - `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - -- Return all users and groups: - - `/entities/by-query?filter=kind=user&filter=kind=group` - -- Return all service components: - - `/entities/by-query?filter=kind=component,spec.type=service` - -- Return all entities with the `java` tag: - - `/entities/by-query?filter=metadata.tags.java` - -- Return all users who are members of the `ops` group (note that the full - [reference](references.md) of the group is used): - - `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` - - * @param fullTextFilterTerm - Text search term. - * @param fullTextFilterFields - A comma separated list of fields to sort returned results by. - */ + * Search for entities by a given query. + * @param fields - By default the full entities are returned, but you can pass in a `fields` query parameter which selects what parts of the entity data to retain. This makes the response smaller and faster to transfer, and may allow the catalog to perform more efficient queries. The query parameter value is a comma separated list of simplified JSON paths like above. Each path corresponds to the key of either a value, or of a subtree root that you want to keep in the output. The rest is pruned away. For example, specifying `?fields=metadata.name,metadata.annotations,spec` retains only the `name` and `annotations` fields of the `metadata` of each entity (it'll be an object with at most two keys), keeps the entire `spec` unchanged, and cuts out all other roots such as `relations`. Some more real world usable examples: - Return only enough data to form the full ref of each entity: `/entities/by-query?fields=kind,metadata.namespace,metadata.name` + * @param limit - Number of records to return in the response. + * @param offset - Number of records to skip in the query page. + * @param orderField - By default the entities are returned ordered by their internal uid. You can customize the `orderField` query parameters to affect that ordering. For example, to return entities by their name: `/entities/by-query?orderField=metadata.name,asc` Each parameter can be followed by `asc` for ascending lexicographical order or `desc` for descending (reverse) lexicographical order. + * @param cursor - You may pass the `cursor` query parameters to perform cursor based pagination through the set of entities. The value of `cursor` will be returned in the response, under the `pageInfo` property: ```json \"pageInfo\": { \"nextCursor\": \"a-cursor\", \"prevCursor\": \"another-cursor\" } ``` If `nextCursor` exists, it can be used to retrieve the next batch of entities. Following the same approach, if `prevCursor` exists, it can be used to retrieve the previous batch of entities. - [`filter`](#filtering), for selecting only a subset of all entities - [`fields`](#field-selection), for selecting only parts of the full data structure of each entity - `limit` for limiting the number of entities returned (20 is the default) - [`orderField`](#ordering), for deciding the order of the entities - `fullTextFilter` **NOTE**: [`filter`, `orderField`, `fullTextFilter`] and `cursor` are mutually exclusive. This means that, it isn't possible to change any of [`filter`, `orderField`, `fullTextFilter`] when passing `cursor` as query parameters, as changing any of these properties will affect pagination. If any of `filter`, `orderField`, `fullTextFilter` is specified together with `cursor`, only the latter is taken into consideration. + * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let's look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` + * @param fullTextFilterTerm - Text search term. + * @param fullTextFilterFields - A comma separated list of fields to sort returned results by. + */ public async getEntitiesByQuery( // @ts-ignore request: GetEntitiesByQuery, @@ -533,93 +300,10 @@ Some more real world usable examples: } /** - * Get a batch set of entities given an array of entityRefs. - * @param filter - You can pass in one or more filter sets that get matched against each entity. -Each filter set is a number of conditions that all have to match for the -condition to be true (conditions effectively have an AND between them). At least -one filter set has to be true for the entity to be part of the result set -(filter sets effectively have an OR between them). - -Example: - -```text -/entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type - - Return entities that match - - Filter set 1: - Condition 1: kind = user - AND - Condition 2: metadata.namespace = default - - OR - - Filter set 2: - Condition 1: kind = group - AND - Condition 2: spec.type exists -``` - -Each condition is either on the form `<key>`, or on the form `<key>=<value>`. -The first form asserts on the existence of a certain key (with any value), and -the second asserts that the key exists and has a certain value. All checks are -always case _insensitive_. - -In all cases, the key is a simplified JSON path in a given piece of entity data. -Each part of the path is a key of an object, and the traversal also descends -through arrays. There are two special forms: - -- Array items that are simple value types (such as strings) match on a key-value - pair where the key is the item as a string, and the value is the string `true` -- Relations can be matched on a `relations.<type>=<targetRef>` form - -Let's look at a simplified example to illustrate the concept: - -```json -{ - "a": { - "b": ["c", { "d": 1 }], - "e": 7 - } -} -``` - -This would match any one of the following conditions: - -- `a` -- `a.b` -- `a.b.c` -- `a.b.c=true` -- `a.b.d` -- `a.b.d=1` -- `a.e` -- `a.e=7` - -Some more real world usable examples: - -- Return all orphaned entities: - - `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - -- Return all users and groups: - - `/entities/by-query?filter=kind=user&filter=kind=group` - -- Return all service components: - - `/entities/by-query?filter=kind=component,spec.type=service` - -- Return all entities with the `java` tag: - - `/entities/by-query?filter=metadata.tags.java` - -- Return all users who are members of the `ops` group (note that the full - [reference](references.md) of the group is used): - - `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` - - * @param getEntitiesByRefsRequest - - */ + * Get a batch set of entities given an array of entityRefs. + * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let's look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` + * @param getEntitiesByRefsRequest - + */ public async getEntitiesByRefs( // @ts-ignore request: GetEntitiesByRefs, @@ -730,93 +414,10 @@ Some more real world usable examples: } /** - * Get all entity facets that match the given filters. - * @param facet - - * @param filter - You can pass in one or more filter sets that get matched against each entity. -Each filter set is a number of conditions that all have to match for the -condition to be true (conditions effectively have an AND between them). At least -one filter set has to be true for the entity to be part of the result set -(filter sets effectively have an OR between them). - -Example: - -```text -/entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type - - Return entities that match - - Filter set 1: - Condition 1: kind = user - AND - Condition 2: metadata.namespace = default - - OR - - Filter set 2: - Condition 1: kind = group - AND - Condition 2: spec.type exists -``` - -Each condition is either on the form `<key>`, or on the form `<key>=<value>`. -The first form asserts on the existence of a certain key (with any value), and -the second asserts that the key exists and has a certain value. All checks are -always case _insensitive_. - -In all cases, the key is a simplified JSON path in a given piece of entity data. -Each part of the path is a key of an object, and the traversal also descends -through arrays. There are two special forms: - -- Array items that are simple value types (such as strings) match on a key-value - pair where the key is the item as a string, and the value is the string `true` -- Relations can be matched on a `relations.<type>=<targetRef>` form - -Let's look at a simplified example to illustrate the concept: - -```json -{ - "a": { - "b": ["c", { "d": 1 }], - "e": 7 - } -} -``` - -This would match any one of the following conditions: - -- `a` -- `a.b` -- `a.b.c` -- `a.b.c=true` -- `a.b.d` -- `a.b.d=1` -- `a.e` -- `a.e=7` - -Some more real world usable examples: - -- Return all orphaned entities: - - `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - -- Return all users and groups: - - `/entities/by-query?filter=kind=user&filter=kind=group` - -- Return all service components: - - `/entities/by-query?filter=kind=component,spec.type=service` - -- Return all entities with the `java` tag: - - `/entities/by-query?filter=metadata.tags.java` - -- Return all users who are members of the `ops` group (note that the full - [reference](references.md) of the group is used): - - `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` - - */ + * Get all entity facets that match the given filters. + * @param facet - + * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let's look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` + */ public async getEntityFacets( // @ts-ignore request: GetEntityFacets, diff --git a/packages/catalog-client/src/schema/openapi/generated/apis/index.ts b/packages/catalog-client/src/schema/openapi/generated/apis/index.ts index af52f9db46..fc7c83b736 100644 --- a/packages/catalog-client/src/schema/openapi/generated/apis/index.ts +++ b/packages/catalog-client/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/index.ts b/packages/catalog-client/src/schema/openapi/generated/index.ts index bb399e97a0..dc3055033d 100644 --- a/packages/catalog-client/src/schema/openapi/generated/index.ts +++ b/packages/catalog-client/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationEntityField.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationEntityField.model.ts index ad2b5aecd1..65d2a2db65 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationEntityField.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationEntityField.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts index fb01ae8e8e..e7563a5c41 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts index e760d9cf94..dca7b943cc 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts index c1085a2a7b..b91f979678 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts index 66fc46bc0f..40d67804b7 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/CreateLocation201Response.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/CreateLocation201Response.model.ts index 27e557639a..75575079c4 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/CreateLocation201Response.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/CreateLocation201Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/CreateLocationRequest.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/CreateLocationRequest.model.ts index 5351ac289b..183aedc1f8 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/CreateLocationRequest.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/CreateLocationRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts index 1a962367d1..7b02ad53d9 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts index 11c42a6aee..ea67ecb2e5 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponsePageInfo.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponsePageInfo.model.ts index 07b4137c77..8e0fab6f68 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponsePageInfo.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponsePageInfo.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/Entity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/Entity.model.ts index f386f3dd17..fed0b96399 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/Entity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/Entity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts index a94f784f05..e48164ea7c 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts index 40594a1515..b82248b5e2 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityFacet.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityFacet.model.ts index f0f89f6eae..36551c0b66 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityFacet.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityFacet.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts index e013f0b5a8..43b10e0965 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityLink.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityLink.model.ts index 53d7d1f9ff..3c406bb785 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityLink.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityLink.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityMeta.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityMeta.model.ts index cc8dd555a6..ea2dedac5c 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityMeta.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityMeta.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityRelation.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityRelation.model.ts index fe98a84481..79313ca658 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityRelation.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityRelation.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ErrorError.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ErrorError.model.ts index fe5811628d..e0265e95d7 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ErrorRequest.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ErrorRequest.model.ts index d44dcb66d9..3eb5e15740 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ErrorResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ErrorResponse.model.ts index 91c120483d..edbcc32df7 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts index 9160f0823b..2308831357 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts index a9f1af1c53..d5f1f1260d 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/Location.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/Location.model.ts index d7e4a22de4..9c6e8fbc96 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/Location.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/Location.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/LocationInput.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/LocationInput.model.ts index 9cdc4c2c13..f86296a14b 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/LocationInput.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/LocationInput.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/LocationSpec.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/LocationSpec.model.ts index 1d56d8f3b0..9286b27d01 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/LocationSpec.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/LocationSpec.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ModelError.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ModelError.model.ts index 5526d703e6..958fde7d0b 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ModelError.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/NullableEntity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/NullableEntity.model.ts index aa6135ded4..fccc37650e 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/NullableEntity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/NullableEntity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts index b3913ccd85..345adaedea 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts index df5ce3811b..da5d7dce1c 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts index 6598f8df96..fc2437efc5 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityRelation.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityRelation.model.ts index 9c7dafb534..6c70398610 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityRelation.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityRelation.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/RefreshEntityRequest.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/RefreshEntityRequest.model.ts index 617f35288b..b26b7f1c45 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/RefreshEntityRequest.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/RefreshEntityRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts index fac9525afa..532b6fa9e3 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts index 8718d7938d..81cf98faac 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntityRequest.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntityRequest.model.ts index c3be04818d..c411697d94 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntityRequest.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntityRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/index.ts b/packages/catalog-client/src/schema/openapi/generated/models/index.ts index d6c81becb5..0e0d46df01 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/index.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/pluginId.ts b/packages/catalog-client/src/schema/openapi/generated/pluginId.ts index b45379f0b0..62afe18e6e 100644 --- a/packages/catalog-client/src/schema/openapi/generated/pluginId.ts +++ b/packages/catalog-client/src/schema/openapi/generated/pluginId.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/index.ts b/packages/catalog-client/src/schema/openapi/index.ts index db98243cbf..196aad553a 100644 --- a/packages/catalog-client/src/schema/openapi/index.ts +++ b/packages/catalog-client/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-model/knip-report.md b/packages/catalog-model/knip-report.md index 9285fce097..a03ecb8555 100644 --- a/packages/catalog-model/knip-report.md +++ b/packages/catalog-model/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :----------------- | :----------- | :------- | -| @types/json-schema | package.json | error | +| @types/json-schema | packages/catalog-model/package.json | error | diff --git a/packages/cli-node/knip-report.md b/packages/cli-node/knip-report.md index 4eeac45d5b..2ada6fe6ce 100644 --- a/packages/cli-node/knip-report.md +++ b/packages/cli-node/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :----- | :----------- | :------- | -| semver | package.json | error | +| semver | packages/cli-node/package.json | error | diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 87be49efce..7947c15cbb 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/cli +## 0.34.2 + +### Patch Changes + +- e6f45dc: Updated the WebPack configuration to use `contenthash`. This fixes an issue were builds would sometimes generate output files with the same name but different content across builds, leading to breakages when loading the frontend app. +- fffd434: Disallow import fallback of critical shared dependencies in module federation. +- 080f252: Fixed the `new-frontend-plugin` template that was incorrectly passing `id` instead of `pluginId` to `createFrontendPlugin` and unnecessarily importing `React`. +- e0db9b8: Modify the `backstage.json` also for custom patterns if it extends the default pattern. + + Examples: + + - `@backstage/*` (default pattern) + - `@{backstage,backstage-community}/*` + - `@{extra1,backstage,extra2}/*` + +- 275bda8: Fixed an issue that could cause conflicts of detected modules in workspaces with multiple apps. +- e1adce4: Updated the backend plugin template to use a new pattern for the `TodoListService` that reduces boilerplate. +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + +## 0.34.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/integration@1.18.0-next.0 + ## 0.34.2-next.1 ### Patch Changes diff --git a/packages/cli/knip-report.md b/packages/cli/knip-report.md index 8781b02f01..1073617a2c 100644 --- a/packages/cli/knip-report.md +++ b/packages/cli/knip-report.md @@ -1,65 +1,68 @@ # Knip report -## Unused dependencies (24) +## Unused dependencies (22) | Name | Location | Severity | | :--------------------------- | :----------- | :------- | -| @spotify/eslint-config-react | package.json | error | -| eslint-formatter-friendly | package.json | error | -| eslint-plugin-react-hooks | package.json | error | -| @backstage/catalog-model | package.json | error | -| @octokit/graphql-schema | package.json | error | -| @sucrase/webpack-loader | package.json | error | -| @backstage/integration | package.json | error | -| eslint-plugin-jsx-a11y | package.json | error | -| jest-environment-jsdom | package.json | error | -| terser-webpack-plugin | package.json | error | -| eslint-plugin-react | package.json | error | -| @octokit/oauth-app | package.json | error | -| @types/webpack-env | package.json | error | -| @octokit/graphql | package.json | error | -| jest-css-modules | package.json | error | -| git-url-parse | package.json | error | -| jest-runtime | package.json | error | -| cross-fetch | package.json | error | -| @swc/jest | package.json | error | -| process | package.json | error | -| sucrase | package.json | error | -| buffer | package.json | error | -| glob | package.json | error | -| util | package.json | error | +| @spotify/eslint-config-react | packages/cli/package.json | error | +| eslint-formatter-friendly | packages/cli/package.json | error | +| eslint-plugin-react-hooks | packages/cli/package.json | error | +| @backstage/catalog-model | packages/cli/package.json | error | +| @octokit/graphql-schema | packages/cli/package.json | error | +| @backstage/integration | packages/cli/package.json | error | +| eslint-plugin-jsx-a11y | packages/cli/package.json | error | +| jest-environment-jsdom | packages/cli/package.json | error | +| eslint-plugin-react | packages/cli/package.json | error | +| @octokit/oauth-app | packages/cli/package.json | error | +| @types/webpack-env | packages/cli/package.json | error | +| @octokit/graphql | packages/cli/package.json | error | +| jest-css-modules | packages/cli/package.json | error | +| git-url-parse | packages/cli/package.json | error | +| jest-runtime | packages/cli/package.json | error | +| cross-fetch | packages/cli/package.json | error | +| @swc/jest | packages/cli/package.json | error | +| process | packages/cli/package.json | error | +| sucrase | packages/cli/package.json | error | +| buffer | packages/cli/package.json | error | +| glob | packages/cli/package.json | error | +| util | packages/cli/package.json | error | ## Unused devDependencies (14) | Name | Location | Severity | | :--------------------------------------------------- | :----------- | :------- | -| @backstage/plugin-auth-backend-module-guest-provider | package.json | error | -| @types/rollup-plugin-peer-deps-external | package.json | error | -| @backstage/plugin-auth-backend | package.json | error | -| @backstage/plugin-catalog-node | package.json | error | -| @types/terser-webpack-plugin | package.json | error | -| @backstage/core-components | package.json | error | -| @backstage/catalog-client | package.json | error | -| @backstage/core-app-api | package.json | error | -| @types/webpack-sources | package.json | error | -| @backstage/dev-utils | package.json | error | -| @types/http-proxy | package.json | error | -| @types/svgo | package.json | error | -| @types/ejs | package.json | error | -| del | package.json | error | +| @backstage/plugin-auth-backend-module-guest-provider | packages/cli/package.json | error | +| @types/rollup-plugin-peer-deps-external | packages/cli/package.json | error | +| @backstage/plugin-auth-backend | packages/cli/package.json | error | +| @types/terser-webpack-plugin | packages/cli/package.json | error | +| @backstage/core-components | packages/cli/package.json | error | +| @backstage/catalog-client | packages/cli/package.json | error | +| @backstage/core-app-api | packages/cli/package.json | error | +| @types/webpack-sources | packages/cli/package.json | error | +| terser-webpack-plugin | packages/cli/package.json | error | +| @backstage/dev-utils | packages/cli/package.json | error | +| @types/http-proxy | packages/cli/package.json | error | +| @types/svgo | packages/cli/package.json | error | +| @types/ejs | packages/cli/package.json | error | +| del | packages/cli/package.json | error | -## Referenced optional peerDependencies (3) +## Referenced optional peerDependencies (8) -| Name | Location | Severity | -| :--------------------------- | :----------- | :------- | -| @rspack/plugin-react-refresh | package.json | error | -| @rspack/dev-server | package.json | error | -| @rspack/core | package.json | error | +| Name | Location | Severity | +| :----------------------------------- | :----------- | :------- | +| @pmmmwh/react-refresh-webpack-plugin | packages/cli/package.json | error | +| fork-ts-checker-webpack-plugin | packages/cli/package.json | error | +| @module-federation/enhanced | packages/cli/package.json | error | +| mini-css-extract-plugin | packages/cli/package.json | error | +| eslint-webpack-plugin | packages/cli/package.json | error | +| webpack-dev-server | packages/cli/package.json | error | +| esbuild-loader | packages/cli/package.json | error | +| webpack | packages/cli/package.json | error | ## Unlisted dependencies (2) | Name | Location | Severity | | :-------- | :------------------------------------------------- | :------- | -| react-dom | src/modules/build/lib/bundler/hasReactDomClient.ts | error | -| react | src/modules/build/lib/bundler/server.ts | error | +| react-dom | packages/cli/src/modules/build/lib/bundler/hasReactDomClient.ts | error | +| react | packages/cli/src/modules/build/lib/bundler/server.ts | error | diff --git a/packages/cli/package.json b/packages/cli/package.json index 51090239d8..f50a5601eb 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli", - "version": "0.34.2-next.1", + "version": "0.34.2", "description": "CLI for developing Backstage plugins and apps", "backstage": { "role": "cli" diff --git a/packages/cli/src/modules/build/lib/bundler/config.ts b/packages/cli/src/modules/build/lib/bundler/config.ts index c4011ede48..da444dff15 100644 --- a/packages/cli/src/modules/build/lib/bundler/config.ts +++ b/packages/cli/src/modules/build/lib/bundler/config.ts @@ -264,24 +264,30 @@ export async function createConfig( singleton: true, requiredVersion: '*', eager: !isRemote, + ...(isRemote && { import: false }), }, 'react-dom': { singleton: true, requiredVersion: '*', eager: !isRemote, + ...(isRemote && { import: false }), }, // React Router 'react-router': { singleton: true, requiredVersion: '*', eager: !isRemote, + ...(isRemote && { import: false }), }, 'react-router-dom': { singleton: true, requiredVersion: '*', eager: !isRemote, + ...(isRemote && { import: false }), }, // MUI v4 + // not setting import: false for MUI packages as this + // will break once Backstage moves to BUI '@material-ui/core/styles': { singleton: true, requiredVersion: '*', @@ -293,6 +299,8 @@ export async function createConfig( eager: !isRemote, }, // MUI v5 + // not setting import: false for MUI packages as this + // will break once Backstage moves to BUI '@mui/material/styles/': { singleton: true, requiredVersion: '*', @@ -422,10 +430,10 @@ export async function createConfig( path: paths.targetDist, publicPath: options.moduleFederation?.mode === 'remote' ? 'auto' : `${publicPath}/`, - filename: isDev ? '[name].js' : 'static/[name].[fullhash:8].js', + filename: isDev ? '[name].js' : 'static/[name].[contenthash:8].js', chunkFilename: isDev ? '[name].chunk.js' - : 'static/[name].[chunkhash:8].chunk.js', + : 'static/[name].[contenthash:8].chunk.js', ...(isDev ? { devtoolModuleFilenameTemplate: (info: any) => diff --git a/packages/cli/src/modules/build/lib/bundler/optimization.ts b/packages/cli/src/modules/build/lib/bundler/optimization.ts index 5d763ea4ac..63319ebec6 100644 --- a/packages/cli/src/modules/build/lib/bundler/optimization.ts +++ b/packages/cli/src/modules/build/lib/bundler/optimization.ts @@ -72,7 +72,7 @@ export const optimization = ( }, filename: isDev ? 'module-[name].js' - : 'static/module-[name].[chunkhash:8].js', + : 'static/module-[name].[contenthash:8].js', priority: 10, minSize: 100000, minChunks: 1, diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts index a309ddb192..1fb28d0289 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts @@ -772,6 +772,7 @@ describe('bump', () => { res( ctx.status(200), ctx.json({ + releaseVersion: '1.0.0', packages: [], }), ), @@ -797,7 +798,7 @@ describe('bump', () => { 'bumping @backstage-extra/custom in b to ^1.1.0', 'bumping @backstage-extra/custom-two in b to ^2.0.0', 'bumping @backstage/theme in b to ^2.0.0', - 'Skipping backstage.json update as custom pattern is used', + 'Your project is now at version 1.0.0, which has been written to backstage.json', 'Running yarn install to install new versions', 'Checking for moved packages to the @backstage-community namespace...', '⚠️ The following packages may have breaking changes:', diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli/src/modules/migrate/commands/versions/bump.ts index 23202ee1b5..391ccf8f22 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.ts @@ -18,6 +18,7 @@ maybeBootstrapProxy(); import fs from 'fs-extra'; import chalk from 'chalk'; +import { minimatch } from 'minimatch'; import semver from 'semver'; import { OptionValues } from 'commander'; import yaml from 'yaml'; @@ -78,6 +79,14 @@ type PkgVersionInfo = { location: string; }; +function extendsDefaultPattern(pattern: string): boolean { + if (!pattern.endsWith('/*')) { + return false; + } + + return minimatch('@backstage/', pattern.slice(0, -1)); +} + export default async (opts: OptionValues) => { const lockfilePath = paths.resolveTargetRoot('yarn.lock'); const lockfile = await Lockfile.load(lockfilePath); @@ -245,8 +254,8 @@ export default async (opts: OptionValues) => { console.log(); - // Do not update backstage.json when upgrade patterns are used. - if (pattern === DEFAULT_PATTERN_GLOB) { + // Do not update backstage.json when default pattern is not covered + if (extendsDefaultPattern(pattern)) { await bumpBackstageJsonVersion( releaseManifest.releaseVersion, hasYarnPlugin, diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index 6fe939f3e6..872136d5a2 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/config-loader +## 1.10.3 + +### Patch Changes + +- a73f495: Allow using `BACKSTAGE_ENV` for loading environment specific config files +- Updated dependencies + - @backstage/types@1.2.2 + +## 1.10.3-next.0 + +### Patch Changes + +- a73f495: Allow using `BACKSTAGE_ENV` for loading environment specific config files + ## 1.10.2 ### Patch Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index f347af8034..f8d103e97b 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/config-loader", - "version": "1.10.2", + "version": "1.10.3", "description": "Config loading functionality used by Backstage backend, and CLI", "backstage": { "role": "node-library" diff --git a/packages/config-loader/src/schema/collect.test.ts b/packages/config-loader/src/schema/collect.test.ts index 53db5c6da7..d48c98fbe4 100644 --- a/packages/config-loader/src/schema/collect.test.ts +++ b/packages/config-loader/src/schema/collect.test.ts @@ -41,16 +41,6 @@ describe('collectConfigSchemas', () => { mockDir.clear(); }); - it('should not find any schemas without packages', async () => { - mockDir.setContent({ - 'lerna.json': JSON.stringify({ - packages: ['packages/*'], - }), - }); - - await expect(collectConfigSchemas([], [])).resolves.toEqual([]); - }); - it('should find schema in a local package', async () => { mockDir.setContent({ node_modules: { diff --git a/packages/config-loader/src/sources/ConfigSources.test.ts b/packages/config-loader/src/sources/ConfigSources.test.ts index e19ac1d51f..a6b0648889 100644 --- a/packages/config-loader/src/sources/ConfigSources.test.ts +++ b/packages/config-loader/src/sources/ConfigSources.test.ts @@ -89,6 +89,19 @@ describe('ConfigSources', () => { { name: 'FileConfigSource', path: `${root}app-config.yaml` }, { name: 'FileConfigSource', path: `${root}app-config.local.yaml` }, ]); + + process.env = Object.assign(process.env, { BACKSTAGE_ENV: 'test' }); + expect( + mergeSources( + ConfigSources.defaultForTargets({ rootDir: '/', targets: [] }), + ), + ).toEqual([ + { name: 'FileConfigSource', path: `${root}app-config.yaml` }, + { name: 'FileConfigSource', path: `${root}app-config.test.yaml` }, + { name: 'FileConfigSource', path: `${root}app-config.local.yaml` }, + { name: 'FileConfigSource', path: `${root}app-config.test.local.yaml` }, + ]); + fsSpy.mockRestore(); expect( diff --git a/packages/config-loader/src/sources/ConfigSources.ts b/packages/config-loader/src/sources/ConfigSources.ts index 37c1c6b087..18d0443986 100644 --- a/packages/config-loader/src/sources/ConfigSources.ts +++ b/packages/config-loader/src/sources/ConfigSources.ts @@ -182,6 +182,14 @@ export class ConfigSources { if (argSources.length === 0) { const defaultPath = resolvePath(rootDir, 'app-config.yaml'); const localPath = resolvePath(rootDir, 'app-config.local.yaml'); + const envPath = resolvePath( + rootDir, + `app-config.${process.env.BACKSTAGE_ENV}.yaml`, + ); + const envLocalPath = resolvePath( + rootDir, + `app-config.${process.env.BACKSTAGE_ENV}.local.yaml`, + ); const alwaysIncludeDefaultConfigSource = !options.allowMissingDefaultConfig; @@ -195,6 +203,16 @@ export class ConfigSources { ); } + if (process.env.BACKSTAGE_ENV && fs.pathExistsSync(envPath)) { + argSources.push( + FileConfigSource.create({ + watch: options.watch, + path: envPath, + substitutionFunc: options.substitutionFunc, + }), + ); + } + if (fs.pathExistsSync(localPath)) { argSources.push( FileConfigSource.create({ @@ -204,6 +222,16 @@ export class ConfigSources { }), ); } + + if (process.env.BACKSTAGE_ENV && fs.pathExistsSync(envLocalPath)) { + argSources.push( + FileConfigSource.create({ + watch: options.watch, + path: envLocalPath, + substitutionFunc: options.substitutionFunc, + }), + ); + } } return this.merge(argSources); diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index c418a80e64..05bdf33224 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/core-app-api +## 1.19.0 + +### Minor Changes + +- 3fca906: Add `OpenShiftAuth` helper to create default OAuth flow for OpenShift. + +### Patch Changes + +- 5ae6d9d: feat: support no en languages +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/types@1.2.2 + ## 1.18.0 ### Minor Changes diff --git a/packages/core-app-api/knip-report.md b/packages/core-app-api/knip-report.md index 76616b6d60..9918534487 100644 --- a/packages/core-app-api/knip-report.md +++ b/packages/core-app-api/knip-report.md @@ -4,15 +4,15 @@ | Name | Location | Severity | | :------ | :----------- | :------- | -| history | package.json | error | +| history | packages/core-app-api/package.json | error | ## Unused devDependencies (5) | Name | Location | Severity | | :--------------------------- | :----------- | :------- | -| @testing-library/react-hooks | package.json | error | -| react-router-dom-stable | package.json | error | -| react-router-dom-beta | package.json | error | -| react-router-stable | package.json | error | -| react-router-beta | package.json | error | +| @testing-library/react-hooks | packages/core-app-api/package.json | error | +| react-router-dom-stable | packages/core-app-api/package.json | error | +| react-router-dom-beta | packages/core-app-api/package.json | error | +| react-router-stable | packages/core-app-api/package.json | error | +| react-router-beta | packages/core-app-api/package.json | error | diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 640921b120..8a52a71867 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-app-api", - "version": "1.18.0", + "version": "1.19.0", "description": "Core app API used by Backstage apps", "backstage": { "role": "web-library" diff --git a/packages/core-app-api/report.api.md b/packages/core-app-api/report.api.md index 445d73392d..0cecc22f4a 100644 --- a/packages/core-app-api/report.api.md +++ b/packages/core-app-api/report.api.md @@ -52,6 +52,7 @@ import { Observable } from '@backstage/types'; import { oktaAuthApiRef } from '@backstage/core-plugin-api'; import { oneloginAuthApiRef } from '@backstage/core-plugin-api'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; +import { openshiftAuthApiRef } from '@backstage/core-plugin-api'; import { PendingOAuthRequest } from '@backstage/core-plugin-api'; import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; @@ -651,6 +652,12 @@ export type OpenLoginPopupOptions = { height?: number; }; +// @public +export class OpenShiftAuth { + // (undocumented) + static create(options: OAuthApiCreateOptions): typeof openshiftAuthApiRef.T; +} + // @public export type PopupOptions = { size?: diff --git a/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.test.ts b/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.test.ts index 702d5e642a..eeeb11634d 100644 --- a/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.test.ts +++ b/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.test.ts @@ -144,11 +144,26 @@ describe('AppLanguageSelector', () => { AppLanguageSelector.createWithStorage({ availableLanguages: ['de'], }), - ).toThrow("Supported languages must include 'en'"); + ).toThrow( + "Initial language must be one of the supported languages, got 'en'", + ); const selector = AppLanguageSelector.createWithStorage(baseOptions); expect(() => selector.setLanguage('sv')).toThrow( "Failed to change language to 'sv', available languages are 'en', 'de'", ); }); + + it('should support no en languages', () => { + const selector = AppLanguageSelector.createWithStorage({ + availableLanguages: ['de'], + defaultLanguage: 'de', + }); + + expect(selector.getLanguage()).toEqual({ language: 'de' }); + + expect(() => selector.setLanguage('en')).toThrow( + "Failed to change language to 'en', available languages are 'de'", + ); + }); }); diff --git a/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.ts b/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.ts index 93f3d6517c..04dcfe9ebb 100644 --- a/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.ts +++ b/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.ts @@ -44,11 +44,9 @@ export class AppLanguageSelector implements AppLanguageApi { )}'`, ); } - if (!languages.includes(DEFAULT_LANGUAGE)) { - throw new Error(`Supported languages must include '${DEFAULT_LANGUAGE}'`); - } const initialLanguage = options?.defaultLanguage ?? DEFAULT_LANGUAGE; + if (!languages.includes(initialLanguage)) { throw new Error( `Initial language must be one of the supported languages, got '${initialLanguage}'`, diff --git a/packages/core-app-api/src/apis/implementations/auth/index.ts b/packages/core-app-api/src/apis/implementations/auth/index.ts index e02e07961a..00effb9384 100644 --- a/packages/core-app-api/src/apis/implementations/auth/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/index.ts @@ -26,4 +26,5 @@ export * from './bitbucket'; export * from './bitbucketServer'; export * from './atlassian'; export * from './vmwareCloud'; +export * from './openshift'; export type { OAuthApiCreateOptions, AuthApiCreateOptions } from './types'; diff --git a/packages/core-app-api/src/apis/implementations/auth/openshift/OpenShiftAuth.ts b/packages/core-app-api/src/apis/implementations/auth/openshift/OpenShiftAuth.ts new file mode 100644 index 0000000000..1d820189d2 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/openshift/OpenShiftAuth.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { openshiftAuthApiRef } from '@backstage/core-plugin-api'; +import { OAuth2 } from '../oauth2'; +import { OAuthApiCreateOptions } from '../types'; + +const DEFAULT_PROVIDER = { + id: 'openshift', + title: 'OpenShift', + icon: () => null, +}; + +/** + * Implements the OAuth flow to OpenShift + * + * @public + */ +export default class OpenShiftAuth { + static create(options: OAuthApiCreateOptions): typeof openshiftAuthApiRef.T { + const { + configApi, + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = ['user:info'], + } = options; + + return OAuth2.create({ + configApi, + discoveryApi, + oauthRequestApi, + provider, + environment, + defaultScopes, + }); + } +} diff --git a/plugins/catalog-backend-module-puppetdb/src/alpha.ts b/packages/core-app-api/src/apis/implementations/auth/openshift/index.ts similarity index 84% rename from plugins/catalog-backend-module-puppetdb/src/alpha.ts rename to packages/core-app-api/src/apis/implementations/auth/openshift/index.ts index 01a9b25f7c..65452114ae 100644 --- a/plugins/catalog-backend-module-puppetdb/src/alpha.ts +++ b/packages/core-app-api/src/apis/implementations/auth/openshift/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,5 +14,4 @@ * limitations under the License. */ -export * from './module'; -export { default } from './module'; +export { default as OpenShiftAuth } from './OpenShiftAuth'; diff --git a/packages/core-compat-api/CHANGELOG.md b/packages/core-compat-api/CHANGELOG.md index a966469313..eb4c7d051f 100644 --- a/packages/core-compat-api/CHANGELOG.md +++ b/packages/core-compat-api/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/core-compat-api +## 0.5.2 + +### Patch Changes + +- dc01d6f: Fix for `compatWrapper` creating many wrapping `Providers` when they should not +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + +## 0.5.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + ## 0.5.2-next.1 ### Patch Changes diff --git a/packages/core-compat-api/knip-report.md b/packages/core-compat-api/knip-report.md index a5794ee10b..7d2feee1f4 100644 --- a/packages/core-compat-api/knip-report.md +++ b/packages/core-compat-api/knip-report.md @@ -4,7 +4,7 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/frontend-app-api | package.json | error | -| @backstage/types | package.json | error | -| zod | package.json | error | +| @backstage/frontend-app-api | packages/core-compat-api/package.json | error | +| @backstage/types | packages/core-compat-api/package.json | error | +| zod | packages/core-compat-api/package.json | error | diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index 1430973106..acab8947c8 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-compat-api", - "version": "0.5.2-next.1", + "version": "0.5.2", "backstage": { "role": "web-library" }, diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 515e6eac5b..63e6e45065 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/core-components +## 0.18.0 + +### Minor Changes + +- b9a87f4: Add optional `distance` property to `DependencyEdge` to reflect the distance to a root. + +### Patch Changes + +- 1ad3d94: Dependency graph can now be opened in full screen mode +- e409bec: Fixes for rendering initials in the avatar component. +- ae7d426: update about card links style for pretty display with other language +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + +## 0.17.6-next.1 + +### Patch Changes + +- 1ad3d94: Dependency graph can now be opened in full screen mode +- ae7d426: update about card links style for pretty display with other language + ## 0.17.6-next.0 ### Patch Changes diff --git a/packages/core-components/knip-report.md b/packages/core-components/knip-report.md index 69f3218a24..1f66f844fa 100644 --- a/packages/core-components/knip-report.md +++ b/packages/core-components/knip-report.md @@ -4,21 +4,21 @@ | Name | Location | Severity | | :-------- | :----------- | :------- | -| pluralize | package.json | error | +| pluralize | packages/core-components/package.json | error | ## Unused devDependencies (3) | Name | Location | Severity | | :---------------------- | :----------- | :------- | -| @backstage/app-defaults | package.json | error | -| @types/google-protobuf | package.json | error | -| cross-fetch | package.json | error | +| @backstage/app-defaults | packages/core-components/package.json | error | +| @types/google-protobuf | packages/core-components/package.json | error | +| cross-fetch | packages/core-components/package.json | error | ## Unlisted dependencies (3) | Name | Location | Severity | | :---------------- | :------------------------------------------------------ | :------- | -| copy-to-clipboard | src/components/LogViewer/useLogViewerSelection.test.tsx | error | -| copy-to-clipboard | src/components/LogViewer/RealLogViewer.test.tsx | error | -| csstype | src/components/Lifecycle/Lifecycle.tsx | error | +| copy-to-clipboard | packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx | error | +| copy-to-clipboard | packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx | error | +| csstype | packages/core-components/src/components/Lifecycle/Lifecycle.tsx | error | diff --git a/packages/core-components/package.json b/packages/core-components/package.json index be91c5dbd5..d369052e99 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-components", - "version": "0.17.6-next.0", + "version": "0.18.0", "description": "Core components used by Backstage plugins and apps", "backstage": { "role": "web-library" @@ -80,6 +80,7 @@ "pluralize": "^8.0.0", "qs": "^6.9.4", "rc-progress": "3.5.1", + "react-full-screen": "^1.1.1", "react-helmet": "6.1.0", "react-hook-form": "^7.12.2", "react-idle-timer": "5.7.2", @@ -106,6 +107,7 @@ "@types/d3-selection": "^3.0.1", "@types/d3-shape": "^3.0.1", "@types/d3-zoom": "^3.0.1", + "@types/fscreen": "^1", "@types/google-protobuf": "^3.7.2", "@types/react": "^18.0.0", "@types/react-helmet": "^6.1.0", diff --git a/packages/core-components/report-alpha.api.md b/packages/core-components/report-alpha.api.md index 32bd2f4939..96ddebcd3a 100644 --- a/packages/core-components/report-alpha.api.md +++ b/packages/core-components/report-alpha.api.md @@ -61,6 +61,7 @@ export const coreComponentsTranslationRef: TranslationRef< readonly 'alertDisplay.message_other': '({{ count }} newer messages)'; readonly 'autoLogout.stillTherePrompt.title': 'Logging out due to inactivity'; readonly 'autoLogout.stillTherePrompt.buttonText': "Yes! Don't log me out"; + readonly 'dependencyGraph.fullscreenTooltip': 'Toggle fullscreen'; readonly 'proxiedSignInPage.title': 'You do not appear to be signed in. Please try reloading the browser page.'; } >; diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index fff1d0365a..b89b61ac77 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -257,6 +257,7 @@ export interface DependencyGraphProps extends SVGProps { acyclicer?: 'greedy'; align?: DependencyGraphTypes.Alignment; + allowFullscreen?: boolean; curve?: 'curveStepBefore' | 'curveMonotoneX'; defs?: JSX.Element | JSX.Element[]; direction?: DependencyGraphTypes.Direction; @@ -291,6 +292,7 @@ export namespace DependencyGraphTypes { from: string; to: string; label?: string; + distance?: number; }; export type DependencyNode = T & { id: string; diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.test.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.test.tsx index 6535808059..0dbf3a36b9 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.test.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import { render } from '@testing-library/react'; import { DependencyGraph } from './DependencyGraph'; import { DependencyGraphTypes as Types } from './types'; import { EDGE_TEST_ID, LABEL_TEST_ID, NODE_TEST_ID } from './constants'; +import { renderInTestApp } from '@backstage/test-utils'; describe('', () => { beforeAll(() => { @@ -36,9 +36,8 @@ describe('', () => { const CUSTOM_TEST_ID = 'custom-test-id'; it('renders each node and edge supplied', async () => { - const { getByText, queryAllByTestId, findAllByTestId } = render( - , - ); + const { getByText, queryAllByTestId, findAllByTestId } = + await renderInTestApp(); const renderedNodes = await findAllByTestId(NODE_TEST_ID); expect(renderedNodes).toHaveLength(3); expect(getByText(nodes[0].id)).toBeInTheDocument(); @@ -49,9 +48,10 @@ describe('', () => { }); it('update render if already referenced nodes are added later', async () => { - const { getByText, queryAllByTestId, findAllByTestId, rerender } = render( - , - ); + const { getByText, queryAllByTestId, findAllByTestId, rerender } = + await renderInTestApp( + , + ); let renderedNodes = await findAllByTestId(NODE_TEST_ID); expect(renderedNodes).toHaveLength(2); @@ -75,9 +75,10 @@ describe('', () => { { ...edges[0], label: 'first' }, { ...edges[1], label: 'second' }, ]; - const { getByText, getAllByTestId, findAllByTestId } = render( - , - ); + const { getByText, getAllByTestId, findAllByTestId } = + await renderInTestApp( + , + ); const renderedEdges = await findAllByTestId(EDGE_TEST_ID); expect(renderedEdges).toHaveLength(2); expect(getAllByTestId(LABEL_TEST_ID)).toHaveLength(2); @@ -94,7 +95,7 @@ describe('', () => { ); - const { getByText, findByTestId, container } = render( + const { getByText, findByTestId, container } = await renderInTestApp( , ); const node = await findByTestId(CUSTOM_TEST_ID); @@ -112,7 +113,7 @@ describe('', () => { ); - const { getByText, findByTestId, container } = render( + const { getByText, findByTestId, container } = await renderInTestApp( ({ + root: { + overflow: 'hidden', + minHeight: '100%', + minWidth: '100%', + }, + fullscreen: { + backgroundColor: theme.palette.background.paper, + }, +})); /** * Properties of {@link DependencyGraph} @@ -181,9 +200,18 @@ export interface DependencyGraphProps * Default: 'grow' */ fit?: 'grow' | 'contain'; + /** + * Controls if user can toggle fullscreen mode + * + * @remarks + * + * Default: true + */ + allowFullscreen?: boolean; } const WORKSPACE_ID = 'workspace'; +const DEPENDENCY_GRAPH_SVG = 'dependency-graph'; /** * Graph component used to visualize relations between entities @@ -216,11 +244,15 @@ export function DependencyGraph( curve = 'curveMonotoneX', showArrowHeads = false, fit = 'grow', + allowFullscreen = true, ...svgProps } = props; const theme = useTheme(); const [containerWidth, setContainerWidth] = useState(100); const [containerHeight, setContainerHeight] = useState(100); + const fullScreenHandle = useFullScreenHandle(); + const styles = useStyles(); + const { t } = useTranslationRef(coreComponentsTranslationRef); const graph = useRef>>( new dagre.graphlib.Graph(), @@ -242,11 +274,17 @@ export function DependencyGraph( const containerRef = useMemo( () => - debounce((node: SVGSVGElement) => { - if (!node) { + debounce((root: HTMLDivElement) => { + if (!root) { return; } // Set up zooming + panning + const node: SVGSVGElement = root.querySelector( + `svg#${DEPENDENCY_GRAPH_SVG}`, + ) as SVGSVGElement; + if (!node) { + return; + } const container = d3Selection.select(node); const workspace = d3Selection.select(node.getElementById(WORKSPACE_ID)); @@ -282,7 +320,7 @@ export function DependencyGraph( } const { width: newContainerWidth, height: newContainerHeight } = - node.getBoundingClientRect(); + root.getBoundingClientRect(); if (containerWidth !== newContainerWidth) { setContainerWidth(newContainerWidth); } @@ -406,68 +444,94 @@ export function DependencyGraph( } return ( - - - - - - {defs} - - +
+ + {allowFullscreen && ( + + + {fullScreenHandle.active ? ( + + ) : ( + + )} + + + )} + - {graphEdges.map(e => { - const edge = graph.current.edge(e) as GraphEdge; - if (!edge) return null; - return ( - + + - ); - })} - {graphNodes.map((id: string) => { - const node = graph.current.node(id); - if (!node) return null; - return ( - - ); - })} + + {defs} + + + + {graphEdges.map(e => { + const edge = graph.current.edge(e) as GraphEdge; + if (!edge) return null; + return ( + + ); + })} + {graphNodes.map((id: string) => { + const node = graph.current.node(id); + if (!node) return null; + return ( + + ); + })} + + - - + +
); } diff --git a/packages/core-components/src/components/DependencyGraph/types.ts b/packages/core-components/src/components/DependencyGraph/types.ts index e8fc9f6da3..a449637e49 100644 --- a/packages/core-components/src/components/DependencyGraph/types.ts +++ b/packages/core-components/src/components/DependencyGraph/types.ts @@ -46,6 +46,10 @@ export namespace DependencyGraphTypes { * Label assigned and rendered with the Edge */ label?: string; + /** + * Distance to a root entity + */ + distance?: number; }; /** diff --git a/packages/core-components/src/translation.ts b/packages/core-components/src/translation.ts index b884f0af92..aa644fa909 100644 --- a/packages/core-components/src/translation.ts +++ b/packages/core-components/src/translation.ts @@ -120,6 +120,9 @@ export const coreComponentsTranslationRef = createTranslationRef({ buttonText: "Yes! Don't log me out", }, }, + dependencyGraph: { + fullscreenTooltip: 'Toggle fullscreen', + }, proxiedSignInPage: { title: 'You do not appear to be signed in. Please try reloading the browser page.', diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index c109a9248e..61291f8e05 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/core-plugin-api +## 1.11.0 + +### Minor Changes + +- 5114627: Make `openshiftAuthApiRef` available in `@backstage/core-plugin-api`. + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + ## 1.10.9 ### Patch Changes diff --git a/packages/core-plugin-api/knip-report.md b/packages/core-plugin-api/knip-report.md index 169117772f..3aaa33dfa4 100644 --- a/packages/core-plugin-api/knip-report.md +++ b/packages/core-plugin-api/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @testing-library/user-event | package.json | error | +| @testing-library/user-event | packages/core-plugin-api/package.json | error | diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index ec789b618f..6e62c66c3a 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-plugin-api", - "version": "1.10.9", + "version": "1.11.0", "description": "Core API used by Backstage plugins", "backstage": { "role": "web-library" diff --git a/packages/core-plugin-api/report.api.md b/packages/core-plugin-api/report.api.md index 54bbfb94cf..b40088dc78 100644 --- a/packages/core-plugin-api/report.api.md +++ b/packages/core-plugin-api/report.api.md @@ -604,6 +604,11 @@ export type OpenIdConnectApi = { getIdToken(options?: AuthRequestOptions): Promise; }; +// @public +export const openshiftAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>; + // @public @deprecated export type OptionalParams< Params extends { diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index 9339a3e18a..f8686cb128 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -474,3 +474,20 @@ export const vmwareCloudAuthApiRef: ApiRef< > = createApiRef({ id: 'core.auth.vmware-cloud', }); + +/** + * Provides authentication towards OpenShift APIs and identities. + * + * @public + * @remarks + * + * See {@link https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/authentication_and_authorization/configuring-oauth-clients} + * on how to configure the OAuth clients and + * {@link https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html-single/authentication_and_authorization/index#tokens-scoping-about_configuring-internal-oauth} + * for available scopes. + */ +export const openshiftAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + id: 'core.auth.openshift', +}); diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 261e595b30..5d317d4afa 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/create-app +## 0.7.4 + +### Patch Changes + +- b2d9fc1: Creates a plugin that redirects from the Home page to the Catalog index page to avoid seeing a not found page error when starting the app. +- 020d484: Bumped create-app version. +- 02dbe8e: Add missing dependency to `@backstage/cli`, `@backstage/core-plugin-api` and `@backstage/integration-react` + +## 0.7.4-next.2 + +### Patch Changes + +- Bumped create-app version. + ## 0.7.4-next.1 ### Patch Changes diff --git a/packages/create-app/knip-report.md b/packages/create-app/knip-report.md index 864a5956ef..dc4b092da6 100644 --- a/packages/create-app/knip-report.md +++ b/packages/create-app/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-------------------- | :----------- | :------- | -| @types/command-exists | package.json | error | +| @types/command-exists | packages/create-app/package.json | error | diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 7f2f9e4cd5..263d65e5c8 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/create-app", - "version": "0.7.4-next.1", + "version": "0.7.4", "description": "A CLI that helps you create your own Backstage app", "backstage": { "role": "cli" diff --git a/packages/create-app/seed-yarn.lock b/packages/create-app/seed-yarn.lock index 21b2aa543d..00a86105cc 100644 --- a/packages/create-app/seed-yarn.lock +++ b/packages/create-app/seed-yarn.lock @@ -16,28 +16,3 @@ // package: the name of the package, e.g. @testing-library/react // query: the version query to pin the version for, e.g. ^14.0.0 // version: the version to pin to, must be in range of the query, e.g. 14.11.0 - -"@google-cloud/storage@^7.0.0": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-7.14.0.tgz#eda9715f68507949214af804c906eba6d168a214" - integrity sha512-H41bPL2cMfSi4EEnFzKvg7XSb7T67ocSXrmF7MPjfgFB0L6CKGzfIYJheAZi1iqXjz6XaCT1OBf6HCG5vDBTOQ== - -"@octokit/types@npm:^13.0.0": - version "13.6.2" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.6.2.tgz#e10fc4d2bdd65d836d1ced223b03ad4cfdb525bd" - integrity sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA== - -"@octokit/types@npm:^13.1.0": - version "13.6.2" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.6.2.tgz#e10fc4d2bdd65d836d1ced223b03ad4cfdb525bd" - integrity sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA== - -"@octokit/types@npm:^13.5.0": - version "13.6.2" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.6.2.tgz#e10fc4d2bdd65d836d1ced223b03ad4cfdb525bd" - integrity sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA== - -"@octokit/openapi-types@^22.2.0": - version "22.2.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-22.2.0.tgz#75aa7dcd440821d99def6a60b5f014207ae4968e" - integrity sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg== diff --git a/packages/create-app/templates/default-app/packages/backend/Dockerfile b/packages/create-app/templates/default-app/packages/backend/Dockerfile index 68d6c2fd58..ee80245d8b 100644 --- a/packages/create-app/templates/default-app/packages/backend/Dockerfile +++ b/packages/create-app/templates/default-app/packages/backend/Dockerfile @@ -12,7 +12,7 @@ # Alternatively, there is also a multi-stage Dockerfile documented here: # https://backstage.io/docs/deployment/docker#multi-stage-build -FROM node:20-bookworm-slim +FROM node:22-bookworm-slim # Set Python interpreter for `node-gyp` to use ENV PYTHON=/usr/bin/python3 diff --git a/packages/create-app/templates/next-app/packages/backend/Dockerfile b/packages/create-app/templates/next-app/packages/backend/Dockerfile index 68d6c2fd58..ee80245d8b 100644 --- a/packages/create-app/templates/next-app/packages/backend/Dockerfile +++ b/packages/create-app/templates/next-app/packages/backend/Dockerfile @@ -12,7 +12,7 @@ # Alternatively, there is also a multi-stage Dockerfile documented here: # https://backstage.io/docs/deployment/docker#multi-stage-build -FROM node:20-bookworm-slim +FROM node:22-bookworm-slim # Set Python interpreter for `node-gyp` to use ENV PYTHON=/usr/bin/python3 diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 503ae406ae..47ec659481 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/dev-utils +## 1.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/app-defaults@1.7.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-app-api@1.19.0 + - @backstage/integration-react@1.2.10 + +## 1.1.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + ## 1.1.14-next.1 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 55b4c3fdfe..40882a898b 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/dev-utils", - "version": "1.1.14-next.1", + "version": "1.1.14", "description": "Utilities for developing Backstage plugins.", "backstage": { "role": "web-library" diff --git a/packages/e2e-test-utils/knip-report.md b/packages/e2e-test-utils/knip-report.md index 5798802e35..b3b407dc65 100644 --- a/packages/e2e-test-utils/knip-report.md +++ b/packages/e2e-test-utils/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :--------------- | :----------- | :------- | -| @playwright/test | package.json | error | +| @playwright/test | packages/e2e-test-utils/package.json | error | diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md index 1a0dd4e3ea..471c2fc53e 100644 --- a/packages/e2e-test/CHANGELOG.md +++ b/packages/e2e-test/CHANGELOG.md @@ -1,5 +1,12 @@ # e2e-test +## 0.2.32 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.7.4 + ## 0.2.32-next.0 ### Patch Changes diff --git a/packages/e2e-test/knip-report.md b/packages/e2e-test/knip-report.md index 33d38c8508..516a9d0872 100644 --- a/packages/e2e-test/knip-report.md +++ b/packages/e2e-test/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-------------------- | :----------- | :------- | -| @backstage/create-app | package.json | error | +| @backstage/create-app | packages/e2e-test/package.json | error | diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 82a0ee1a2a..6b255f43b1 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -1,6 +1,6 @@ { "name": "e2e-test", - "version": "0.2.32-next.0", + "version": "0.2.32", "description": "E2E test for verifying Backstage packages", "backstage": { "role": "cli" diff --git a/packages/frontend-app-api/CHANGELOG.md b/packages/frontend-app-api/CHANGELOG.md index 50cd8614a2..1a23d4b535 100644 --- a/packages/frontend-app-api/CHANGELOG.md +++ b/packages/frontend-app-api/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/frontend-app-api +## 0.13.0 + +### Minor Changes + +- 6516c3d: The `createSpecializedApp` no longer throws when encountering many common errors when starting up the app. It will instead return them through the `errors` property so that they can be handled more gracefully in the app. + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/types@1.2.2 + - @backstage/frontend-defaults@0.3.1 + - @backstage/core-app-api@1.19.0 + ## 0.12.1-next.0 ### Patch Changes diff --git a/packages/frontend-app-api/knip-report.md b/packages/frontend-app-api/knip-report.md index b050438d7c..77a3cfea97 100644 --- a/packages/frontend-app-api/knip-report.md +++ b/packages/frontend-app-api/knip-report.md @@ -1,10 +1,17 @@ # Knip report -## Unused dependencies (3) +## Unused dependencies (4) | Name | Location | Severity | | :--------------------------- | :----------- | :------- | -| @backstage/frontend-defaults | package.json | error | -| @backstage/errors | package.json | error | -| zod | package.json | error | +| @backstage/frontend-defaults | packages/frontend-app-api/package.json | error | +| @backstage/version-bridge | packages/frontend-app-api/package.json | error | +| @backstage/errors | packages/frontend-app-api/package.json | error | +| zod | packages/frontend-app-api/package.json | error | + +## Unused devDependencies (1) + +| Name | Location | Severity | +| :----------------------------- | :----------- | :------- | +| @backstage/frontend-test-utils | packages/frontend-app-api/package.json | error | diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index ce62403915..3f2dab8571 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-app-api", - "version": "0.12.1-next.0", + "version": "0.13.0", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-app-api/report.api.md b/packages/frontend-app-api/report.api.md index 039d0bb7b1..bd0709813b 100644 --- a/packages/frontend-app-api/report.api.md +++ b/packages/frontend-app-api/report.api.md @@ -4,16 +4,128 @@ ```ts import { ApiHolder } from '@backstage/core-plugin-api'; +import { AppNode } from '@backstage/frontend-plugin-api'; import { AppTree } from '@backstage/frontend-plugin-api'; import { ConfigApi } from '@backstage/core-plugin-api'; import { ExtensionFactoryMiddleware } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; import { FrontendFeature } from '@backstage/frontend-plugin-api'; +import { FrontendPlugin } from '@backstage/frontend-plugin-api'; import { FrontendPluginInfo } from '@backstage/frontend-plugin-api'; import { JsonObject } from '@backstage/types'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { SubRouteRef } from '@backstage/frontend-plugin-api'; +// @public (undocumented) +export type AppError = + keyof AppErrorTypes extends infer ICode extends keyof AppErrorTypes + ? ICode extends any + ? { + code: ICode; + message: string; + context: AppErrorTypes[ICode]['context']; + } + : never + : never; + +// @public (undocumented) +export type AppErrorTypes = { + EXTENSION_IGNORED: { + context: { + plugin: FrontendPlugin; + extensionId: string; + }; + }; + INVALID_EXTENSION_CONFIG_KEY: { + context: { + extensionId: string; + }; + }; + EXTENSION_INPUT_REDIRECT_CONFLICT: { + context: { + node: AppNode; + inputName: string; + }; + }; + EXTENSION_INPUT_DATA_IGNORED: { + context: { + node: AppNode; + inputName: string; + }; + }; + EXTENSION_INPUT_DATA_MISSING: { + context: { + node: AppNode; + inputName: string; + }; + }; + EXTENSION_ATTACHMENT_CONFLICT: { + context: { + node: AppNode; + inputName: string; + }; + }; + EXTENSION_ATTACHMENT_MISSING: { + context: { + node: AppNode; + inputName: string; + }; + }; + EXTENSION_CONFIGURATION_INVALID: { + context: { + node: AppNode; + }; + }; + EXTENSION_INVALID: { + context: { + node: AppNode; + }; + }; + EXTENSION_OUTPUT_CONFLICT: { + context: { + node: AppNode; + dataRefId: string; + }; + }; + EXTENSION_OUTPUT_MISSING: { + context: { + node: AppNode; + dataRefId: string; + }; + }; + EXTENSION_OUTPUT_IGNORED: { + context: { + node: AppNode; + dataRefId: string; + }; + }; + EXTENSION_FACTORY_ERROR: { + context: { + node: AppNode; + }; + }; + API_EXTENSION_INVALID: { + context: { + node: AppNode; + }; + }; + ROUTE_DUPLICATE: { + context: { + routeId: string; + }; + }; + ROUTE_BINDING_INVALID_VALUE: { + context: { + routeId: string; + }; + }; + ROUTE_NOT_FOUND: { + context: { + routeId: string; + }; + }; +}; + // @public export type CreateAppRouteBinder = < TExternalRoutes extends { @@ -31,6 +143,7 @@ export type CreateAppRouteBinder = < export function createSpecializedApp(options?: CreateSpecializedAppOptions): { apis: ApiHolder; tree: AppTree; + errors?: AppError[]; }; // @public diff --git a/packages/frontend-app-api/src/routing/collectRouteIds.test.ts b/packages/frontend-app-api/src/routing/collectRouteIds.test.ts index 395886f982..b1bab3fbbe 100644 --- a/packages/frontend-app-api/src/routing/collectRouteIds.test.ts +++ b/packages/frontend-app-api/src/routing/collectRouteIds.test.ts @@ -20,6 +20,18 @@ import { createFrontendPlugin, } from '@backstage/frontend-plugin-api'; import { collectRouteIds } from './collectRouteIds'; +import { createErrorCollector } from '../wiring/createErrorCollector'; + +const collector = createErrorCollector(); + +afterEach(() => { + const errors = collector.collectErrors(); + if (errors) { + throw new Error( + `Unexpected errors: ${errors.map(e => e.message).join(', ')}`, + ); + } +}); describe('collectRouteIds', () => { it('should assign IDs to routes', () => { @@ -33,13 +45,16 @@ describe('collectRouteIds', () => { /^ExternalRouteRef\{created at '.*collectRouteIds\.test\.ts.*'\}$/, ); - const collected = collectRouteIds([ - createFrontendPlugin({ - pluginId: 'test', - routes: { ref }, - externalRoutes: { extRef }, - }), - ]); + const collected = collectRouteIds( + [ + createFrontendPlugin({ + pluginId: 'test', + routes: { ref }, + externalRoutes: { extRef }, + }), + ], + collector, + ); expect(Object.fromEntries(collected.routes)).toEqual({ 'test.ref': ref, }); @@ -50,4 +65,40 @@ describe('collectRouteIds', () => { expect(String(ref)).toBe('RouteRef{test.ref}'); expect(String(extRef)).toBe('ExternalRouteRef{test.extRef}'); }); + + it('should report duplicate route IDs', () => { + const ref = createRouteRef(); + const extRef = createExternalRouteRef(); + + collectRouteIds( + [ + createFrontendPlugin({ + pluginId: 'test', + routes: { 'mid.ref': ref }, + externalRoutes: { 'mid.extRef': extRef }, + }), + createFrontendPlugin({ + pluginId: 'test.mid', + routes: { ref }, + externalRoutes: { extRef }, + }), + ], + collector, + ); + + expect(collector.collectErrors()).toEqual([ + { + code: 'ROUTE_DUPLICATE', + message: + "Duplicate route id 'test.mid.ref' encountered while collecting routes", + context: { routeId: 'test.mid.ref' }, + }, + { + code: 'ROUTE_DUPLICATE', + message: + "Duplicate external route id 'test.mid.extRef' encountered while collecting routes", + context: { routeId: 'test.mid.extRef' }, + }, + ]); + }); }); diff --git a/packages/frontend-app-api/src/routing/collectRouteIds.ts b/packages/frontend-app-api/src/routing/collectRouteIds.ts index 8f189bda81..79afb07928 100644 --- a/packages/frontend-app-api/src/routing/collectRouteIds.ts +++ b/packages/frontend-app-api/src/routing/collectRouteIds.ts @@ -30,6 +30,7 @@ import { toInternalExternalRouteRef } from '../../../frontend-plugin-api/src/rou // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalSubRouteRef } from '../../../frontend-plugin-api/src/routing/SubRouteRef'; import { OpaqueFrontendPlugin } from '@internal/frontend'; +import { ErrorCollector } from '../wiring/createErrorCollector'; /** @internal */ export interface RouteRefsById { @@ -38,7 +39,10 @@ export interface RouteRefsById { } /** @internal */ -export function collectRouteIds(features: FrontendFeature[]): RouteRefsById { +export function collectRouteIds( + features: FrontendFeature[], + collector: ErrorCollector, +): RouteRefsById { const routesById = new Map(); const externalRoutesById = new Map(); @@ -50,7 +54,12 @@ export function collectRouteIds(features: FrontendFeature[]): RouteRefsById { for (const [name, ref] of Object.entries(feature.routes)) { const refId = `${feature.id}.${name}`; if (routesById.has(refId)) { - throw new Error(`Unexpected duplicate route '${refId}'`); + collector.report({ + code: 'ROUTE_DUPLICATE', + message: `Duplicate route id '${refId}' encountered while collecting routes`, + context: { routeId: refId }, + }); + continue; } if (isRouteRef(ref)) { @@ -65,7 +74,12 @@ export function collectRouteIds(features: FrontendFeature[]): RouteRefsById { for (const [name, ref] of Object.entries(feature.externalRoutes)) { const refId = `${feature.id}.${name}`; if (externalRoutesById.has(refId)) { - throw new Error(`Unexpected duplicate external route '${refId}'`); + collector.report({ + code: 'ROUTE_DUPLICATE', + message: `Duplicate external route id '${refId}' encountered while collecting routes`, + context: { routeId: refId }, + }); + continue; } const internalRef = toInternalExternalRouteRef(ref); diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts index bb03b5604b..efcc9ef18c 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts @@ -38,6 +38,18 @@ import { Root } from '../extensions/Root'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; import { createRouteAliasResolver } from './RouteAliasResolver'; +import { createErrorCollector } from '../wiring/createErrorCollector'; + +const collector = createErrorCollector(); + +afterEach(() => { + const errors = collector.collectErrors(); + if (errors) { + throw new Error( + `Unexpected errors: ${errors.map(e => e.message).join(', ')}`, + ); + } +}); const ref1 = createRouteRef(); const ref2 = createRouteRef(); @@ -102,10 +114,12 @@ function routeInfoFromExtensions( ], parameters: readAppExtensionsConfig(mockApis.config()), forbidden: new Set(['root']), + collector, }), + collector, ); - instantiateAppNodeTree(tree.root, TestApiRegistry.from()); + instantiateAppNodeTree(tree.root, TestApiRegistry.from(), collector); return extractRouteInfoFromAppNode( tree.root, diff --git a/packages/frontend-app-api/src/routing/resolveRouteBindings.test.ts b/packages/frontend-app-api/src/routing/resolveRouteBindings.test.ts index 6618bef152..003980e617 100644 --- a/packages/frontend-app-api/src/routing/resolveRouteBindings.test.ts +++ b/packages/frontend-app-api/src/routing/resolveRouteBindings.test.ts @@ -20,6 +20,18 @@ import { } from '@backstage/frontend-plugin-api'; import { resolveRouteBindings } from './resolveRouteBindings'; import { ConfigReader } from '@backstage/config'; +import { createErrorCollector } from '../wiring/createErrorCollector'; + +const collector = createErrorCollector(); + +afterEach(() => { + const errors = collector.collectErrors(); + if (errors) { + throw new Error( + `Unexpected errors: ${errors.map(e => e.message).join(', ')}`, + ); + } +}); const emptyIds = { routes: new Map(), externalRoutes: new Map() }; @@ -33,23 +45,30 @@ describe('resolveRouteBindings', () => { }, new ConfigReader({}), emptyIds, + collector, ); expect(result.get(external.myRoute)).toBe(ref); }); - it('throws on unknown keys', () => { + it('reports error on unknown keys', () => { const external = { myRoute: createExternalRouteRef() }; const ref = createRouteRef(); - expect(() => - resolveRouteBindings( - ({ bind }) => { - bind(external, { someOtherRoute: ref } as any); - }, - new ConfigReader({}), - emptyIds, - ), - ).toThrow('Key someOtherRoute is not an existing external route'); + resolveRouteBindings( + ({ bind }) => { + bind(external, { someOtherRoute: ref } as any); + }, + new ConfigReader({}), + emptyIds, + collector, + ); + expect(collector.collectErrors()).toEqual([ + { + code: 'ROUTE_NOT_FOUND', + message: 'Key someOtherRoute is not an existing external route', + context: { routeId: 'someOtherRoute' }, + }, + ]); }); it('reads bindings from config', () => { @@ -64,6 +83,7 @@ describe('resolveRouteBindings', () => { routes: new Map([['myTarget', myTarget]]), externalRoutes: new Map([['mySource', mySource]]), }, + collector, ); expect(result.get(mySource)).toBe(myTarget); @@ -85,6 +105,7 @@ describe('resolveRouteBindings', () => { routes: new Map([['myTarget', myTarget]]), externalRoutes: new Map([['mySource', mySource]]), }, + collector, ).get(mySource), ).toBe(undefined); @@ -100,57 +121,74 @@ describe('resolveRouteBindings', () => { routes: new Map([['myTarget', myTarget]]), externalRoutes: new Map([['mySource', mySource]]), }, + collector, ).get(mySource), ).toBe(myTarget); }); - it('throws on invalid config', () => { + it('reports errors on invalid config', () => { expect(() => resolveRouteBindings( () => {}, new ConfigReader({ app: { routes: { bindings: 'derp' } } }), emptyIds, + collector, ), ).toThrow( "Invalid type in config for key 'app.routes.bindings' in 'mock-config', got string, wanted object", ); - expect(() => - resolveRouteBindings( - () => {}, - new ConfigReader({ app: { routes: { bindings: { mySource: true } } } }), - emptyIds, - ), - ).toThrow( - "Invalid config at app.routes.bindings['mySource'], value must be a non-empty string", + resolveRouteBindings( + () => {}, + new ConfigReader({ app: { routes: { bindings: { mySource: true } } } }), + emptyIds, + collector, ); + expect(collector.collectErrors()).toEqual([ + { + code: 'ROUTE_BINDING_INVALID_VALUE', + message: + "Invalid config at app.routes.bindings['mySource'], value must be a non-empty string or false", + context: { routeId: 'mySource' }, + }, + ]); - expect(() => - resolveRouteBindings( - () => {}, - new ConfigReader({ - app: { routes: { bindings: { mySource: 'myTarget' } } }, - }), - emptyIds, - ), - ).toThrow( - "Invalid config at app.routes.bindings, 'mySource' is not a valid external route", + resolveRouteBindings( + () => {}, + new ConfigReader({ + app: { routes: { bindings: { mySource: 'myTarget' } } }, + }), + emptyIds, + collector, ); + expect(collector.collectErrors()).toEqual([ + { + code: 'ROUTE_NOT_FOUND', + message: + "Invalid config at app.routes.bindings, 'mySource' is not a valid external route", + context: { routeId: 'mySource' }, + }, + ]); - expect(() => - resolveRouteBindings( - () => {}, - new ConfigReader({ - app: { routes: { bindings: { mySource: 'myTarget' } } }, - }), - { - ...emptyIds, - externalRoutes: new Map([['mySource', createExternalRouteRef()]]), - }, - ), - ).toThrow( - "Invalid config at app.routes.bindings['mySource'], 'myTarget' is not a valid route", + resolveRouteBindings( + () => {}, + new ConfigReader({ + app: { routes: { bindings: { mySource: 'myTarget' } } }, + }), + { + ...emptyIds, + externalRoutes: new Map([['mySource', createExternalRouteRef()]]), + }, + collector, ); + expect(collector.collectErrors()).toEqual([ + { + code: 'ROUTE_NOT_FOUND', + message: + "Invalid config at app.routes.bindings['mySource'], 'myTarget' is not a valid route", + context: { routeId: 'myTarget' }, + }, + ]); }); it('can have default targets, but at the lowest priority', () => { @@ -170,6 +208,7 @@ describe('resolveRouteBindings', () => { () => {}, new ConfigReader({}), routesById, + collector, ); expect(result.get(source)).toBe(target1); @@ -181,6 +220,7 @@ describe('resolveRouteBindings', () => { app: { routes: { bindings: { source: 'target2' } } }, }), routesById, + collector, ); expect(result.get(source)).toBe(target2); @@ -192,6 +232,7 @@ describe('resolveRouteBindings', () => { }, new ConfigReader({}), routesById, + collector, ); expect(result.get(source)).toBe(target2); @@ -210,6 +251,7 @@ describe('resolveRouteBindings', () => { () => {}, new ConfigReader({}), routesById, + collector, ); expect(result.get(source)).toBe(target1); @@ -219,6 +261,7 @@ describe('resolveRouteBindings', () => { () => {}, new ConfigReader({ app: { routes: { bindings: { source: false } } } }), routesById, + collector, ); expect(result.get(source)).toBe(undefined); diff --git a/packages/frontend-app-api/src/routing/resolveRouteBindings.ts b/packages/frontend-app-api/src/routing/resolveRouteBindings.ts index 76d620c20c..f9836d7107 100644 --- a/packages/frontend-app-api/src/routing/resolveRouteBindings.ts +++ b/packages/frontend-app-api/src/routing/resolveRouteBindings.ts @@ -20,6 +20,7 @@ import { ExternalRouteRef, } from '@backstage/frontend-plugin-api'; import { RouteRefsById } from './collectRouteIds'; +import { ErrorCollector } from '../wiring/createErrorCollector'; import { Config } from '@backstage/config'; import { JsonObject } from '@backstage/types'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports @@ -80,6 +81,7 @@ export function resolveRouteBindings( bindRoutes: ((context: { bind: CreateAppRouteBinder }) => void) | undefined, config: Config, routesById: RouteRefsById, + collector: ErrorCollector, ): Map { const result = new Map(); const disabledExternalRefs = new Set(); @@ -93,7 +95,12 @@ export function resolveRouteBindings( for (const [key, value] of Object.entries(targetRoutes)) { const externalRoute = externalRoutes[key]; if (!externalRoute) { - throw new Error(`Key ${key} is not an existing external route`); + collector.report({ + code: 'ROUTE_NOT_FOUND', + message: `Key ${key} is not an existing external route`, + context: { routeId: String(key) }, + }); + continue; } if (value) { result.set(externalRoute, value); @@ -112,16 +119,22 @@ export function resolveRouteBindings( if (bindings) { for (const [externalRefId, targetRefId] of Object.entries(bindings)) { if (!isValidTargetRefId(targetRefId)) { - throw new Error( - `Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string or false`, - ); + collector.report({ + code: 'ROUTE_BINDING_INVALID_VALUE', + message: `Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string or false`, + context: { routeId: externalRefId }, + }); + continue; } const externalRef = routesById.externalRoutes.get(externalRefId); if (!externalRef) { - throw new Error( - `Invalid config at app.routes.bindings, '${externalRefId}' is not a valid external route`, - ); + collector.report({ + code: 'ROUTE_NOT_FOUND', + message: `Invalid config at app.routes.bindings, '${externalRefId}' is not a valid external route`, + context: { routeId: externalRefId }, + }); + continue; } // Skip if binding was already defined in code @@ -134,9 +147,14 @@ export function resolveRouteBindings( } else { const targetRef = routesById.routes.get(targetRefId); if (!targetRef) { - throw new Error( - `Invalid config at app.routes.bindings['${externalRefId}'], '${targetRefId}' is not a valid route`, - ); + collector.report({ + code: 'ROUTE_NOT_FOUND', + message: `Invalid config at app.routes.bindings['${externalRefId}'], '${String( + targetRefId, + )}' is not a valid route`, + context: { routeId: String(targetRefId) }, + }); + continue; } result.set(externalRef, targetRef); diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index 57ec7b23f3..13ec6e8a59 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -43,6 +43,7 @@ import { // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { createSchemaFromZod } from '../../../frontend-plugin-api/src/schema/createSchemaFromZod'; import { TestApiRegistry, withLogCollector } from '@backstage/test-utils'; +import { createErrorCollector } from '../wiring/createErrorCollector'; const testApis = TestApiRegistry.from(); const testDataRef = createExtensionDataRef().with({ id: 'test' }); @@ -51,6 +52,17 @@ const inputMirrorDataRef = createExtensionDataRef().with({ id: 'mirror', }); +const collector = createErrorCollector(); + +afterEach(() => { + const errors = collector.collectErrors(); + if (errors) { + throw new Error( + `Unexpected errors: ${errors.map(e => e.message).join(', ')}`, + ); + } +}); + function makeSpec( extension: Extension, spec?: Partial, @@ -88,6 +100,7 @@ function makeInstanceWithId( node, attachments: new Map(), apis: testApis, + collector, }), }; } @@ -151,75 +164,37 @@ describe('instantiateAppNodeTree', () => { }); it('should instantiate a single node', () => { - const tree = resolveAppTree('root-node', [ - makeSpec(simpleExtension, { id: 'root-node' }), - ]); + const tree = resolveAppTree( + 'root-node', + [makeSpec(simpleExtension, { id: 'root-node' })], + collector, + ); expect(tree.root.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root, testApis); + instantiateAppNodeTree(tree.root, testApis, collector); expect(tree.root.instance).toBeDefined(); expect(tree.root.instance?.getData(testDataRef)).toBe('test'); // Multiple calls should have no effect - instantiateAppNodeTree(tree.root, testApis); + instantiateAppNodeTree(tree.root, testApis, collector); expect(tree.root.instance).toBeDefined(); }); it('should not instantiate disabled nodes', () => { - const tree = resolveAppTree('root-node', [ - makeSpec(simpleExtension, { id: 'root-node', disabled: true }), - ]); + const tree = resolveAppTree( + 'root-node', + [makeSpec(simpleExtension, { id: 'root-node', disabled: true })], + collector, + ); expect(tree.root.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root, testApis); + instantiateAppNodeTree(tree.root, testApis, collector); expect(tree.root.instance).not.toBeDefined(); }); it('should instantiate a node with attachments', () => { - const tree = resolveAppTree('root-node', [ - makeSpec( - createV1Extension({ - namespace: 'root-node', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - test: createV1ExtensionInput({ test: testDataRef }), - }, - output: { - inputMirror: inputMirrorDataRef, - }, - factory({ inputs }) { - return { inputMirror: inputs }; - }, - }), - ), - makeSpec(simpleExtension, { - id: 'child-node', - attachTo: { id: 'root-node', input: 'test' }, - }), - ]); - - const childNode = tree.nodes.get('child-node'); - expect(childNode).toBeDefined(); - - expect(tree.root.instance).not.toBeDefined(); - expect(childNode?.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root, testApis); - expect(tree.root.instance).toBeDefined(); - expect(childNode?.instance).toBeDefined(); - expect(tree.root.instance?.getData(inputMirrorDataRef)).toMatchObject({ - test: [ - { node: { spec: { id: 'child-node' } }, output: { test: 'test' } }, - ], - }); - - // Multiple calls should have no effect - instantiateAppNodeTree(tree.root, testApis); - expect(tree.root.instance).toBeDefined(); - expect(childNode?.instance).toBeDefined(); - }); - - it('should not instantiate disabled attachments', () => { - const tree = resolveAppTree('root-node', [ - { - ...makeSpec( + const tree = resolveAppTree( + 'root-node', + [ + makeSpec( createV1Extension({ namespace: 'root-node', attachTo: { id: 'ignored', input: 'ignored' }, @@ -234,22 +209,72 @@ describe('instantiateAppNodeTree', () => { }, }), ), - }, - { - ...makeSpec(simpleExtension), - id: 'child-node', - // Using an invalid input should not be an error when disabled - attachTo: { id: 'root-node', input: 'invalid' }, - disabled: true, - }, - ]); + makeSpec(simpleExtension, { + id: 'child-node', + attachTo: { id: 'root-node', input: 'test' }, + }), + ], + collector, + ); const childNode = tree.nodes.get('child-node'); expect(childNode).toBeDefined(); expect(tree.root.instance).not.toBeDefined(); expect(childNode?.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root, testApis); + instantiateAppNodeTree(tree.root, testApis, collector); + expect(tree.root.instance).toBeDefined(); + expect(childNode?.instance).toBeDefined(); + expect(tree.root.instance?.getData(inputMirrorDataRef)).toMatchObject({ + test: [ + { node: { spec: { id: 'child-node' } }, output: { test: 'test' } }, + ], + }); + + // Multiple calls should have no effect + instantiateAppNodeTree(tree.root, testApis, collector); + expect(tree.root.instance).toBeDefined(); + expect(childNode?.instance).toBeDefined(); + }); + + it('should not instantiate disabled attachments', () => { + const tree = resolveAppTree( + 'root-node', + [ + { + ...makeSpec( + createV1Extension({ + namespace: 'root-node', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + test: createV1ExtensionInput({ test: testDataRef }), + }, + output: { + inputMirror: inputMirrorDataRef, + }, + factory({ inputs }) { + return { inputMirror: inputs }; + }, + }), + ), + }, + { + ...makeSpec(simpleExtension), + id: 'child-node', + // Using an invalid input should not be an error when disabled + attachTo: { id: 'root-node', input: 'invalid' }, + disabled: true, + }, + ], + collector, + ); + + const childNode = tree.nodes.get('child-node'); + expect(childNode).toBeDefined(); + + expect(tree.root.instance).not.toBeDefined(); + expect(childNode?.instance).not.toBeDefined(); + instantiateAppNodeTree(tree.root, testApis, collector); expect(tree.root.instance).toBeDefined(); expect(childNode?.instance).not.toBeDefined(); expect(tree.root.instance?.getData(inputMirrorDataRef)).toEqual({ @@ -264,13 +289,14 @@ describe('instantiateAppNodeTree', () => { node: makeNode(simpleExtension), attachments, apis: testApis, + collector, }); - expect(Array.from(instance.getDataRefs())).toEqual([ + expect(Array.from(instance?.getDataRefs() ?? [])).toEqual([ testDataRef, otherDataRef.optional(), ]); - expect(instance.getData(testDataRef)).toEqual('test'); + expect(instance?.getData(testDataRef)).toEqual('test'); }); it('should create an extension with different kind of inputs', () => { @@ -346,12 +372,13 @@ describe('instantiateAppNodeTree', () => { }, }), ), + collector, }); - expect(Array.from(instance.getDataRefs())).toEqual([ + expect(Array.from(instance?.getDataRefs() ?? [])).toEqual([ inputMirrorDataRef, ]); - expect(instance.getData(inputMirrorDataRef)).toMatchObject({ + expect(instance?.getData(inputMirrorDataRef)).toMatchObject({ optionalSingletonPresent: { node: { spec: { id: 'app/test' } }, output: { test: 'optionalSingletonPresent' }, @@ -371,118 +398,158 @@ describe('instantiateAppNodeTree', () => { }); it('should refuse to create an extension with invalid config', () => { - expect(() => + const node = makeNode(simpleExtension, { + config: { other: 'not-a-number' }, + }); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode(simpleExtension, { - config: { other: 'not-a-number' }, - }), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Invalid configuration for extension 'app/test'; caused by Error: Expected number, received string at 'other'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_CONFIGURATION_INVALID', + message: + "Invalid configuration for extension 'app/test'; caused by Error: Expected number, received string at 'other'", + context: { node }, + }, + ]); }); it('should forward extension factory errors', () => { - expect(() => + const node = makeNode( + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: {}, + factory() { + const error = new Error('NOPE'); + error.name = 'NopeError'; + throw error; + }, + }), + ); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - createV1Extension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: {}, - factory() { - const error = new Error('NOPE'); - error.name = 'NopeError'; - throw error; - }, - }), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test'; caused by NopeError: NOPE", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'app/test'; caused by NopeError: NOPE", + context: { node }, + }, + ]); }); it('should refuse to create an instance with duplicate output', () => { - expect(() => + const node = makeNode( + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + test1: testDataRef, + test2: testDataRef, + }, + factory({}) { + return { test1: 'test', test2: 'test2' }; + }, + }), + ); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - createV1Extension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: { - test1: testDataRef, - test2: testDataRef, - }, - factory({}) { - return { test1: 'test', test2: 'test2' }; - }, - }), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', duplicate extension data 'test' received via output 'test2'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'app/test', duplicate extension data 'test' received via output 'test2'", + context: { node }, + }, + ]); }); it('should refuse to create an instance with disconnected output data', () => { - expect(() => + const node = makeNode( + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + test: testDataRef, + }, + factory({}) { + return { nonexistent: 'test' } as any; + }, + }), + ); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - createV1Extension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: { - test: testDataRef, - }, - factory({}) { - return { nonexistent: 'test' } as any; - }, - }), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', unknown output provided via 'nonexistent'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'app/test', unknown output provided via 'nonexistent'", + context: { node }, + }, + ]); }); it('should refuse to create an instance with missing required input', () => { - expect(() => + const node = makeNode( + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createV1ExtensionInput( + { + test: testDataRef, + }, + { singleton: true }, + ), + }, + output: {}, + factory: () => ({}), + }), + ); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - createV1Extension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createV1ExtensionInput( - { - test: testDataRef, - }, - { singleton: true }, - ), - }, - output: {}, - factory: () => ({}), - }), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', input 'singleton' is required but was not received", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'app/test', input 'singleton' is required but was not received", + context: { node }, + }, + ]); }); it('should warn when creating an instance with undeclared inputs', () => { @@ -521,6 +588,7 @@ describe('instantiateAppNodeTree', () => { factory: () => ({}), }), ), + collector, }), ); @@ -555,6 +623,7 @@ describe('instantiateAppNodeTree', () => { factory: () => ({}), }), ), + collector, }), ); @@ -565,7 +634,24 @@ describe('instantiateAppNodeTree', () => { }); it('should refuse to create an instance with multiple inputs for required singleton', () => { - expect(() => + const node = makeNode( + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createV1ExtensionInput( + { + test: testDataRef, + }, + { singleton: true }, + ), + }, + output: {}, + factory: () => ({}), + }), + ); + expect( createAppNodeInstance({ apis: testApis, attachments: new Map([ @@ -577,31 +663,39 @@ describe('instantiateAppNodeTree', () => { ], ], ]), - node: makeNode( - createV1Extension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createV1ExtensionInput( - { - test: testDataRef, - }, - { singleton: true }, - ), - }, - output: {}, - factory: () => ({}), - }), - ), + node, + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', expected exactly one 'singleton' input but received multiple: 'app/test', 'app/test'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'app/test', expected exactly one 'singleton' input but received multiple: 'app/test', 'app/test'", + context: { node }, + }, + ]); }); it('should refuse to create an instance with multiple inputs for optional singleton', () => { - expect(() => + const node = makeNode( + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createV1ExtensionInput( + { + test: testDataRef, + }, + { singleton: true, optional: true }, + ), + }, + output: {}, + factory: () => ({}), + }), + ); + expect( createAppNodeInstance({ apis: testApis, attachments: new Map([ @@ -613,57 +707,56 @@ describe('instantiateAppNodeTree', () => { ], ], ]), - node: makeNode( - createV1Extension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createV1ExtensionInput( - { - test: testDataRef, - }, - { singleton: true, optional: true }, - ), - }, - output: {}, - factory: () => ({}), - }), - ), + node, + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', expected at most one 'singleton' input but received multiple: 'app/test', 'app/test'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'app/test', expected at most one 'singleton' input but received multiple: 'app/test', 'app/test'", + context: { node }, + }, + ]); }); it('should refuse to create an instance with multiple inputs that did not provide required data', () => { - expect(() => + const node = makeNode( + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createV1ExtensionInput( + { + other: otherDataRef, + }, + { singleton: true }, + ), + }, + output: {}, + factory: () => ({}), + }), + ); + expect( createAppNodeInstance({ apis: testApis, attachments: new Map([ ['singleton', [makeInstanceWithId(simpleExtension, undefined)]], ]), - node: makeNode( - createV1Extension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createV1ExtensionInput( - { - other: otherDataRef, - }, - { singleton: true }, - ), - }, - output: {}, - factory: () => ({}), - }), - ), + node, + collector, }), - ).toThrowErrorMatchingInlineSnapshot( - `"Failed to instantiate extension 'app/test', extension 'app/test' could not be attached because its output data ('test', 'other') does not match what the input 'singleton' requires ('other')"`, - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'app/test', extension 'app/test' could not be attached because its output data ('test', 'other') does not match what the input 'singleton' requires ('other')", + context: { node }, + }, + ]); }); }); }); @@ -727,71 +820,37 @@ describe('instantiateAppNodeTree', () => { } it('should instantiate a single node', () => { - const tree = resolveAppTree('root-node', [ - makeSpec(simpleExtension, { id: 'root-node' }), - ]); + const tree = resolveAppTree( + 'root-node', + [makeSpec(simpleExtension, { id: 'root-node' })], + collector, + ); expect(tree.root.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root, testApis); + instantiateAppNodeTree(tree.root, testApis, collector); expect(tree.root.instance).toBeDefined(); expect(tree.root.instance?.getData(testDataRef)).toBe('test'); // Multiple calls should have no effect - instantiateAppNodeTree(tree.root, testApis); + instantiateAppNodeTree(tree.root, testApis, collector); expect(tree.root.instance).toBeDefined(); }); it('should not instantiate disabled nodes', () => { - const tree = resolveAppTree('root-node', [ - makeSpec(simpleExtension, { id: 'root-node', disabled: true }), - ]); + const tree = resolveAppTree( + 'root-node', + [makeSpec(simpleExtension, { id: 'root-node', disabled: true })], + collector, + ); expect(tree.root.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root, testApis); + instantiateAppNodeTree(tree.root, testApis, collector); expect(tree.root.instance).not.toBeDefined(); }); it('should instantiate a node with attachments', () => { - const tree = resolveAppTree('root-node', [ - makeSpec( - resolveExtensionDefinition( - createExtension({ - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - test: createExtensionInput([testDataRef]), - }, - output: [inputMirrorDataRef], - factory: mirrorInputs, - }), - { namespace: 'root-node' }, - ), - ), - makeSpec(simpleExtension, { - id: 'child-node', - attachTo: { id: 'root-node', input: 'test' }, - }), - ]); - - const childNode = tree.nodes.get('child-node'); - expect(childNode).toBeDefined(); - - expect(tree.root.instance).not.toBeDefined(); - expect(childNode?.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root, testApis); - expect(tree.root.instance).toBeDefined(); - expect(childNode?.instance).toBeDefined(); - expect(tree.root.instance?.getData(inputMirrorDataRef)).toMatchObject({ - test: [{ node: { spec: { id: 'child-node' } }, test: 'test' }], - }); - - // Multiple calls should have no effect - instantiateAppNodeTree(tree.root, testApis); - expect(tree.root.instance).toBeDefined(); - expect(childNode?.instance).toBeDefined(); - }); - - it('should not instantiate disabled attachments', () => { - const tree = resolveAppTree('root-node', [ - { - ...makeSpec( + const tree = resolveAppTree( + 'root-node', + [ + makeSpec( resolveExtensionDefinition( createExtension({ attachTo: { id: 'ignored', input: 'ignored' }, @@ -804,22 +863,68 @@ describe('instantiateAppNodeTree', () => { { namespace: 'root-node' }, ), ), - }, - { - ...makeSpec(simpleExtension), - id: 'child-node', - // Using an invalid input should not be an error when disabled - attachTo: { id: 'root-node', input: 'invalid' }, - disabled: true, - }, - ]); + makeSpec(simpleExtension, { + id: 'child-node', + attachTo: { id: 'root-node', input: 'test' }, + }), + ], + collector, + ); const childNode = tree.nodes.get('child-node'); expect(childNode).toBeDefined(); expect(tree.root.instance).not.toBeDefined(); expect(childNode?.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root, testApis); + instantiateAppNodeTree(tree.root, testApis, collector); + expect(tree.root.instance).toBeDefined(); + expect(childNode?.instance).toBeDefined(); + expect(tree.root.instance?.getData(inputMirrorDataRef)).toMatchObject({ + test: [{ node: { spec: { id: 'child-node' } }, test: 'test' }], + }); + + // Multiple calls should have no effect + instantiateAppNodeTree(tree.root, testApis, collector); + expect(tree.root.instance).toBeDefined(); + expect(childNode?.instance).toBeDefined(); + }); + + it('should not instantiate disabled attachments', () => { + const tree = resolveAppTree( + 'root-node', + [ + { + ...makeSpec( + resolveExtensionDefinition( + createExtension({ + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + test: createExtensionInput([testDataRef]), + }, + output: [inputMirrorDataRef], + factory: mirrorInputs, + }), + { namespace: 'root-node' }, + ), + ), + }, + { + ...makeSpec(simpleExtension), + id: 'child-node', + // Using an invalid input should not be an error when disabled + attachTo: { id: 'root-node', input: 'invalid' }, + disabled: true, + }, + ], + collector, + ); + + const childNode = tree.nodes.get('child-node'); + expect(childNode).toBeDefined(); + + expect(tree.root.instance).not.toBeDefined(); + expect(childNode?.instance).not.toBeDefined(); + instantiateAppNodeTree(tree.root, testApis, collector); expect(tree.root.instance).toBeDefined(); expect(childNode?.instance).not.toBeDefined(); expect(tree.root.instance?.getData(inputMirrorDataRef)).toEqual({ @@ -834,10 +939,13 @@ describe('instantiateAppNodeTree', () => { node: makeNode(simpleExtension), attachments, apis: testApis, + collector, }); - expect(Array.from(instance.getDataRefs())).toEqual([testDataRef]); - expect(instance.getData(testDataRef)).toEqual('test'); + expect(Array.from(instance?.getDataRefs() ?? [])).toEqual([ + testDataRef, + ]); + expect(instance?.getData(testDataRef)).toEqual('test'); }); it('should create an extension with different kind of inputs', () => { @@ -903,12 +1011,13 @@ describe('instantiateAppNodeTree', () => { { namespace: 'app' }, ), ), + collector, }); - expect(Array.from(instance.getDataRefs())).toEqual([ + expect(Array.from(instance?.getDataRefs() ?? [])).toEqual([ inputMirrorDataRef, ]); - expect(instance.getData(inputMirrorDataRef)).toMatchObject({ + expect(instance?.getData(inputMirrorDataRef)).toMatchObject({ optionalSingletonPresent: { node: { spec: { id: 'app/test' } }, test: 'optionalSingletonPresent', @@ -941,24 +1050,35 @@ describe('instantiateAppNodeTree', () => { }); yield* output; }, + collector, }); - expect(Array.from(instance.getDataRefs())).toEqual([testDataRef]); - expect(instance.getData(testDataRef)).toEqual('modified'); + expect(Array.from(instance?.getDataRefs() ?? [])).toEqual([ + testDataRef, + ]); + expect(instance?.getData(testDataRef)).toEqual('modified'); }); it('should refuse to create an extension with invalid config', () => { - expect(() => + const node = makeNode(simpleExtension, { + config: { other: 'not-a-number' }, + }); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode(simpleExtension, { - config: { other: 'not-a-number' }, - }), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Invalid configuration for extension 'app/test'; caused by Error: Expected number, received string at 'other'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_CONFIGURATION_INVALID', + message: + "Invalid configuration for extension 'app/test'; caused by Error: Expected number, received string at 'other'", + context: { node }, + }, + ]); }); it('should throw if extension factories do not provide an iterable object', () => { @@ -966,14 +1086,21 @@ describe('instantiateAppNodeTree', () => { extension: ExtensionDefinition, middleware?: ExtensionFactoryMiddleware, ) { - return createAppNodeInstance({ + const node = makeNode( + resolveExtensionDefinition(extension, { namespace: 'test' }), + ); + createAppNodeInstance({ extensionFactoryMiddleware: middleware, apis: testApis, - node: makeNode( - resolveExtensionDefinition(extension, { namespace: 'test' }), - ), + node, attachments: new Map(), + collector, }); + const errors = collector.collectErrors(); + for (const error of errors ?? []) { + expect('node' in error.context && error.context.node).toBe(node); + } + return errors; } const baseOpts = { @@ -984,7 +1111,7 @@ describe('instantiateAppNodeTree', () => { const badFactory = () => 'not-iterable' as any; const goodFactory = () => [testDataRef('test')]; - expect(() => + expect( createInstance( createExtension({ attachTo: { id: 'ignored', input: 'ignored' }, @@ -992,11 +1119,16 @@ describe('instantiateAppNodeTree', () => { factory: badFactory, }), ), - ).toThrow( - `Failed to instantiate extension 'test', extension factory did not provide an iterable object`, - ); + ).toEqual([ + { + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'test', extension factory did not provide an iterable object", + context: { node: expect.anything() }, + }, + ]); - expect(() => + expect( createInstance( createExtension({ ...baseOpts, @@ -1005,12 +1137,17 @@ describe('instantiateAppNodeTree', () => { factory: badFactory, }), ), - ).toThrow( - `Failed to instantiate extension 'test', extension factory override did not provide an iterable object`, - ); + ).toEqual([ + { + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'test', extension factory override did not provide an iterable object", + context: { node: expect.anything() }, + }, + ]); // Bad middleware - expect(() => + expect( createInstance( createExtension({ ...baseOpts, @@ -1018,11 +1155,16 @@ describe('instantiateAppNodeTree', () => { }), () => 'not-iterable' as any, ), - ).toThrow( - `Failed to instantiate extension 'test', extension factory middleware did not provide an iterable object`, - ); + ).toEqual([ + { + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'test', extension factory middleware did not provide an iterable object", + context: { node: expect.anything() }, + }, + ]); - expect(() => + expect( createInstance( createExtensionBlueprint({ kind: 'test', @@ -1030,12 +1172,17 @@ describe('instantiateAppNodeTree', () => { factory: badFactory, }).make({ params: {} }), ), - ).toThrow( - `Failed to instantiate extension 'test:test', extension factory did not provide an iterable object`, - ); + ).toEqual([ + { + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'test:test', extension factory did not provide an iterable object", + context: { node: expect.anything() }, + }, + ]); // Using makeWithOverrides - expect(() => + expect( createInstance( createExtensionBlueprint({ kind: 'test', @@ -1045,12 +1192,17 @@ describe('instantiateAppNodeTree', () => { factory: badFactory, }), ), - ).toThrow( - `Failed to instantiate extension 'test:test', extension factory did not provide an iterable object`, - ); + ).toEqual([ + { + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'test:test', extension factory did not provide an iterable object", + context: { node: expect.anything() }, + }, + ]); // Using makeWithOverrides and factory middleware - expect(() => + expect( createInstance( createExtensionBlueprint({ kind: 'test', @@ -1061,12 +1213,17 @@ describe('instantiateAppNodeTree', () => { }), orig => orig(), ), - ).toThrow( - `Failed to instantiate extension 'test:test', extension factory did not provide an iterable object`, - ); + ).toEqual([ + { + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'test:test', extension factory did not provide an iterable object", + context: { node: expect.anything() }, + }, + ]); // Using makeWithOverrides and factory middleware - expect(() => + expect( createInstance( createExtensionBlueprint({ kind: 'test', @@ -1077,135 +1234,180 @@ describe('instantiateAppNodeTree', () => { }), orig => orig(), ), - ).toThrow( - `Failed to instantiate extension 'test:test', original blueprint factory did not provide an iterable object`, - ); + ).toEqual([ + { + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'test:test', original blueprint factory did not provide an iterable object", + context: { node: expect.anything() }, + }, + ]); }); it('should forward extension factory errors', () => { - expect(() => + const node = makeNode( + resolveExtensionDefinition( + createExtension({ + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [testDataRef], + factory() { + const error = new Error('NOPE'); + error.name = 'NopeError'; + throw error; + }, + }), + { namespace: 'app' }, + ), + ); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - resolveExtensionDefinition( - createExtension({ - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: [testDataRef], - factory() { - const error = new Error('NOPE'); - error.name = 'NopeError'; - throw error; - }, - }), - { namespace: 'app' }, - ), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test'; caused by NopeError: NOPE", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'app/test'; caused by NopeError: NOPE", + context: { node }, + }, + ]); }); it('should refuse to create an instance with duplicate output', () => { - expect(() => + const node = makeNode( + resolveExtensionDefinition( + createExtension({ + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [testDataRef, testDataRef], + factory({}) { + return [testDataRef('test'), testDataRef('test2')]; + }, + }), + { namespace: 'app' }, + ), + ); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - resolveExtensionDefinition( - createExtension({ - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: [testDataRef, testDataRef], - factory({}) { - return [testDataRef('test'), testDataRef('test2')]; - }, - }), - { namespace: 'app' }, - ), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', duplicate extension data output 'test'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_OUTPUT_CONFLICT', + message: "extension factory output duplicate data 'test'", + context: { dataRefId: 'test', node }, + }, + ]); }); it('should refuse to create an instance without required', () => { - expect(() => + const node = makeNode( + resolveExtensionDefinition( + createExtension({ + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [testDataRef], + factory({}) { + return [] as any; + }, + }), + { namespace: 'app' }, + ), + ); + + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - resolveExtensionDefinition( - createExtension({ - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: [testDataRef], - factory({}) { - return [] as any; - }, - }), - { namespace: 'app' }, - ), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', missing required extension data output 'test'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_OUTPUT_MISSING', + message: "missing required extension data output 'test'", + context: { + dataRefId: 'test', + node, + }, + }, + ]); }); it('should refuse to create an instance with unknown output data', () => { - expect(() => + const node = makeNode( + resolveExtensionDefinition( + // @ts-expect-error + createExtension({ + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [], // Output not declared + factory({}) { + return [testDataRef('test')] as any; + }, + }), + { namespace: 'app' }, + ), + ); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - resolveExtensionDefinition( - // @ts-expect-error - createExtension({ - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: [], // Output not declared - factory({}) { - return [testDataRef('test')] as any; - }, - }), - { namespace: 'app' }, - ), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', unexpected output 'test'", - ); + ).toBeDefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_OUTPUT_IGNORED', + message: "unexpected output 'test'", + context: { dataRefId: 'test', node }, + }, + ]); }); it('should refuse to create an instance with missing required input', () => { - expect(() => + const node = makeNode( + resolveExtensionDefinition( + createExtension({ + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput([testDataRef], { + singleton: true, + }), + }, + output: [], + factory: () => [], + }), + { namespace: 'app' }, + ), + ); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - resolveExtensionDefinition( - createExtension({ - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput([testDataRef], { - singleton: true, - }), - }, - output: [], - factory: () => [], - }), - { namespace: 'app' }, - ), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', input 'singleton' is required but was not received", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_ATTACHMENT_MISSING', + message: "input 'singleton' is required but was not received", + context: { inputName: 'singleton', node }, + }, + ]); }); it('should warn when creating an instance with undeclared inputs', () => { @@ -1244,6 +1446,7 @@ describe('instantiateAppNodeTree', () => { { namespace: 'app' }, ), ), + collector, }), ); @@ -1280,6 +1483,7 @@ describe('instantiateAppNodeTree', () => { { namespace: 'app' }, ), ), + collector, }), ); @@ -1290,7 +1494,23 @@ describe('instantiateAppNodeTree', () => { }); it('should refuse to create an instance with multiple inputs for required singleton', () => { - expect(() => + const node = makeNode( + resolveExtensionDefinition( + createExtension({ + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput([testDataRef], { + singleton: true, + }), + }, + output: [], + factory: () => [], + }), + { namespace: 'app' }, + ), + ); + expect( createAppNodeInstance({ apis: testApis, attachments: new Map([ @@ -1302,30 +1522,39 @@ describe('instantiateAppNodeTree', () => { ], ], ]), - node: makeNode( - resolveExtensionDefinition( - createExtension({ - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput([testDataRef], { - singleton: true, - }), - }, - output: [], - factory: () => [], - }), - { namespace: 'app' }, - ), - ), + node, + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', expected exactly one 'singleton' input but received multiple: 'app/test', 'app/test'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_ATTACHMENT_CONFLICT', + message: + "expected exactly one 'singleton' input but received multiple: 'app/test', 'app/test'", + context: { inputName: 'singleton', node }, + }, + ]); }); it('should refuse to create an instance with multiple inputs for optional singleton', () => { - expect(() => + const node = makeNode( + resolveExtensionDefinition( + createExtension({ + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput([testDataRef], { + singleton: true, + optional: true, + }), + }, + output: [], + factory: () => [], + }), + { namespace: 'app' }, + ), + ); + expect( createAppNodeInstance({ apis: testApis, attachments: new Map([ @@ -1337,56 +1566,55 @@ describe('instantiateAppNodeTree', () => { ], ], ]), - node: makeNode( - resolveExtensionDefinition( - createExtension({ - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput([testDataRef], { - singleton: true, - optional: true, - }), - }, - output: [], - factory: () => [], - }), - { namespace: 'app' }, - ), - ), + node, + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', expected at most one 'singleton' input but received multiple: 'app/test', 'app/test'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_ATTACHMENT_CONFLICT', + message: + "expected at most one 'singleton' input but received multiple: 'app/test', 'app/test'", + context: { inputName: 'singleton', node }, + }, + ]); }); it('should refuse to create an instance with multiple inputs that did not provide required data', () => { - expect(() => + const node = makeNode( + resolveExtensionDefinition( + createExtension({ + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput([otherDataRef], { + singleton: true, + }), + }, + output: [], + factory: () => [], + }), + { namespace: 'app' }, + ), + ); + expect( createAppNodeInstance({ apis: testApis, attachments: new Map([ ['singleton', [makeInstanceWithId(simpleExtension, undefined)]], ]), - node: makeNode( - resolveExtensionDefinition( - createExtension({ - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput([otherDataRef], { - singleton: true, - }), - }, - output: [], - factory: () => [], - }), - { namespace: 'app' }, - ), - ), + node, + collector, }), - ).toThrowErrorMatchingInlineSnapshot( - `"Failed to instantiate extension 'app/test', extension 'app/test' could not be attached because its output data ('test') does not match what the input 'singleton' requires ('other')"`, - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_INPUT_DATA_MISSING', + message: + "extension 'app/test' could not be attached because its output data ('test') does not match what the input 'singleton' requires ('other')", + context: { node, inputName: 'singleton' }, + }, + ]); }); }); }); diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index 2c917ed5d4..39fc88c19a 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -27,6 +27,36 @@ import { AppNode, AppNodeInstance } from '@backstage/frontend-plugin-api'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtension } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; import { createExtensionDataContainer } from '@internal/frontend'; +import { ErrorCollector } from '../wiring/createErrorCollector'; + +const INSTANTIATION_FAILED = new Error('Instantiation failed'); + +/** + * Like `array.map`, but if `INSTANTIATION_FAILED` is thrown, the iteration will continue but afterwards re-throw `INSTANTIATION_FAILED` + * @returns + */ +function mapWithFailures( + iterable: Iterable, + callback: (item: T) => U, +): U[] { + let failed = false; + const results = Array.from(iterable).map(item => { + try { + return callback(item); + } catch (error) { + if (error === INSTANTIATION_FAILED) { + failed = true; + } else { + throw error; + } + return null as any; + } + }); + if (failed) { + throw INSTANTIATION_FAILED; + } + return results; +} type Mutable = { -readonly [P in keyof T]: T[P]; @@ -39,37 +69,45 @@ function resolveV1InputDataMap( attachment: AppNode, inputName: string, ) { - return mapValues(dataMap, ref => { - const value = attachment.instance?.getData(ref); - if (value === undefined && !ref.config.optional) { - const expected = Object.values(dataMap) - .filter(r => !r.config.optional) - .map(r => `'${r.id}'`) - .join(', '); + return Object.fromEntries( + mapWithFailures(Object.entries(dataMap), ([key, ref]) => { + const value = attachment.instance?.getData(ref); + if (value === undefined && !ref.config.optional) { + const expected = Object.values(dataMap) + .filter(r => !r.config.optional) + .map(r => `'${r.id}'`) + .join(', '); - const provided = [...(attachment.instance?.getDataRefs() ?? [])] - .map(r => `'${r.id}'`) - .join(', '); + const provided = [...(attachment.instance?.getDataRefs() ?? [])] + .map(r => `'${r.id}'`) + .join(', '); - throw new Error( - `extension '${attachment.spec.id}' could not be attached because its output data (${provided}) does not match what the input '${inputName}' requires (${expected})`, - ); - } - return value; - }); + throw new Error( + `extension '${attachment.spec.id}' could not be attached because its output data (${provided}) does not match what the input '${inputName}' requires (${expected})`, + ); + } + return [key, value]; + }), + ); } function resolveInputDataContainer( extensionData: Array, attachment: AppNode, inputName: string, + collector: ErrorCollector<{ node: AppNode; inputName: string }>, ): { node: AppNode } & ExtensionDataContainer { const dataMap = new Map(); - for (const ref of extensionData) { + mapWithFailures(extensionData, ref => { if (dataMap.has(ref.id)) { - throw new Error(`Unexpected duplicate input data '${ref.id}'`); + collector.report({ + code: 'EXTENSION_INPUT_DATA_IGNORED', + message: `Unexpected duplicate input data '${ref.id}'`, + }); + return; } + const value = attachment.instance?.getData(ref); if (value === undefined && !ref.config.optional) { const expected = extensionData @@ -81,13 +119,15 @@ function resolveInputDataContainer( .map(r => `'${r.id}'`) .join(', '); - throw new Error( - `extension '${attachment.spec.id}' could not be attached because its output data (${provided}) does not match what the input '${inputName}' requires (${expected})`, - ); + collector.report({ + code: 'EXTENSION_INPUT_DATA_MISSING', + message: `extension '${attachment.spec.id}' could not be attached because its output data (${provided}) does not match what the input '${inputName}' requires (${expected})`, + }); + throw INSTANTIATION_FAILED; } dataMap.set(ref.id, value); - } + }); return { node: attachment, @@ -147,40 +187,52 @@ function resolveV1Inputs( }, attachments: ReadonlyMap, ) { - return mapValues(inputMap, (input, inputName) => { - const attachedNodes = attachments.get(inputName) ?? []; + return Object.fromEntries( + mapWithFailures(Object.entries(inputMap), ([inputName, input]) => { + const attachedNodes = attachments.get(inputName) ?? []; - if (input.config.singleton) { - if (attachedNodes.length > 1) { - const attachedNodeIds = attachedNodes.map(e => e.spec.id); - throw Error( - `expected ${ - input.config.optional ? 'at most' : 'exactly' - } one '${inputName}' input but received multiple: '${attachedNodeIds.join( - "', '", - )}'`, - ); - } else if (attachedNodes.length === 0) { - if (input.config.optional) { - return undefined; + if (input.config.singleton) { + if (attachedNodes.length > 1) { + const attachedNodeIds = attachedNodes.map(e => e.spec.id); + throw Error( + `expected ${ + input.config.optional ? 'at most' : 'exactly' + } one '${inputName}' input but received multiple: '${attachedNodeIds.join( + "', '", + )}'`, + ); + } else if (attachedNodes.length === 0) { + if (input.config.optional) { + return [inputName, undefined]; + } + throw Error(`input '${inputName}' is required but was not received`); } - throw Error(`input '${inputName}' is required but was not received`); - } - return { - node: attachedNodes[0], - output: resolveV1InputDataMap( - input.extensionData, - attachedNodes[0], + return [ inputName, - ), - }; - } + { + node: attachedNodes[0], + output: resolveV1InputDataMap( + input.extensionData, + attachedNodes[0], + inputName, + ), + }, + ]; + } - return attachedNodes.map(attachment => ({ - node: attachment, - output: resolveV1InputDataMap(input.extensionData, attachment, inputName), - })); - }) as { + return [ + inputName, + attachedNodes.map(attachment => ({ + node: attachment, + output: resolveV1InputDataMap( + input.extensionData, + attachment, + inputName, + ), + })), + ]; + }), + ) as { [inputName in string]: { node: AppNode; output: { @@ -198,6 +250,7 @@ function resolveV2Inputs( >; }, attachments: ReadonlyMap, + parentCollector: ErrorCollector<{ node: AppNode }>, ): ResolvedExtensionInputs<{ [inputName in string]: ExtensionInput< ExtensionDataRef, @@ -206,32 +259,43 @@ function resolveV2Inputs( }> { return mapValues(inputMap, (input, inputName) => { const attachedNodes = attachments.get(inputName) ?? []; + const collector = parentCollector.child({ inputName }); if (input.config.singleton) { if (attachedNodes.length > 1) { - const attachedNodeIds = attachedNodes.map(e => e.spec.id); - throw Error( - `expected ${ + const attachedNodeIds = attachedNodes.map(e => e.spec.id).join("', '"); + collector.report({ + code: 'EXTENSION_ATTACHMENT_CONFLICT', + message: `expected ${ input.config.optional ? 'at most' : 'exactly' - } one '${inputName}' input but received multiple: '${attachedNodeIds.join( - "', '", - )}'`, - ); + } one '${inputName}' input but received multiple: '${attachedNodeIds}'`, + }); + throw INSTANTIATION_FAILED; } else if (attachedNodes.length === 0) { - if (input.config.optional) { - return undefined; + if (!input.config.optional) { + collector.report({ + code: 'EXTENSION_ATTACHMENT_MISSING', + message: `input '${inputName}' is required but was not received`, + }); + throw INSTANTIATION_FAILED; } - throw Error(`input '${inputName}' is required but was not received`); + return undefined; } return resolveInputDataContainer( input.extensionData, attachedNodes[0], inputName, + collector, ); } - return attachedNodes.map(attachment => - resolveInputDataContainer(input.extensionData, attachment, inputName), + return mapWithFailures(attachedNodes, attachment => + resolveInputDataContainer( + input.extensionData, + attachment, + inputName, + collector, + ), ); }) as ResolvedExtensionInputs<{ [inputName in string]: ExtensionInput< @@ -247,8 +311,10 @@ export function createAppNodeInstance(options: { node: AppNode; apis: ApiHolder; attachments: ReadonlyMap; -}): AppNodeInstance { + collector: ErrorCollector; +}): AppNodeInstance | undefined { const { node, apis, attachments } = options; + const collector = options.collector.child({ node }); const { id, extension, config } = node.spec; const extensionData = new Map(); const extensionDataRefs = new Set>(); @@ -259,9 +325,11 @@ export function createAppNodeInstance(options: { [x: string]: any; }; } catch (e) { - throw new Error( - `Invalid configuration for extension '${id}'; caused by ${e}`, - ); + collector.report({ + code: 'EXTENSION_CONFIGURATION_INVALID', + message: `Invalid configuration for extension '${id}'; caused by ${e}`, + }); + return undefined; } try { @@ -297,7 +365,11 @@ export function createAppNodeInstance(options: { node, apis, config: parsedConfig, - inputs: resolveV2Inputs(internalExtension.inputs, attachments), + inputs: resolveV2Inputs( + internalExtension.inputs, + attachments, + collector, + ), }; const outputDataValues = options.extensionFactoryMiddleware ? createExtensionDataContainer( @@ -324,21 +396,34 @@ export function createAppNodeInstance(options: { } const outputDataMap = new Map(); - for (const value of outputDataValues) { + mapWithFailures(outputDataValues, value => { if (outputDataMap.has(value.id)) { - throw new Error(`duplicate extension data output '${value.id}'`); + collector.report({ + code: 'EXTENSION_OUTPUT_CONFLICT', + message: `extension factory output duplicate data '${value.id}'`, + context: { + dataRefId: value.id, + }, + }); + throw INSTANTIATION_FAILED; + } else { + outputDataMap.set(value.id, value.value); } - outputDataMap.set(value.id, value.value); - } + }); for (const ref of internalExtension.output) { const value = outputDataMap.get(ref.id); outputDataMap.delete(ref.id); if (value === undefined) { if (!ref.config.optional) { - throw new Error( - `missing required extension data output '${ref.id}'`, - ); + collector.report({ + code: 'EXTENSION_OUTPUT_MISSING', + message: `missing required extension data output '${ref.id}'`, + context: { + dataRefId: ref.id, + }, + }); + throw INSTANTIATION_FAILED; } } else { extensionData.set(ref.id, value); @@ -347,23 +432,36 @@ export function createAppNodeInstance(options: { } if (outputDataMap.size > 0) { - throw new Error( - `unexpected output '${Array.from(outputDataMap.keys()).join( - "', '", - )}'`, - ); + for (const dataRefId of outputDataMap.keys()) { + // TODO: Make this a warning + collector.report({ + code: 'EXTENSION_OUTPUT_IGNORED', + message: `unexpected output '${dataRefId}'`, + context: { + dataRefId: dataRefId, + }, + }); + } } } else { - throw new Error( - `unexpected extension version '${(internalExtension as any).version}'`, - ); + collector.report({ + code: 'EXTENSION_INVALID', + message: `unexpected extension version '${ + (internalExtension as any).version + }'`, + }); + throw INSTANTIATION_FAILED; } } catch (e) { - throw new Error( - `Failed to instantiate extension '${id}'${ - e.name === 'Error' ? `, ${e.message}` : `; caused by ${e.stack}` - }`, - ); + if (e !== INSTANTIATION_FAILED) { + collector.report({ + code: 'EXTENSION_FACTORY_ERROR', + message: `Failed to instantiate extension '${id}'${ + e.name === 'Error' ? `, ${e.message}` : `; caused by ${e}` + }`, + }); + } + return undefined; } return { @@ -383,8 +481,9 @@ export function createAppNodeInstance(options: { export function instantiateAppNodeTree( rootNode: AppNode, apis: ApiHolder, + collector: ErrorCollector, extensionFactoryMiddleware?: ExtensionFactoryMiddleware, -): void { +): boolean { function createInstance(node: AppNode): AppNodeInstance | undefined { if (node.instance) { return node.instance; @@ -413,10 +512,11 @@ export function instantiateAppNodeTree( node, apis, attachments: instantiatedAttachments, + collector, }); return node.instance; } - createInstance(rootNode); + return createInstance(rootNode) !== undefined; } diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts index 83abcf3dc4..2eacd0ae67 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts @@ -21,6 +21,18 @@ import { ExtensionDefinition, } from '@backstage/frontend-plugin-api'; import { resolveAppNodeSpecs } from './resolveAppNodeSpecs'; +import { createErrorCollector } from '../wiring/createErrorCollector'; + +const collector = createErrorCollector(); + +afterEach(() => { + const errors = collector.collectErrors(); + if (errors) { + throw new Error( + `Unexpected errors: ${errors.map(e => e.message).join(', ')}`, + ); + } +}); function makeExt( id: string, @@ -61,6 +73,7 @@ describe('resolveAppNodeSpecs', () => { features: [], builtinExtensions: [a], parameters: [], + collector, }), ).toEqual([ { @@ -83,6 +96,7 @@ describe('resolveAppNodeSpecs', () => { features: [], builtinExtensions: [a, b], parameters: [], + collector, }), ).toEqual([ { @@ -122,6 +136,7 @@ describe('resolveAppNodeSpecs', () => { attachTo: { id: 'derp', input: 'default' }, }, ], + collector, }), ).toEqual([ { @@ -169,6 +184,7 @@ describe('resolveAppNodeSpecs', () => { config: { foo: { qux: 3 } }, }, ], + collector, }), ).toEqual([ { @@ -209,6 +225,7 @@ describe('resolveAppNodeSpecs', () => { disabled: false, }, ], + collector, }), ).toEqual([ { @@ -249,6 +266,7 @@ describe('resolveAppNodeSpecs', () => { { id: 'd', disabled: false }, { id: 'c', disabled: false }, ], + collector, }), ).toEqual([ { @@ -341,6 +359,7 @@ describe('resolveAppNodeSpecs', () => { ], builtinExtensions: [], parameters: [], + collector, }), ).toEqual([ { @@ -395,6 +414,7 @@ describe('resolveAppNodeSpecs', () => { id, disabled: false, })), + collector, }); expect(result.map(r => r.extension.id)).toEqual([ @@ -405,53 +425,83 @@ describe('resolveAppNodeSpecs', () => { }); it('throws an error when a forbidden extension is overridden by a plugin', () => { - expect(() => - resolveAppNodeSpecs({ - features: [ - createFrontendPlugin({ - pluginId: 'test', - extensions: [makeExtDef('forbidden')], - }), - ], - builtinExtensions: [], - parameters: [], - forbidden: new Set(['test/forbidden']), - }), - ).toThrow( - "It is forbidden to override the following extension(s): 'test/forbidden', which is done by the following plugin(s): 'test'", - ); + const plugin = createFrontendPlugin({ + pluginId: 'test', + extensions: [makeExtDef('forbidden')], + }); + const result = resolveAppNodeSpecs({ + features: [plugin], + builtinExtensions: [], + parameters: [], + forbidden: new Set(['test/forbidden']), + collector, + }); + expect(result).toEqual([]); + + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_IGNORED', + message: + "It is forbidden to override the 'test/forbidden' extension, attempted by the 'test' plugin", + context: { + plugin, + extensionId: 'test/forbidden', + }, + }, + ]); }); it('throws an error when a forbidden extension is overridden by module', () => { - expect(() => - resolveAppNodeSpecs({ - features: [ - createFrontendPlugin({ - pluginId: 'forbidden', - extensions: [], - }), - createFrontendModule({ - pluginId: 'forbidden', - extensions: [makeExtDef()], - }), - ], - builtinExtensions: [], - parameters: [], - forbidden: new Set(['forbidden']), - }), - ).toThrow( - "It is forbidden to override the following extension(s): 'forbidden', which is done by a module for the following plugin(s): 'forbidden'", - ); + const plugin = createFrontendPlugin({ + pluginId: 'forbidden', + extensions: [], + }); + const result = resolveAppNodeSpecs({ + features: [ + plugin, + createFrontendModule({ + pluginId: 'forbidden', + extensions: [makeExtDef()], + }), + ], + builtinExtensions: [], + parameters: [], + forbidden: new Set(['forbidden']), + collector, + }); + expect(result).toEqual([]); + + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_IGNORED', + message: + "It is forbidden to override the 'forbidden' extension, attempted by the 'forbidden' plugin", + context: { + plugin, + extensionId: 'forbidden', + }, + }, + ]); }); it('throws an error when a forbidden extension is parametrized', () => { - expect(() => - resolveAppNodeSpecs({ - features: [], - builtinExtensions: [], - parameters: [{ id: 'forbidden', disabled: false }], - forbidden: new Set(['forbidden']), - }), - ).toThrow("Configuration of the 'forbidden' extension is forbidden"); + const result = resolveAppNodeSpecs({ + features: [], + builtinExtensions: [], + parameters: [{ id: 'forbidden', disabled: false }], + forbidden: new Set(['forbidden']), + collector, + }); + expect(result).toEqual([]); + + expect(collector.collectErrors()).toEqual([ + { + code: 'INVALID_EXTENSION_CONFIG_KEY', + message: "Configuration of the 'forbidden' extension is forbidden", + context: { + extensionId: 'forbidden', + }, + }, + ]); }); }); diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts index f9ce7387c2..d3b4ad6723 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts @@ -18,6 +18,7 @@ import { createFrontendPlugin, Extension, FrontendFeature, + FrontendPlugin, } from '@backstage/frontend-plugin-api'; import { ExtensionParameters } from './readAppExtensionsConfig'; import { AppNodeSpec } from '@backstage/frontend-plugin-api'; @@ -29,6 +30,7 @@ import { } from '../../../frontend-plugin-api/src/wiring/createFrontendModule'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtension } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; +import { ErrorCollector } from '../wiring/createErrorCollector'; /** @internal */ export function resolveAppNodeSpecs(options: { @@ -36,61 +38,58 @@ export function resolveAppNodeSpecs(options: { builtinExtensions?: Extension[]; parameters?: Array; forbidden?: Set; - allowUnknownExtensionConfig?: boolean; + collector: ErrorCollector; }): AppNodeSpec[] { const { builtinExtensions = [], parameters = [], forbidden = new Set(), features = [], - allowUnknownExtensionConfig = false, + collector, } = options; const plugins = features.filter(OpaqueFrontendPlugin.isType); const modules = features.filter(isInternalFrontendModule); + const filterForbidden = ( + extension: Extension & { plugin: FrontendPlugin }, + ) => { + if (forbidden.has(extension.id)) { + collector.report({ + code: 'EXTENSION_IGNORED', + message: `It is forbidden to override the '${extension.id}' extension, attempted by the '${extension.plugin.id}' plugin`, + context: { + plugin: extension.plugin, + extensionId: extension.id, + }, + }); + return false; + } + return true; + }; + const pluginExtensions = plugins.flatMap(plugin => { - return OpaqueFrontendPlugin.toInternal(plugin).extensions.map( - extension => ({ + return OpaqueFrontendPlugin.toInternal(plugin) + .extensions.map(extension => ({ ...extension, plugin, - }), - ); + })) + .filter(filterForbidden); }); const moduleExtensions = modules.flatMap(mod => - toInternalFrontendModule(mod).extensions.flatMap(extension => { - // Modules for plugins that are not installed are ignored - const plugin = plugins.find(p => p.id === mod.pluginId); - if (!plugin) { - return []; - } + toInternalFrontendModule(mod) + .extensions.flatMap(extension => { + // Modules for plugins that are not installed are ignored + const plugin = plugins.find(p => p.id === mod.pluginId); + if (!plugin) { + return []; + } - return [{ ...extension, plugin }]; - }), + return [{ ...extension, plugin }]; + }) + .filter(filterForbidden), ); - // Prevent core override - if (pluginExtensions.some(({ id }) => forbidden.has(id))) { - const pluginsStr = pluginExtensions - .filter(({ id }) => forbidden.has(id)) - .map(({ plugin }) => `'${plugin.id}'`) - .join(', '); - const forbiddenStr = [...forbidden].map(id => `'${id}'`).join(', '); - throw new Error( - `It is forbidden to override the following extension(s): ${forbiddenStr}, which is done by the following plugin(s): ${pluginsStr}`, - ); - } - if (moduleExtensions.some(({ id }) => forbidden.has(id))) { - const pluginsStr = moduleExtensions - .filter(({ id }) => forbidden.has(id)) - .map(({ plugin }) => `'${plugin.id}'`) - .join(', '); - const forbiddenStr = [...forbidden].map(id => `'${id}'`).join(', '); - throw new Error( - `It is forbidden to override the following extension(s): ${forbiddenStr}, which is done by a module for the following plugin(s): ${pluginsStr}`, - ); - } - const appPlugin = plugins.find(plugin => plugin.id === 'app') ?? createFrontendPlugin({ @@ -154,52 +153,41 @@ export function resolveAppNodeSpecs(options: { } } - const duplicatedExtensionIds = new Set(); - const duplicatedExtensionData = configuredExtensions.reduce< - Record> - >((data, { extension, params }) => { - const extensionId = extension.id; - const extensionData = data?.[extensionId]; - if (extensionData) duplicatedExtensionIds.add(extensionId); - const pluginId = params.source?.id ?? 'internal'; - const pluginCount = extensionData?.[pluginId] ?? 0; - return { - ...data, - [extensionId]: { ...extensionData, [pluginId]: pluginCount + 1 }, - }; - }, {}); + const seenExtensionIds = new Set(); + const deduplicatedExtensions = configuredExtensions.filter( + ({ extension, params }) => { + if (seenExtensionIds.has(extension.id)) { + collector.report({ + code: 'EXTENSION_IGNORED', + message: `The '${extension.id}' extension from the '${params.plugin.id}' plugin is a duplicate and will be ignored`, + context: { + plugin: params.plugin, + extensionId: extension.id, + }, + }); + return false; + } + seenExtensionIds.add(extension.id); + return true; + }, + ); - if (duplicatedExtensionIds.size > 0) { - throw new Error( - `The following extensions are duplicated: ${Array.from( - duplicatedExtensionIds, - ) - .map( - extensionId => - `The extension '${extensionId}' was provided ${Object.keys( - duplicatedExtensionData[extensionId], - ) - .map( - pluginId => - `${duplicatedExtensionData[extensionId][pluginId]} time(s) by the plugin '${pluginId}'`, - ) - .join(' and ')}`, - ) - .join(', ')}`, - ); - } - - const order = new Map(); + const order = new Map(); for (const overrideParam of parameters) { const extensionId = overrideParam.id; if (forbidden.has(extensionId)) { - throw new Error( - `Configuration of the '${extensionId}' extension is forbidden`, - ); + collector.report({ + code: 'INVALID_EXTENSION_CONFIG_KEY', + message: `Configuration of the '${extensionId}' extension is forbidden`, + context: { + extensionId, + }, + }); + continue; } - const existing = configuredExtensions.find( + const existing = deduplicatedExtensions.find( e => e.extension.id === extensionId, ); if (existing) { @@ -216,14 +204,20 @@ export function resolveAppNodeSpecs(options: { existing.params.disabled = Boolean(overrideParam.disabled); } order.set(extensionId, existing); - } else if (!allowUnknownExtensionConfig) { - throw new Error(`Extension ${extensionId} does not exist`); + } else { + collector.report({ + code: 'INVALID_EXTENSION_CONFIG_KEY', + message: `Extension ${extensionId} does not exist`, + context: { + extensionId, + }, + }); } } const orderedExtensions = [ ...order.values(), - ...configuredExtensions.filter(e => !order.has(e.extension.id)), + ...deduplicatedExtensions.filter(e => !order.has(e.extension.id)), ]; return orderedExtensions.map(param => ({ diff --git a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts index f1161b202e..bffc156509 100644 --- a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts @@ -24,6 +24,18 @@ import { import { resolveAppTree } from './resolveAppTree'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; +import { createErrorCollector } from '../wiring/createErrorCollector'; + +const collector = createErrorCollector(); + +afterEach(() => { + const errors = collector.collectErrors(); + if (errors) { + throw new Error( + `Unexpected errors: ${errors.map(e => e.message).join(', ')}`, + ); + } +}); const extension = resolveExtensionDefinition( createExtension({ @@ -43,13 +55,13 @@ const baseSpec = { describe('buildAppTree', () => { it('should fail to create an empty tree', () => { - expect(() => resolveAppTree('app', [])).toThrow( + expect(() => resolveAppTree('app', [], collector)).toThrow( "No root node with id 'app' found in app tree", ); }); it('should create a tree with only one node', () => { - const tree = resolveAppTree('app', [{ ...baseSpec, id: 'app' }]); + const tree = resolveAppTree('app', [{ ...baseSpec, id: 'app' }], collector); expect(tree.root).toEqual({ spec: { ...baseSpec, id: 'app' }, edges: { attachments: new Map() }, @@ -59,15 +71,19 @@ describe('buildAppTree', () => { }); it('should create a tree', () => { - const tree = resolveAppTree('b', [ - { ...baseSpec, id: 'a' }, - { ...baseSpec, id: 'b' }, - { ...baseSpec, id: 'c' }, - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, - { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, - { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, - ]); + const tree = resolveAppTree( + 'b', + [ + { ...baseSpec, id: 'a' }, + { ...baseSpec, id: 'b' }, + { ...baseSpec, id: 'c' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, + { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, + { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, + ], + collector, + ); expect(Array.from(tree.nodes.keys())).toEqual([ 'a', @@ -122,26 +138,30 @@ describe('buildAppTree', () => { }); it('should create a tree with clones', () => { - const tree = resolveAppTree('a', [ - { ...baseSpec, id: 'a' }, - { ...baseSpec, id: 'b', attachTo: { id: 'a', input: 'x' } }, - { - ...baseSpec, - id: 'c', - attachTo: [ - { id: 'a', input: 'x' }, - { id: 'b', input: 'x' }, - ], - }, - { - ...baseSpec, - id: 'd', - attachTo: [ - { id: 'b', input: 'x' }, - { id: 'c', input: 'x' }, - ], - }, - ]); + const tree = resolveAppTree( + 'a', + [ + { ...baseSpec, id: 'a' }, + { ...baseSpec, id: 'b', attachTo: { id: 'a', input: 'x' } }, + { + ...baseSpec, + id: 'c', + attachTo: [ + { id: 'a', input: 'x' }, + { id: 'b', input: 'x' }, + ], + }, + { + ...baseSpec, + id: 'd', + attachTo: [ + { id: 'b', input: 'x' }, + { id: 'c', input: 'x' }, + ], + }, + ], + collector, + ); expect(Array.from(tree.nodes.keys())).toEqual(['a', 'b', 'c', 'd']); @@ -172,15 +192,19 @@ describe('buildAppTree', () => { }); it('should create a tree out of order', () => { - const tree = resolveAppTree('b', [ - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, - { ...baseSpec, id: 'a' }, - { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, - { ...baseSpec, id: 'b' }, - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, - { ...baseSpec, id: 'c' }, - { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, - ]); + const tree = resolveAppTree( + 'b', + [ + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, + { ...baseSpec, id: 'a' }, + { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, + { ...baseSpec, id: 'b' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, + { ...baseSpec, id: 'c' }, + { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, + ], + collector, + ); expect(Array.from(tree.nodes.keys())).toEqual([ 'bx2', @@ -214,13 +238,16 @@ describe('buildAppTree', () => { `); }); - it('throws an error when duplicated extensions are detected', () => { - expect(() => - resolveAppTree('app', [ + it('ignores duplicate extensions', () => { + const tree = resolveAppTree( + 'a', + [ { ...baseSpec, id: 'a' }, { ...baseSpec, id: 'a' }, - ]), - ).toThrow("Unexpected duplicate extension id 'a'"); + ], + collector, + ); + expect(Array.from(tree.nodes.keys())).toEqual(['a']); }); describe('redirects', () => { @@ -253,12 +280,30 @@ describe('buildAppTree', () => { }), ) as Extension; - expect(() => - resolveAppTree('a', [ + const tree = resolveAppTree( + 'a', + [ { ...baseSpec, id: 'a', extension: e1 }, { ...baseSpec, id: 'b', extension: e2 }, - ]), - ).toThrow("Duplicate redirect target for input 'test' in extension 'b'"); + ], + collector, + ); + + expect(Array.from(tree.nodes.keys())).toEqual(['a', 'b']); + + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_INPUT_REDIRECT_CONFLICT', + message: + "Duplicate redirect target for input 'test' in extension 'b'", + context: { + node: expect.objectContaining({ + spec: { ...baseSpec, id: 'b', extension: e2 }, + }), + inputName: 'test', + }, + }, + ]); }); it('should set the correct attachment point for a redirect', () => { @@ -285,22 +330,26 @@ describe('buildAppTree', () => { }), ) as Extension; - const tree = resolveAppTree('a', [ - { - attachTo: e1.attachTo, - id: 'a', - extension: e1, - disabled: false, - plugin: baseSpec.plugin, - }, - { - attachTo: e2.attachTo, - id: 'b', - extension: e2, - disabled: false, - plugin: baseSpec.plugin, - }, - ]); + const tree = resolveAppTree( + 'a', + [ + { + attachTo: e1.attachTo, + id: 'a', + extension: e1, + disabled: false, + plugin: baseSpec.plugin, + }, + { + attachTo: e2.attachTo, + id: 'b', + extension: e2, + disabled: false, + plugin: baseSpec.plugin, + }, + ], + collector, + ); expect(tree.root).toMatchInlineSnapshot(` { @@ -365,29 +414,33 @@ describe('buildAppTree', () => { }), ) as Extension; - const tree = resolveAppTree('test-2', [ - { - attachTo: e1.attachTo, - id: e1.id, - extension: e1, - disabled: false, - plugin: baseSpec.plugin, - }, - { - attachTo: e2.attachTo, - id: e2.id, - extension: e2, - disabled: false, - plugin: baseSpec.plugin, - }, - { - attachTo: e3.attachTo, - id: e3.id, - extension: e3, - disabled: false, - plugin: baseSpec.plugin, - }, - ]); + const tree = resolveAppTree( + 'test-2', + [ + { + attachTo: e1.attachTo, + id: e1.id, + extension: e1, + disabled: false, + plugin: baseSpec.plugin, + }, + { + attachTo: e2.attachTo, + id: e2.id, + extension: e2, + disabled: false, + plugin: baseSpec.plugin, + }, + { + attachTo: e3.attachTo, + id: e3.id, + extension: e3, + disabled: false, + plugin: baseSpec.plugin, + }, + ], + collector, + ); expect(tree.nodes.get('test-3')?.edges.attachedTo?.node).toBe( tree.nodes.get('test-2'), diff --git a/packages/frontend-app-api/src/tree/resolveAppTree.ts b/packages/frontend-app-api/src/tree/resolveAppTree.ts index dacd06407a..f2f31f8950 100644 --- a/packages/frontend-app-api/src/tree/resolveAppTree.ts +++ b/packages/frontend-app-api/src/tree/resolveAppTree.ts @@ -23,6 +23,7 @@ import { // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtension } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; +import { ErrorCollector } from '../wiring/createErrorCollector'; function indent(str: string) { return str.replace(/^/gm, ' '); @@ -114,15 +115,16 @@ const isValidAttachmentPoint = ( export function resolveAppTree( rootNodeId: string, specs: AppNodeSpec[], + errorCollector: ErrorCollector, ): AppTree { const nodes = new Map(); const redirectTargetsByKey = new Map(); for (const spec of specs) { - // The main check with a more helpful error message happens in resolveAppNodeSpecs + // The main check with a helpful error message happens in resolveAppNodeSpecs if (nodes.has(spec.id)) { - throw new Error(`Unexpected duplicate extension id '${spec.id}'`); + continue; } const node = new SerializableAppNode(spec); @@ -134,9 +136,15 @@ export function resolveAppTree( for (const replace of input.replaces) { const key = makeRedirectKey(replace); if (redirectTargetsByKey.has(key)) { - throw new Error( - `Duplicate redirect target for input '${inputName}' in extension '${spec.id}'`, - ); + errorCollector.report({ + code: 'EXTENSION_INPUT_REDIRECT_CONFLICT', + message: `Duplicate redirect target for input '${inputName}' in extension '${spec.id}'`, + context: { + node, + inputName, + }, + }); + continue; } redirectTargetsByKey.set(key, { id: spec.id, input: inputName }); } diff --git a/packages/frontend-app-api/src/wiring/createErrorCollector.ts b/packages/frontend-app-api/src/wiring/createErrorCollector.ts new file mode 100644 index 0000000000..c20e92d0f0 --- /dev/null +++ b/packages/frontend-app-api/src/wiring/createErrorCollector.ts @@ -0,0 +1,150 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AppNode, FrontendPlugin } from '@backstage/frontend-plugin-api'; + +/** + * @public + */ +export type AppErrorTypes = { + // resolveAppNodeSpecs + EXTENSION_IGNORED: { + context: { plugin: FrontendPlugin; extensionId: string }; + }; + INVALID_EXTENSION_CONFIG_KEY: { + context: { extensionId: string }; + }; + // resolveAppTree + EXTENSION_INPUT_REDIRECT_CONFLICT: { + context: { node: AppNode; inputName: string }; + }; + // instantiateAppNodeTree + EXTENSION_INPUT_DATA_IGNORED: { + context: { node: AppNode; inputName: string }; + }; + EXTENSION_INPUT_DATA_MISSING: { + context: { node: AppNode; inputName: string }; + }; + EXTENSION_ATTACHMENT_CONFLICT: { + context: { node: AppNode; inputName: string }; + }; + EXTENSION_ATTACHMENT_MISSING: { + context: { node: AppNode; inputName: string }; + }; + EXTENSION_CONFIGURATION_INVALID: { + context: { node: AppNode }; + }; + EXTENSION_INVALID: { + context: { node: AppNode }; + }; + EXTENSION_OUTPUT_CONFLICT: { + context: { node: AppNode; dataRefId: string }; + }; + EXTENSION_OUTPUT_MISSING: { + context: { node: AppNode; dataRefId: string }; + }; + EXTENSION_OUTPUT_IGNORED: { + context: { node: AppNode; dataRefId: string }; + }; + EXTENSION_FACTORY_ERROR: { + context: { node: AppNode }; + }; + // createSpecializedApp + API_EXTENSION_INVALID: { + context: { node: AppNode }; + }; + // routing + ROUTE_DUPLICATE: { + context: { routeId: string }; + }; + ROUTE_BINDING_INVALID_VALUE: { + context: { routeId: string }; + }; + ROUTE_NOT_FOUND: { + context: { routeId: string }; + }; +}; + +/** + * @public + */ +export type AppError = + keyof AppErrorTypes extends infer ICode extends keyof AppErrorTypes + ? ICode extends any + ? { + code: ICode; + message: string; + context: AppErrorTypes[ICode]['context']; + } + : never + : never; + +/** @internal */ +export interface ErrorCollector { + report( + report: Omit< + AppErrorTypes[TCode]['context'], + keyof TContext + > extends infer IContext extends {} + ? {} extends IContext + ? { + code: TCode; + message: string; + } + : { + code: TCode; + message: string; + context: IContext; + } + : never, + ): void; + child( + context: TAdditionalContext, + ): ErrorCollector; + collectErrors(): AppError[] | undefined; +} + +/** @internal */ +export function createErrorCollector( + context?: Partial, +): ErrorCollector { + const errors: AppError[] = []; + const children: ErrorCollector[] = []; + return { + report(report: { code: string; message: string; context?: {} }) { + errors.push({ + ...report, + context: { ...context, ...report.context }, + } as AppError); + }, + collectErrors() { + const allErrors = [ + ...errors, + ...children.flatMap(child => child.collectErrors() ?? []), + ]; + errors.length = 0; + if (allErrors.length === 0) { + return undefined; + } + return allErrors; + }, + child(childContext) { + const child = createErrorCollector({ ...context, ...childContext }); + children.push(child); + return child as ErrorCollector; + }, + }; +} diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx index 01e0a6f657..6c6d75415b 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx @@ -83,6 +83,11 @@ import { FrontendPluginInfoResolver, } from './createPluginInfoAttacher'; import { createRouteAliasResolver } from '../routing/RouteAliasResolver'; +import { + AppError, + createErrorCollector, + ErrorCollector, +} from './createErrorCollector'; function deduplicateFeatures( allFeatures: FrontendFeature[], @@ -284,12 +289,15 @@ export type CreateSpecializedAppOptions = { export function createSpecializedApp(options?: CreateSpecializedAppOptions): { apis: ApiHolder; tree: AppTree; + errors?: AppError[]; } { const config = options?.config ?? new ConfigReader({}, 'empty-config'); const features = deduplicateFeatures(options?.features ?? []).map( createPluginInfoAttacher(config, options?.advanced?.pluginInfoResolver), ); + const collector = createErrorCollector(); + const tree = resolveAppTree( 'root', resolveAppNodeSpecs({ @@ -299,18 +307,18 @@ export function createSpecializedApp(options?: CreateSpecializedAppOptions): { ], parameters: readAppExtensionsConfig(config), forbidden: new Set(['root']), - allowUnknownExtensionConfig: - options?.advanced?.allowUnknownExtensionConfig, + collector, }), + collector, ); - const factories = createApiFactories({ tree }); + const factories = createApiFactories({ tree, collector }); const appBasePath = getBasePath(config); const appTreeApi = new AppTreeApiProxy(tree, appBasePath); - const routeRefsById = collectRouteIds(features); + const routeRefsById = collectRouteIds(features, collector); const routeResolutionApi = new RouteResolutionApiProxy( - resolveRouteBindings(options?.bindRoutes, config, routeRefsById), + resolveRouteBindings(options?.bindRoutes, config, routeRefsById, collector), appBasePath, ); @@ -353,6 +361,7 @@ export function createSpecializedApp(options?: CreateSpecializedAppOptions): { instantiateAppNodeTree( tree.root, apis, + collector, mergeExtensionFactoryMiddleware( options?.advanced?.extensionFactoryMiddleware, ), @@ -366,22 +375,32 @@ export function createSpecializedApp(options?: CreateSpecializedAppOptions): { routeResolutionApi.initialize(routeInfo, routeRefsById.routes); appTreeApi.initialize(routeInfo); - return { apis, tree }; + return { apis, tree, errors: collector.collectErrors() }; } -function createApiFactories(options: { tree: AppTree }): AnyApiFactory[] { +function createApiFactories(options: { + tree: AppTree; + collector: ErrorCollector; +}): AnyApiFactory[] { const emptyApiHolder = ApiRegistry.from([]); const factories = new Array(); for (const apiNode of options.tree.root.edges.attachments.get('apis') ?? []) { - instantiateAppNodeTree(apiNode, emptyApiHolder); - const apiFactory = apiNode.instance?.getData(ApiBlueprint.dataRefs.factory); - if (!apiFactory) { - throw new Error( - `No API factory found in for extension ${apiNode.spec.id}`, - ); + if (!instantiateAppNodeTree(apiNode, emptyApiHolder, options.collector)) { + continue; + } + const apiFactory = apiNode.instance?.getData(ApiBlueprint.dataRefs.factory); + if (apiFactory) { + factories.push(apiFactory); + } else { + options.collector.report({ + code: 'API_EXTENSION_INVALID', + message: `API extension '${apiNode.spec.id}' did not output an API factory`, + context: { + node: apiNode, + }, + }); } - factories.push(apiFactory); } return factories; diff --git a/packages/frontend-app-api/src/wiring/index.ts b/packages/frontend-app-api/src/wiring/index.ts index 2571cadfbd..1e18859ddd 100644 --- a/packages/frontend-app-api/src/wiring/index.ts +++ b/packages/frontend-app-api/src/wiring/index.ts @@ -19,3 +19,4 @@ export { type CreateSpecializedAppOptions, } from './createSpecializedApp'; export { type FrontendPluginInfoResolver } from './createPluginInfoAttacher'; +export { type AppError, type AppErrorTypes } from './createErrorCollector'; diff --git a/packages/frontend-defaults/CHANGELOG.md b/packages/frontend-defaults/CHANGELOG.md index 86eae4efdf..65713fac5f 100644 --- a/packages/frontend-defaults/CHANGELOG.md +++ b/packages/frontend-defaults/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/frontend-defaults +## 0.3.1 + +### Patch Changes + +- 6516c3d: The default app now leverages the new error reporting functionality from `@backstage/frontend-app-api`. If there are critical errors during startup, an error screen that shows a summary of all errors will now be shown, rather than leaving the screen blank. Other errors will be logged as warnings in the console. +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-app@0.3.0 + - @backstage/core-components@0.18.0 + - @backstage/frontend-app-api@0.13.0 + ## 0.3.1-next.0 ### Patch Changes diff --git a/packages/frontend-defaults/knip-report.md b/packages/frontend-defaults/knip-report.md index e6696c0d8d..8c0979b2c4 100644 --- a/packages/frontend-defaults/knip-report.md +++ b/packages/frontend-defaults/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :--------------- | :----------- | :------- | -| @react-hookz/web | package.json | error | +| @react-hookz/web | packages/frontend-defaults/package.json | error | diff --git a/packages/frontend-defaults/package.json b/packages/frontend-defaults/package.json index 880cd618ef..6a8b100c0d 100644 --- a/packages/frontend-defaults/package.json +++ b/packages/frontend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-defaults", - "version": "0.3.1-next.0", + "version": "0.3.1", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-defaults/report.api.md b/packages/frontend-defaults/report.api.md index 6fd2536251..75392d2dc1 100644 --- a/packages/frontend-defaults/report.api.md +++ b/packages/frontend-defaults/report.api.md @@ -3,6 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AppError } from '@backstage/frontend-app-api'; +import { AppErrorTypes } from '@backstage/frontend-app-api'; import { Config } from '@backstage/config'; import { ConfigApi } from '@backstage/frontend-plugin-api'; import { CreateAppRouteBinder } from '@backstage/frontend-app-api'; @@ -45,6 +47,16 @@ export function discoverAvailableFeatures(config: Config): { features: (FrontendFeature | FrontendFeatureLoader)[]; }; +// @public +export function maybeCreateErrorPage( + app: { + errors?: AppError[]; + }, + options?: { + warningCodes?: Array; + }, +): JSX_2.Element | undefined; + // @public (undocumented) export function resolveAsyncFeatures(options: { config: Config; diff --git a/packages/frontend-defaults/src/createApp.test.tsx b/packages/frontend-defaults/src/createApp.test.tsx index 351bc8f1b5..7b5e345e12 100644 --- a/packages/frontend-defaults/src/createApp.test.tsx +++ b/packages/frontend-defaults/src/createApp.test.tsx @@ -372,6 +372,7 @@ describe('createApp', () => { + diff --git a/packages/frontend-defaults/src/createApp.tsx b/packages/frontend-defaults/src/createApp.tsx index 2c8a80791f..4ff67f6d5a 100644 --- a/packages/frontend-defaults/src/createApp.tsx +++ b/packages/frontend-defaults/src/createApp.tsx @@ -36,6 +36,7 @@ import { import appPlugin from '@backstage/plugin-app'; import { discoverAvailableFeatures } from './discovery'; import { resolveAsyncFeatures } from './resolution'; +import { maybeCreateErrorPage } from './maybeCreateErrorPage'; /** * Options for {@link createApp}. @@ -136,6 +137,11 @@ export function createApp(options?: CreateAppOptions): { advanced: options?.advanced, }); + const errorPage = maybeCreateErrorPage(app); + if (errorPage) { + return { default: () => errorPage }; + } + const rootEl = app.tree.root.instance!.getData( coreExtensionData.reactElement, ); diff --git a/packages/frontend-defaults/src/index.ts b/packages/frontend-defaults/src/index.ts index 68d4c72568..37ec62e794 100644 --- a/packages/frontend-defaults/src/index.ts +++ b/packages/frontend-defaults/src/index.ts @@ -24,3 +24,4 @@ export { createApp, type CreateAppOptions } from './createApp'; export { createPublicSignInApp } from './createPublicSignInApp'; export { discoverAvailableFeatures } from './discovery'; export { resolveAsyncFeatures } from './resolution'; +export { maybeCreateErrorPage } from './maybeCreateErrorPage'; diff --git a/packages/frontend-defaults/src/maybeCreateErrorPage.tsx b/packages/frontend-defaults/src/maybeCreateErrorPage.tsx new file mode 100644 index 0000000000..6bad97f666 --- /dev/null +++ b/packages/frontend-defaults/src/maybeCreateErrorPage.tsx @@ -0,0 +1,116 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FrontendPluginInfo } from '@backstage/frontend-plugin-api'; +import { JSX, useEffect, useState } from 'react'; +import { AppError, AppErrorTypes } from '@backstage/frontend-app-api'; + +const DEFAULT_WARNING_CODES: Array = [ + 'EXTENSION_IGNORED', + 'INVALID_EXTENSION_CONFIG_KEY', + 'EXTENSION_INPUT_DATA_IGNORED', + 'EXTENSION_OUTPUT_IGNORED', +]; + +function AppErrorItem(props: { error: AppError }): JSX.Element { + const { context } = props.error; + + const node = 'node' in context ? context.node : undefined; + const extensionId = + 'extensionId' in context ? context.extensionId : node?.spec.id; + const routeId = 'routeId' in context ? context.routeId : undefined; + const plugin = 'plugin' in context ? context.plugin : node?.spec.plugin; + const pluginId = plugin?.id ?? 'N/A'; + + const [info, setInfo] = useState(undefined); + useEffect(() => { + plugin?.info().then(setInfo, error => { + // eslint-disable-next-line no-console + console.error(`Failed to load info for plugin ${plugin.id}: ${error}`); + }); + }, [plugin]); + + return ( +
+ {props.error.code}: {props.error.message} +
+        {extensionId && 
extensionId: {extensionId}
} + {routeId &&
routeId: {routeId}
} + {pluginId &&
pluginId: {pluginId}
} + {info && ( +
+ package: {info.packageName}@{info.version} +
+ )} +
+
+ ); +} + +function AppErrorPage(props: { errors: AppError[] }): JSX.Element { + return ( +
+

App startup failed

+ {props.errors.map((error, index) => ( + + ))} +
+ ); +} + +/** + * If there are any unrecoverable errors in the app, this will return an error page in the form of a JSX element. + * + * If there are any recoverable errors, they will always be logged as warnings in the console. + * @public + */ +export function maybeCreateErrorPage( + app: { errors?: AppError[] }, + options?: { + warningCodes?: Array; + }, +): JSX.Element | undefined { + if (!app.errors) { + return undefined; + } + + const errors = new Array(); + const warnings = new Array(); + + const warningCodes = new Set(options?.warningCodes ?? DEFAULT_WARNING_CODES); + for (const error of app.errors) { + if (warningCodes.has(error.code)) { + warnings.push(error); + } else { + errors.push(error); + } + } + + if (warnings.length > 0) { + // eslint-disable-next-line no-console + console.warn('App startup encountered warnings:'); + for (const warning of warnings) { + // eslint-disable-next-line no-console + console.warn(`${warning.code}: ${warning.message}`); + } + } + + if (errors.length === 0) { + return undefined; + } + + return ; +} diff --git a/packages/frontend-dynamic-feature-loader/CHANGELOG.md b/packages/frontend-dynamic-feature-loader/CHANGELOG.md index dda15e8fd1..0e7886365d 100644 --- a/packages/frontend-dynamic-feature-loader/CHANGELOG.md +++ b/packages/frontend-dynamic-feature-loader/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/frontend-dynamic-feature-loader +## 0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + ## 0.1.5-next.0 ### Patch Changes diff --git a/packages/frontend-dynamic-feature-loader/knip-report.md b/packages/frontend-dynamic-feature-loader/knip-report.md index 3d019810db..0645b2b1cc 100644 --- a/packages/frontend-dynamic-feature-loader/knip-report.md +++ b/packages/frontend-dynamic-feature-loader/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---------------- | :----------- | :------- | -| @backstage/config | package.json | error | +| @backstage/config | packages/frontend-dynamic-feature-loader/package.json | error | diff --git a/packages/frontend-dynamic-feature-loader/package.json b/packages/frontend-dynamic-feature-loader/package.json index b965bdf7a1..7ed00c92de 100644 --- a/packages/frontend-dynamic-feature-loader/package.json +++ b/packages/frontend-dynamic-feature-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-dynamic-feature-loader", - "version": "0.1.5-next.0", + "version": "0.1.5", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-internal/CHANGELOG.md b/packages/frontend-internal/CHANGELOG.md index 0537c8ffd1..7de465f8c0 100644 --- a/packages/frontend-internal/CHANGELOG.md +++ b/packages/frontend-internal/CHANGELOG.md @@ -1,5 +1,13 @@ # @internal/frontend +## 0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/types@1.2.2 + ## 0.0.13-next.0 ### Patch Changes diff --git a/packages/frontend-internal/knip-report.md b/packages/frontend-internal/knip-report.md index edbbb970ac..e23233d94e 100644 --- a/packages/frontend-internal/knip-report.md +++ b/packages/frontend-internal/knip-report.md @@ -4,16 +4,16 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @backstage/version-bridge | package.json | error | -| zod | package.json | error | +| @backstage/version-bridge | packages/frontend-internal/package.json | error | +| zod | packages/frontend-internal/package.json | error | ## Unused devDependencies (5) | Name | Location | Severity | | :----------------------------- | :----------- | :------- | -| @backstage/frontend-test-utils | package.json | error | -| @backstage/frontend-app-api | package.json | error | -| @testing-library/jest-dom | package.json | error | -| @testing-library/react | package.json | error | -| @backstage/test-utils | package.json | error | +| @backstage/frontend-test-utils | packages/frontend-internal/package.json | error | +| @backstage/frontend-app-api | packages/frontend-internal/package.json | error | +| @testing-library/jest-dom | packages/frontend-internal/package.json | error | +| @testing-library/react | packages/frontend-internal/package.json | error | +| @backstage/test-utils | packages/frontend-internal/package.json | error | diff --git a/packages/frontend-internal/package.json b/packages/frontend-internal/package.json index ea68ff1405..2399eaa453 100644 --- a/packages/frontend-internal/package.json +++ b/packages/frontend-internal/package.json @@ -1,6 +1,6 @@ { "name": "@internal/frontend", - "version": "0.0.13-next.0", + "version": "0.0.13", "backstage": { "role": "web-library", "inline": true diff --git a/packages/frontend-plugin-api/CHANGELOG.md b/packages/frontend-plugin-api/CHANGELOG.md index 1b39c5ad44..647bd8f6f1 100644 --- a/packages/frontend-plugin-api/CHANGELOG.md +++ b/packages/frontend-plugin-api/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/frontend-plugin-api +## 0.12.0 + +### Minor Changes + +- 894d514: Make `openshiftApiRef` available to the new frontend system. + +### Patch Changes + +- 2fb8b04: Improved the types of `createFrontendPlugin` and `createFrontendModule` so that errors due to incompatible options are indicated more clearly. +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + ## 0.11.1-next.0 ### Patch Changes diff --git a/packages/frontend-plugin-api/knip-report.md b/packages/frontend-plugin-api/knip-report.md index 402fea3d47..d76f9ef7d9 100644 --- a/packages/frontend-plugin-api/knip-report.md +++ b/packages/frontend-plugin-api/knip-report.md @@ -4,13 +4,13 @@ | Name | Location | Severity | | :------------------------- | :----------- | :------- | -| @backstage/core-components | package.json | error | -| @material-ui/core | package.json | error | -| lodash | package.json | error | +| @backstage/core-components | packages/frontend-plugin-api/package.json | error | +| @material-ui/core | packages/frontend-plugin-api/package.json | error | +| lodash | packages/frontend-plugin-api/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/frontend-app-api | package.json | error | +| @backstage/frontend-app-api | packages/frontend-plugin-api/package.json | error | diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index 2eb218e5ab..1e261787fb 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-plugin-api", - "version": "0.11.1-next.0", + "version": "0.12.0", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index e6259c70d7..f9e2336408 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -69,6 +69,7 @@ import { OAuthScope } from '@backstage/core-plugin-api'; import { oktaAuthApiRef } from '@backstage/core-plugin-api'; import { oneloginAuthApiRef } from '@backstage/core-plugin-api'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; +import { openshiftAuthApiRef } from '@backstage/core-plugin-api'; import { PendingOAuthRequest } from '@backstage/core-plugin-api'; import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; @@ -1518,6 +1519,8 @@ export { oneloginAuthApiRef }; export { OpenIdConnectApi }; +export { openshiftAuthApiRef }; + // @public export interface OverridableFrontendPlugin< TRoutes extends { diff --git a/packages/frontend-plugin-api/src/apis/definitions/auth.ts b/packages/frontend-plugin-api/src/apis/definitions/auth.ts index 89509082f0..5a31c1603d 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/auth.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/auth.ts @@ -37,4 +37,5 @@ export { microsoftAuthApiRef, oneloginAuthApiRef, vmwareCloudAuthApiRef, + openshiftAuthApiRef, } from '@backstage/core-plugin-api'; diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 00dd981e26..59a81f1b59 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -1078,7 +1078,7 @@ describe('createExtension', () => { .add(multi2Ext) .get(outputRef), ).toThrowErrorMatchingInlineSnapshot( - `"Failed to instantiate extension 'subject', override data provided for input 'multi' must match the length of the original inputs"`, + `"Failed to resolve the extension tree: Failed to instantiate extension 'subject', override data provided for input 'multi' must match the length of the original inputs"`, ); // Mix forward and data override @@ -1101,7 +1101,7 @@ describe('createExtension', () => { .add(multi2Ext) .get(outputRef), ).toThrowErrorMatchingInlineSnapshot( - `"Failed to instantiate extension 'subject', override data for input 'multi' may not mix forwarded inputs with data overrides"`, + `"Failed to resolve the extension tree: Failed to instantiate extension 'subject', override data for input 'multi' may not mix forwarded inputs with data overrides"`, ); // Required input not provided @@ -1124,7 +1124,7 @@ describe('createExtension', () => { .add(multi2Ext) .get(outputRef), ).toThrowErrorMatchingInlineSnapshot( - `"Failed to instantiate extension 'subject', missing required extension data value(s) 'test1'"`, + `"Failed to resolve the extension tree: Failed to instantiate extension 'subject', missing required extension data value(s) 'test1'"`, ); // Wrong value provided @@ -1153,7 +1153,7 @@ describe('createExtension', () => { .add(multi2Ext) .get(outputRef), ).toThrowErrorMatchingInlineSnapshot( - `"Failed to instantiate extension 'subject', extension data 'test2' was provided but not declared"`, + `"Failed to resolve the extension tree: Failed to instantiate extension 'subject', extension data 'test2' was provided but not declared"`, ); }); }); diff --git a/packages/frontend-test-utils/CHANGELOG.md b/packages/frontend-test-utils/CHANGELOG.md index 585322aa8e..4e96c27dd3 100644 --- a/packages/frontend-test-utils/CHANGELOG.md +++ b/packages/frontend-test-utils/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/frontend-test-utils +## 0.3.6 + +### Patch Changes + +- 6516c3d: Internal update to use and throw app errors. +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-app@0.3.0 + - @backstage/frontend-app-api@0.13.0 + - @backstage/types@1.2.2 + ## 0.3.6-next.0 ### Patch Changes diff --git a/packages/frontend-test-utils/knip-report.md b/packages/frontend-test-utils/knip-report.md index 50f4f788e3..5415a4c648 100644 --- a/packages/frontend-test-utils/knip-report.md +++ b/packages/frontend-test-utils/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @backstage/version-bridge | package.json | error | -| zod | package.json | error | +| @backstage/version-bridge | packages/frontend-test-utils/package.json | error | +| zod | packages/frontend-test-utils/package.json | error | diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index b45dbb76e2..ba71540d7a 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-test-utils", - "version": "0.3.6-next.0", + "version": "0.3.6", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index 7b321d4413..50d65fddc7 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -35,6 +35,8 @@ import { resolveAppNodeSpecs } from '../../../frontend-app-api/src/tree/resolveA import { instantiateAppNodeTree } from '../../../frontend-app-api/src/tree/instantiateAppNodeTree'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { readAppExtensionsConfig } from '../../../frontend-app-api/src/tree/readAppExtensionsConfig'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { createErrorCollector } from '../../../frontend-app-api/src/wiring/createErrorCollector'; import { TestApiRegistry } from '@backstage/test-utils'; import { OpaqueExtensionDefinition } from '@internal/frontend'; @@ -191,16 +193,29 @@ export class ExtensionTester { ); } + const collector = createErrorCollector(); + const tree = resolveAppTree( subject.id, resolveAppNodeSpecs({ features: [], builtinExtensions: this.#extensions.map(_ => _.extension), parameters: readAppExtensionsConfig(this.#getConfig()), + collector, }), + collector, ); - instantiateAppNodeTree(tree.root, TestApiRegistry.from()); + instantiateAppNodeTree(tree.root, TestApiRegistry.from(), collector); + + const errors = collector.collectErrors(); + if (errors) { + throw new Error( + `Failed to resolve the extension tree: ${errors + .map(e => e.message) + .join(', ')}`, + ); + } this.#tree = tree; diff --git a/packages/integration-aws-node/knip-report.md b/packages/integration-aws-node/knip-report.md index 8927891173..efa9f826b6 100644 --- a/packages/integration-aws-node/knip-report.md +++ b/packages/integration-aws-node/knip-report.md @@ -4,13 +4,13 @@ | Name | Location | Severity | | :-------------------------------- | :----------- | :------- | -| @aws-sdk/credential-provider-node | package.json | error | -| @backstage/errors | package.json | error | +| @aws-sdk/credential-provider-node | packages/integration-aws-node/package.json | error | +| @backstage/errors | packages/integration-aws-node/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :----------------------- | :----------- | :------- | -| @backstage/config-loader | package.json | error | -| @backstage/test-utils | package.json | error | +| @backstage/config-loader | packages/integration-aws-node/package.json | error | +| @backstage/test-utils | packages/integration-aws-node/package.json | error | diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index 543f8796dd..b152cf511b 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/integration-react +## 1.2.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/integration@1.18.0 + ## 1.2.10-next.0 ### Patch Changes diff --git a/packages/integration-react/knip-report.md b/packages/integration-react/knip-report.md index 551f19ab2f..dd0df6e7f4 100644 --- a/packages/integration-react/knip-report.md +++ b/packages/integration-react/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------------------- | :----------- | :------- | -| @testing-library/dom | package.json | error | -| msw | package.json | error | +| @testing-library/dom | packages/integration-react/package.json | error | +| msw | packages/integration-react/package.json | error | diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 1a7d6dc01e..a3d8000f94 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration-react", - "version": "1.2.10-next.0", + "version": "1.2.10", "description": "Frontend package for managing integrations towards external systems", "backstage": { "role": "web-library" diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index 11f87da988..946e392c0b 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/integration +## 1.18.0 + +### Minor Changes + +- 03bdc68: Added support for limiting GithubAppCredentialsMux to specific apps + +### Patch Changes + +- 56897d7: Fixes issue with Github credentials provider which fails to match organization name if using allowedInstallationOwners + ## 1.18.0-next.0 ### Minor Changes diff --git a/packages/integration/package.json b/packages/integration/package.json index 014bd3564e..68289e2079 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration", - "version": "1.18.0-next.0", + "version": "1.18.0", "description": "Helpers for managing integrations towards external systems", "backstage": { "role": "common-library" diff --git a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts index ec07d1b465..0468e96e14 100644 --- a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts +++ b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts @@ -231,6 +231,42 @@ describe('SingleInstanceGithubCredentialsProvider tests', () => { expect(token).toEqual(undefined); }); + it('should not fail to issue tokens for an organization when there is a case mismatch in the organization name', async () => { + octokit.apps.listInstallations.mockResolvedValue({ + headers: { + etag: '123', + }, + data: [ + { + id: 1, + repository_selection: 'selected', + account: { + login: 'backstage', + }, + }, + ], + } as RestEndpointMethodTypes['apps']['listInstallations']['response']); + + octokit.apps.createInstallationAccessToken.mockResolvedValueOnce({ + data: { + expires_at: DateTime.local().plus({ hours: 1 }).toString(), + token: 'secret_token', + repository_selection: 'selected', + }, + } as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']); + + octokit.apps.listReposAccessibleToInstallation.mockReturnValue({ + data: [{ name: 'some-repo' }], + } as unknown as RestEndpointMethodTypes['apps']['listReposAccessibleToInstallation']['response']); + + const { token, headers } = await github.getCredentials({ + url: 'https://github.com/Backstage', + }); + const expectedToken = 'secret_token'; + expect(headers).toEqual({ Authorization: `Bearer ${expectedToken}` }); + expect(token).toEqual('secret_token'); + }); + it('should not fail to issue tokens for an organization when the app is installed for a single repo', async () => { octokit.apps.listInstallations.mockResolvedValue({ headers: { diff --git a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts index fd3c02486f..eaaabe8b0f 100644 --- a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts +++ b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts @@ -102,7 +102,9 @@ class GithubAppManager { private readonly allowedInstallationOwners: string[] | undefined; // undefined allows all installations constructor(config: GithubAppConfig, baseUrl?: string) { - this.allowedInstallationOwners = config.allowedInstallationOwners; + this.allowedInstallationOwners = config.allowedInstallationOwners?.map( + owner => owner.toLocaleLowerCase('en-US'), + ); this.baseUrl = baseUrl; this.baseAuthConfig = { appId: config.appId, @@ -121,7 +123,11 @@ class GithubAppManager { repo?: string, ): Promise<{ accessToken: string | undefined }> { if (this.allowedInstallationOwners) { - if (!this.allowedInstallationOwners?.includes(owner)) { + if ( + !this.allowedInstallationOwners?.includes( + owner.toLocaleLowerCase('en-US'), + ) + ) { return { accessToken: undefined }; // An empty token allows anonymous access to public repos } } diff --git a/packages/opaque-internal/knip-report.md b/packages/opaque-internal/knip-report.md index b131ea8948..b49ecc8f5f 100644 --- a/packages/opaque-internal/knip-report.md +++ b/packages/opaque-internal/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @testing-library/jest-dom | package.json | error | -| @testing-library/react | package.json | error | +| @testing-library/jest-dom | packages/opaque-internal/package.json | error | +| @testing-library/react | packages/opaque-internal/package.json | error | diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index 569444b4b8..d93d88571a 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/repo-tools +## 0.15.2 + +### Patch Changes + +- 133ac7a: Fixed knip-reports command failing when workspace path contains spaces and process termination issues by replacing `execFile` with `spawn` and removing `shell` option. +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/backend-plugin-api@1.4.3 + +## 0.15.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + ## 0.15.2-next.0 ### Patch Changes diff --git a/packages/repo-tools/knip-report.md b/packages/repo-tools/knip-report.md index 02f92286c1..f87d954145 100644 --- a/packages/repo-tools/knip-report.md +++ b/packages/repo-tools/knip-report.md @@ -4,21 +4,21 @@ | Name | Location | Severity | | :---------------------------------- | :----------- | :------- | -| @openapitools/openapi-generator-cli | package.json | error | -| @stoplight/spectral-runtime | package.json | error | -| @electric-sql/pglite | package.json | error | -| is-glob | package.json | error | +| @openapitools/openapi-generator-cli | packages/repo-tools/package.json | error | +| @stoplight/spectral-runtime | packages/repo-tools/package.json | error | +| @electric-sql/pglite | packages/repo-tools/package.json | error | +| is-glob | packages/repo-tools/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :------------- | :----------- | :------- | -| @types/is-glob | package.json | error | -| typedoc | package.json | error | +| @types/is-glob | packages/repo-tools/package.json | error | +| typedoc | packages/repo-tools/package.json | error | ## Referenced optional peerDependencies (1) | Name | Location | Severity | | :------- | :----------- | :------- | -| prettier | package.json | error | +| prettier | packages/repo-tools/package.json | error | diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index 9b7c875279..0b0954da70 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/repo-tools", - "version": "0.15.2-next.0", + "version": "0.15.2", "description": "CLI for Backstage repo tooling ", "backstage": { "role": "cli" diff --git a/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts b/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts index a97d1ddd8f..68bb0a9835 100644 --- a/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts +++ b/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts @@ -106,13 +106,17 @@ async function handlePackage({ let report = await run( `${knipDir}/knip.js`, - `-W ${packageDir}`, // Run the desired workspace - '--config knip.json', + '-W', // Run the desired workspace + packageDir, + '--config', + 'knip.json', '--no-exit-code', // Removing this will end the process in case there are findings by knip '--no-progress', // Remove unnecessary debugging from output // TODO: Add more checks when dependencies start to look ok, see https://knip.dev/reference/cli#--include - '--include dependencies,unlisted', - '--reporter markdown', + '--include', + 'dependencies,unlisted', + '--reporter', + 'markdown', ); // Adjust report paths to be relative to workspace diff --git a/packages/repo-tools/src/commands/util.ts b/packages/repo-tools/src/commands/util.ts index ff4cdad1b8..c232a8aff2 100644 --- a/packages/repo-tools/src/commands/util.ts +++ b/packages/repo-tools/src/commands/util.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { execFile } from 'child_process'; +import { spawn } from 'child_process'; import os from 'os'; import pLimit from 'p-limit'; @@ -29,26 +29,43 @@ export function createBinRunner(cwd: string, path: string) { limiter( () => new Promise((resolve, reject) => { - execFile( - 'node', - [path, ...command], - { - cwd, - shell: true, - timeout: 3 * 60 * 1000, - maxBuffer: 1024 * 1024, - }, - (err, stdout, stderr) => { - if (err) { - console.log('err', err); - reject(new Error(`${err.message}\n${stderr}`)); - } else if (stderr) { - reject(new Error(`Command printed error output: ${stderr}`)); - } else { - resolve(stdout); - } - }, - ); + // Handle the case where path is empty and the script path is the first command argument + const args = path ? [path, ...command] : command; + const child = spawn('node', args, { + cwd, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + let stdout = ''; + let stderr = ''; + + child.stdout?.on('data', data => { + stdout += data.toString(); + }); + + child.stderr?.on('data', data => { + stderr += data.toString(); + }); + + child.on('error', err => { + reject(new Error(`Process error: ${err.message}`)); + }); + + child.on('close', (code, signal) => { + if (signal) { + reject( + new Error( + `Process was killed with signal ${signal}\n${stderr}`, + ), + ); + } else if (code !== 0) { + reject(new Error(`Process exited with code ${code}\n${stderr}`)); + } else if (stderr.trim()) { + reject(new Error(`Command printed error output: ${stderr}`)); + } else { + resolve(stdout); + } + }); }), ); } diff --git a/packages/scaffolder-internal/CHANGELOG.md b/packages/scaffolder-internal/CHANGELOG.md index dea0dbb318..de8c725e03 100644 --- a/packages/scaffolder-internal/CHANGELOG.md +++ b/packages/scaffolder-internal/CHANGELOG.md @@ -1,5 +1,13 @@ # @internal/scaffolder +## 0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-scaffolder-react@1.19.1 + ## 0.0.13-next.1 ### Patch Changes diff --git a/packages/scaffolder-internal/package.json b/packages/scaffolder-internal/package.json index 6bfae424d7..abbd487103 100644 --- a/packages/scaffolder-internal/package.json +++ b/packages/scaffolder-internal/package.json @@ -1,6 +1,6 @@ { "name": "@internal/scaffolder", - "version": "0.0.13-next.1", + "version": "0.0.13", "backstage": { "role": "web-library", "inline": true diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 7bc24fb346..76a8f81a63 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,20 @@ # techdocs-cli-embedded-app +## 0.2.112 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.34.2 + - @backstage/plugin-techdocs@1.15.0 + - @backstage/app-defaults@1.7.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-components@0.18.0 + - @backstage/core-app-api@1.19.0 + - @backstage/integration-react@1.2.10 + ## 0.2.112-next.1 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/knip-report.md b/packages/techdocs-cli-embedded-app/knip-report.md index 96aa361a87..3711ac996d 100644 --- a/packages/techdocs-cli-embedded-app/knip-report.md +++ b/packages/techdocs-cli-embedded-app/knip-report.md @@ -4,14 +4,14 @@ | Name | Location | Severity | | :-------- | :----------- | :------- | -| react-use | package.json | error | -| history | package.json | error | +| react-use | packages/techdocs-cli-embedded-app/package.json | error | +| history | packages/techdocs-cli-embedded-app/package.json | error | ## Unused devDependencies (3) | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @testing-library/user-event | package.json | error | -| @testing-library/dom | package.json | error | -| cross-env | package.json | error | +| @testing-library/user-event | packages/techdocs-cli-embedded-app/package.json | error | +| @testing-library/dom | packages/techdocs-cli-embedded-app/package.json | error | +| cross-env | packages/techdocs-cli-embedded-app/package.json | error | diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 132cf5a987..09e974ed88 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.112-next.1", + "version": "0.2.112", "backstage": { "role": "frontend" }, diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index c888ff6b4f..1fb32bec51 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,14 @@ # @techdocs/cli +## 1.9.8 + +### Patch Changes + +- db63208: Fixed an issue where `@techdocs/cli serve` command did not pick up the latest changes to TechDocs. +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-techdocs-node@1.13.7 + ## 1.9.8-next.0 ### Patch Changes diff --git a/packages/techdocs-cli/knip-report.md b/packages/techdocs-cli/knip-report.md index 5948844a00..f13d2d7ab6 100644 --- a/packages/techdocs-cli/knip-report.md +++ b/packages/techdocs-cli/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :----------- | :----------- | :------- | -| global-agent | package.json | error | +| global-agent | packages/techdocs-cli/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| techdocs-cli-embedded-app | package.json | error | -| @types/webpack-env | package.json | error | +| techdocs-cli-embedded-app | packages/techdocs-cli/package.json | error | +| @types/webpack-env | packages/techdocs-cli/package.json | error | diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 8ae30b9889..7ce62cd803 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,6 +1,6 @@ { "name": "@techdocs/cli", - "version": "1.9.8-next.0", + "version": "1.9.8", "description": "Utility CLI for managing TechDocs sites in Backstage.", "backstage": { "role": "cli" diff --git a/packages/theme/knip-report.md b/packages/theme/knip-report.md index 9aa0ae1d46..270d625e91 100644 --- a/packages/theme/knip-report.md +++ b/packages/theme/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :--------------- | :----------- | :------- | -| react-router-dom | package.json | error | +| react-router-dom | packages/theme/package.json | error | diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index c6adabdae1..abfaacfd45 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/types +## 1.2.2 + +### Patch Changes + +- a95cebd: Internal refactoring for better type support + ## 1.2.1 ### Patch Changes diff --git a/packages/types/package.json b/packages/types/package.json index 55c3a36f4a..f7dcb5d11f 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/types", - "version": "1.2.1", + "version": "1.2.2", "description": "Common TypeScript types used within Backstage", "backstage": { "role": "common-library" diff --git a/packages/types/src/observable.ts b/packages/types/src/observable.ts index f7292c293c..667875ccd1 100644 --- a/packages/types/src/observable.ts +++ b/packages/types/src/observable.ts @@ -46,7 +46,7 @@ export type Subscription = { // We get the actual runtime polyfill from zen-observable declare global { interface SymbolConstructor { - readonly observable: symbol; + readonly observable: unique symbol; } } diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index a88c4593e1..0fcd9d2ff6 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/ui +## 0.7.1 + +### Patch Changes + +- 7307930: Add missing class for flex: baseline +- 89da341: Fix Select component to properly attach aria-label and aria-labelledby props to the rendered element for improved accessibility. +- 0ffa4c7: Removed the need to mock `window.matchMedia` in tests, falling back to default breakpoint values instead. + +## 0.7.1-next.0 + +### Patch Changes + +- 7307930: Add missing class for flex: baseline +- 89da341: Fix Select component to properly attach aria-label and aria-labelledby props to the rendered element for improved accessibility. + ## 0.7.0 ### Minor Changes diff --git a/packages/ui/knip-report.md b/packages/ui/knip-report.md index c242c05e3f..3f3284a1a0 100644 --- a/packages/ui/knip-report.md +++ b/packages/ui/knip-report.md @@ -1,20 +1,22 @@ # Knip report -## Unused devDependencies (5) +## Unused dependencies (1) -| Name | Location | Severity | -| :------------------------------- | :----------- | :------- | -| @storybook/addon-styling-webpack | package.json | error | -| mini-css-extract-plugin | package.json | error | -| @storybook/blocks | package.json | error | -| globals | package.json | error | -| glob | package.json | error | +| Name | Location | Severity | +| :-------------------- | :----------- | :------- | +| @tanstack/react-table | packages/ui/package.json | error | -## Unlisted dependencies (3) +## Unused devDependencies (2) -| Name | Location | Severity | -| :--------------------- | :----------------------------------------- | :------- | -| @react-types/overlays | src/components/Tooltip/Tooltip.stories.tsx | error | -| react-aria | src/components/Menu/Combobox.tsx | error | -| @storybook/preview-api | .storybook/preview.tsx | error | +| Name | Location | Severity | +| :------ | :----------- | :------- | +| globals | packages/ui/package.json | error | +| glob | packages/ui/package.json | error | + +## Unlisted dependencies (2) + +| Name | Location | Severity | +| :-------------------- | :------------------------------------------- | :------- | +| react-stately | packages/ui/src/components/TagGroup/TagGroup.stories.tsx | error | +| @react-types/overlays | packages/ui/src/components/Tooltip/Tooltip.stories.tsx | error | diff --git a/packages/ui/package.json b/packages/ui/package.json index 2ac399fcee..c0a0e7c143 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/ui", - "version": "0.7.0", + "version": "0.7.1", "backstage": { "role": "web-library" }, @@ -40,18 +40,16 @@ "dependencies": { "@base-ui-components/react": "1.0.0-alpha.7", "@remixicon/react": "^4.6.0", - "@storybook/test": "^8.6.12", "@tanstack/react-table": "^8.21.3", "clsx": "^2.1.1", "react-aria-components": "^1.10.1" }, "devDependencies": { "@backstage/cli": "workspace:^", - "@storybook/react": "^8.6.12", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "chalk": "^5.4.1", - "eslint-plugin-storybook": "^0.12.0", + "eslint-plugin-storybook": "^9.1.5", "glob": "^11.0.1", "globals": "^15.11.0", "lightningcss": "^1.29.1", @@ -59,7 +57,7 @@ "react": "^18.0.2", "react-dom": "^18.0.2", "react-router-dom": "^6.3.0", - "storybook": "^8.6.12" + "storybook": "^9.1.5" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0", diff --git a/packages/ui/src/components/Avatar/Avatar.stories.tsx b/packages/ui/src/components/Avatar/Avatar.stories.tsx index 69dcd5d369..c91fe736db 100644 --- a/packages/ui/src/components/Avatar/Avatar.stories.tsx +++ b/packages/ui/src/components/Avatar/Avatar.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Avatar } from './index'; import { Flex } from '../..'; diff --git a/packages/ui/src/components/Box/Box.stories.tsx b/packages/ui/src/components/Box/Box.stories.tsx index 6c31f63f96..f0086c3d25 100644 --- a/packages/ui/src/components/Box/Box.stories.tsx +++ b/packages/ui/src/components/Box/Box.stories.tsx @@ -15,7 +15,7 @@ */ import { ReactNode } from 'react'; -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Box } from './Box'; import { Flex } from '../Flex'; diff --git a/packages/ui/src/components/Button/Button.stories.tsx b/packages/ui/src/components/Button/Button.stories.tsx index 962db2e898..db8e999f98 100644 --- a/packages/ui/src/components/Button/Button.stories.tsx +++ b/packages/ui/src/components/Button/Button.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Button } from './Button'; import { Flex } from '../Flex'; import { Text } from '../Text'; diff --git a/packages/ui/src/components/ButtonIcon/ButtonIcon.stories.tsx b/packages/ui/src/components/ButtonIcon/ButtonIcon.stories.tsx index 10e57fbb45..4dcc858084 100644 --- a/packages/ui/src/components/ButtonIcon/ButtonIcon.stories.tsx +++ b/packages/ui/src/components/ButtonIcon/ButtonIcon.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { ButtonIcon } from './ButtonIcon'; import { Flex } from '../Flex'; import { Text } from '../Text'; diff --git a/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx b/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx index 0ded9ef741..b4667f017b 100644 --- a/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx +++ b/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { ButtonLink } from './ButtonLink'; import { Flex } from '../Flex'; import { Text } from '../Text'; diff --git a/packages/ui/src/components/Card/Card.stories.tsx b/packages/ui/src/components/Card/Card.stories.tsx index 2b8466f145..382de8fb8a 100644 --- a/packages/ui/src/components/Card/Card.stories.tsx +++ b/packages/ui/src/components/Card/Card.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Card, CardHeader, CardBody, CardFooter } from './Card'; import { IconNames, Text } from '../..'; diff --git a/packages/ui/src/components/Checkbox/Checkbox.stories.tsx b/packages/ui/src/components/Checkbox/Checkbox.stories.tsx index a8aa46fc07..080cae40b6 100644 --- a/packages/ui/src/components/Checkbox/Checkbox.stories.tsx +++ b/packages/ui/src/components/Checkbox/Checkbox.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Checkbox } from './Checkbox'; import { Flex } from '../Flex'; import { Text } from '../Text'; diff --git a/packages/ui/src/components/Collapsible/Collapsible.stories.tsx b/packages/ui/src/components/Collapsible/Collapsible.stories.tsx index 6f2c761519..7271e6f582 100644 --- a/packages/ui/src/components/Collapsible/Collapsible.stories.tsx +++ b/packages/ui/src/components/Collapsible/Collapsible.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Collapsible } from './Collapsible'; import { Button } from '../Button'; import { Box } from '../Box'; diff --git a/packages/ui/src/components/Container/Container.stories.tsx b/packages/ui/src/components/Container/Container.stories.tsx index 7dfbd9ea6e..504cb3de87 100644 --- a/packages/ui/src/components/Container/Container.stories.tsx +++ b/packages/ui/src/components/Container/Container.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Box } from '../Box/Box'; import { Container } from './Container'; diff --git a/packages/ui/src/components/FieldError/FieldError.stories.tsx b/packages/ui/src/components/FieldError/FieldError.stories.tsx index 2fad9708d8..1922e12a0d 100644 --- a/packages/ui/src/components/FieldError/FieldError.stories.tsx +++ b/packages/ui/src/components/FieldError/FieldError.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { TextField, Input, Form } from 'react-aria-components'; import { FieldError } from './FieldError'; diff --git a/packages/ui/src/components/FieldLabel/FieldLabel.stories.tsx b/packages/ui/src/components/FieldLabel/FieldLabel.stories.tsx index 59a0200711..d0e8f3e4fb 100644 --- a/packages/ui/src/components/FieldLabel/FieldLabel.stories.tsx +++ b/packages/ui/src/components/FieldLabel/FieldLabel.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { FieldLabel } from './FieldLabel'; const meta = { diff --git a/packages/ui/src/components/Flex/Flex.stories.tsx b/packages/ui/src/components/Flex/Flex.stories.tsx index e557d4700b..40f9339ad6 100644 --- a/packages/ui/src/components/Flex/Flex.stories.tsx +++ b/packages/ui/src/components/Flex/Flex.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Flex } from './Flex'; import { Text } from '../Text'; diff --git a/packages/ui/src/components/Grid/Grid.stories.tsx b/packages/ui/src/components/Grid/Grid.stories.tsx index 26cce0387b..66ea346b4f 100644 --- a/packages/ui/src/components/Grid/Grid.stories.tsx +++ b/packages/ui/src/components/Grid/Grid.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Grid } from './Grid'; import type { GridItemProps } from './types'; import { Box } from '../Box/Box'; @@ -23,7 +23,7 @@ import { Flex } from '../Flex'; const meta = { title: 'Backstage UI/Grid', component: Grid.Root, -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; diff --git a/packages/ui/src/components/Header/Header.stories.tsx b/packages/ui/src/components/Header/Header.stories.tsx index 9386ea5001..7ae834f82e 100644 --- a/packages/ui/src/components/Header/Header.stories.tsx +++ b/packages/ui/src/components/Header/Header.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj, StoryFn } from '@storybook/react'; +import type { Meta, StoryObj, StoryFn } from '@storybook/react-vite'; import { Header } from './Header'; import type { HeaderTab } from './types'; import { diff --git a/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx b/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx index f819ec369d..c4849ddc20 100644 --- a/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx +++ b/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj, StoryFn } from '@storybook/react'; +import type { Meta, StoryObj, StoryFn } from '@storybook/react-vite'; import { HeaderPage } from './HeaderPage'; import type { HeaderTab } from '../Header/types'; import { MemoryRouter } from 'react-router-dom'; diff --git a/packages/ui/src/components/Icon/Icon.stories.tsx b/packages/ui/src/components/Icon/Icon.stories.tsx index b4eb01f2ff..83dc62c173 100644 --- a/packages/ui/src/components/Icon/Icon.stories.tsx +++ b/packages/ui/src/components/Icon/Icon.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Meta, StoryObj } from '@storybook/react'; +import { Meta, StoryObj } from '@storybook/react-vite'; import { Icon } from './Icon'; import { IconProvider } from './provider'; import { icons } from './icons'; diff --git a/packages/ui/src/components/Link/Link.stories.tsx b/packages/ui/src/components/Link/Link.stories.tsx index 027869d660..37f142c4dc 100644 --- a/packages/ui/src/components/Link/Link.stories.tsx +++ b/packages/ui/src/components/Link/Link.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryFn, StoryObj } from '@storybook/react'; +import type { Meta, StoryFn, StoryObj } from '@storybook/react-vite'; import { Link } from './Link'; import { Flex } from '../Flex'; import { Text } from '../Text'; diff --git a/packages/ui/src/components/Menu/Menu.stories.tsx b/packages/ui/src/components/Menu/Menu.stories.tsx index 585eabe737..272d2c5342 100644 --- a/packages/ui/src/components/Menu/Menu.stories.tsx +++ b/packages/ui/src/components/Menu/Menu.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { MenuTrigger, SubmenuTrigger, diff --git a/packages/ui/src/components/RadioGroup/RadioGroup.stories.tsx b/packages/ui/src/components/RadioGroup/RadioGroup.stories.tsx index b9c4c26f08..df7a826c50 100644 --- a/packages/ui/src/components/RadioGroup/RadioGroup.stories.tsx +++ b/packages/ui/src/components/RadioGroup/RadioGroup.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { RadioGroup, Radio } from './RadioGroup'; const meta = { diff --git a/packages/ui/src/components/ScrollArea/ScrollArea.stories.tsx b/packages/ui/src/components/ScrollArea/ScrollArea.stories.tsx index 95a7fc3329..48a329eea4 100644 --- a/packages/ui/src/components/ScrollArea/ScrollArea.stories.tsx +++ b/packages/ui/src/components/ScrollArea/ScrollArea.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { ScrollArea } from './ScrollArea'; import { Text } from '../Text/Text'; diff --git a/packages/ui/src/components/SearchField/SearchField.stories.tsx b/packages/ui/src/components/SearchField/SearchField.stories.tsx index c8867215a1..14f265b243 100644 --- a/packages/ui/src/components/SearchField/SearchField.stories.tsx +++ b/packages/ui/src/components/SearchField/SearchField.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { SearchField } from './SearchField'; import { Form } from 'react-aria-components'; import { Icon } from '../Icon'; diff --git a/packages/ui/src/components/Select/Select.stories.tsx b/packages/ui/src/components/Select/Select.stories.tsx index 5c3ed242dc..6e96e37f08 100644 --- a/packages/ui/src/components/Select/Select.stories.tsx +++ b/packages/ui/src/components/Select/Select.stories.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Select } from './Select'; import { Flex } from '../Flex'; import { Form } from 'react-aria-components'; diff --git a/packages/ui/src/components/Skeleton/Skeleton.stories.tsx b/packages/ui/src/components/Skeleton/Skeleton.stories.tsx index 95f3e57547..bbe25942f0 100644 --- a/packages/ui/src/components/Skeleton/Skeleton.stories.tsx +++ b/packages/ui/src/components/Skeleton/Skeleton.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Skeleton } from './Skeleton'; import { Flex } from '../Flex'; diff --git a/packages/ui/src/components/Switch/Switch.stories.tsx b/packages/ui/src/components/Switch/Switch.stories.tsx index a8d12bccca..6d841bf772 100644 --- a/packages/ui/src/components/Switch/Switch.stories.tsx +++ b/packages/ui/src/components/Switch/Switch.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Switch } from './Switch'; const meta = { diff --git a/packages/ui/src/components/Table/Table.stories.tsx b/packages/ui/src/components/Table/Table.stories.tsx index 903caba25d..564357bfde 100644 --- a/packages/ui/src/components/Table/Table.stories.tsx +++ b/packages/ui/src/components/Table/Table.stories.tsx @@ -15,7 +15,7 @@ */ import { useState } from 'react'; -import type { Meta, StoryFn, StoryObj } from '@storybook/react'; +import type { Meta, StoryFn, StoryObj } from '@storybook/react-vite'; import { Table, TableHeader, diff --git a/packages/ui/src/components/TablePagination/TablePagination.stories.tsx b/packages/ui/src/components/TablePagination/TablePagination.stories.tsx index 2782837428..4550d00abb 100644 --- a/packages/ui/src/components/TablePagination/TablePagination.stories.tsx +++ b/packages/ui/src/components/TablePagination/TablePagination.stories.tsx @@ -16,7 +16,7 @@ // TODO: Bring useArgs() back when we update Storybook to 9 // import { useArgs } from 'storybook/preview-api'; -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { TablePagination } from './TablePagination'; const meta = { diff --git a/packages/ui/src/components/Tabs/Tabs.stories.tsx b/packages/ui/src/components/Tabs/Tabs.stories.tsx index 8ff08b8c5a..cbfe3d8562 100644 --- a/packages/ui/src/components/Tabs/Tabs.stories.tsx +++ b/packages/ui/src/components/Tabs/Tabs.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryFn, StoryObj } from '@storybook/react'; +import type { Meta, StoryFn, StoryObj } from '@storybook/react-vite'; import { Tabs, TabList, Tab, TabPanel } from './Tabs'; import { MemoryRouter } from 'react-router-dom'; import { Box } from '../Box'; diff --git a/packages/ui/src/components/TagGroup/TagGroup.stories.tsx b/packages/ui/src/components/TagGroup/TagGroup.stories.tsx index 03b5de23e9..409956e049 100644 --- a/packages/ui/src/components/TagGroup/TagGroup.stories.tsx +++ b/packages/ui/src/components/TagGroup/TagGroup.stories.tsx @@ -15,7 +15,7 @@ */ import { useState } from 'react'; -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { TagGroup, Tag } from '.'; import type { Selection } from 'react-aria-components'; import { Flex, Icon, IconNames } from '../../'; diff --git a/packages/ui/src/components/Text/Text.stories.tsx b/packages/ui/src/components/Text/Text.stories.tsx index 8329ed70dc..7f95f4d5a1 100644 --- a/packages/ui/src/components/Text/Text.stories.tsx +++ b/packages/ui/src/components/Text/Text.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Text } from './Text'; import { Flex } from '../Flex'; diff --git a/packages/ui/src/components/TextField/TextField.stories.tsx b/packages/ui/src/components/TextField/TextField.stories.tsx index 57cc2b2909..86da34d147 100644 --- a/packages/ui/src/components/TextField/TextField.stories.tsx +++ b/packages/ui/src/components/TextField/TextField.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { TextField } from './TextField'; import { Form } from 'react-aria-components'; import { Icon } from '../Icon'; @@ -113,6 +113,7 @@ export const DisabledWithIcon: Story = { ...WithIcon.args, isDisabled: true, }, + render: WithIcon.render, }; export const ShowError: Story = { diff --git a/packages/ui/src/components/Tooltip/Tooltip.stories.tsx b/packages/ui/src/components/Tooltip/Tooltip.stories.tsx index ff2e19f2a2..cc6b513ed4 100644 --- a/packages/ui/src/components/Tooltip/Tooltip.stories.tsx +++ b/packages/ui/src/components/Tooltip/Tooltip.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Placement } from '@react-types/overlays'; import { TooltipTrigger, Tooltip } from './Tooltip'; import { Button } from '../Button/Button'; @@ -41,7 +41,7 @@ const meta = { control: { type: 'number' }, }, }, - render: ({ tooltip, isOpen, isDisabled, placement, delay, closeDelay }) => ( + render: ({ children, isOpen, isDisabled, placement, delay, closeDelay }) => ( - {tooltip ?? 'I am a tooltip'} + {children ?? 'I am a tooltip'} ), } as Meta<{ - tooltip?: string; + children?: string; isOpen?: boolean; isDisabled?: boolean; placement?: Placement; @@ -66,7 +66,7 @@ type Story = StoryObj; export const Default: Story = { args: { - tooltip: 'I am a tooltip', + children: 'I am a tooltip', }, }; @@ -102,14 +102,14 @@ export const OrthogonalPlacements: Story = { ...Default.args, isOpen: true, }, - render: ({ isOpen, tooltip }) => { + render: ({ isOpen, children }) => { return ( - {tooltip} - {tooltip} - {tooltip} - {tooltip} + {children} + {children} + {children} + {children} ); }, @@ -119,7 +119,7 @@ export const WithLongText: Story = { args: { ...Default.args, isOpen: true, - tooltip: + children: 'I am a tooltip with a very long text. orem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', }, }; diff --git a/packages/ui/src/hooks/useMediaQuery.ts b/packages/ui/src/hooks/useMediaQuery.ts index 9e2cc9ef0b..659ea4a418 100644 --- a/packages/ui/src/hooks/useMediaQuery.ts +++ b/packages/ui/src/hooks/useMediaQuery.ts @@ -22,7 +22,8 @@ type UseMediaQueryOptions = { initializeWithValue?: boolean; }; -const IS_SERVER = typeof window === 'undefined'; +const IS_SERVER = + typeof window === 'undefined' || typeof window.matchMedia === 'undefined'; export function useMediaQuery( query: string, @@ -51,6 +52,9 @@ export function useMediaQuery( } useIsomorphicLayoutEffect(() => { + if (IS_SERVER) { + return; + } const matchMedia = window.matchMedia(query); // Triggered at the first client-side load and if query changes diff --git a/packages/version-bridge/knip-report.md b/packages/version-bridge/knip-report.md index 9aa0ae1d46..2bdccfc4d2 100644 --- a/packages/version-bridge/knip-report.md +++ b/packages/version-bridge/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :--------------- | :----------- | :------- | -| react-router-dom | package.json | error | +| react-router-dom | packages/version-bridge/package.json | error | diff --git a/packages/yarn-plugin/package.json b/packages/yarn-plugin/package.json index c534c50c76..4bd43fce40 100644 --- a/packages/yarn-plugin/package.json +++ b/packages/yarn-plugin/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "@backstage/cli-common": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/release-manifests": "workspace:^", "@yarnpkg/core": "^4.4.1", "@yarnpkg/fslib": "^3.1.2", diff --git a/packages/yarn-plugin/src/util/getCurrentBackstageVersion.test.ts b/packages/yarn-plugin/src/util/getCurrentBackstageVersion.test.ts index 4b1f008fce..a0ce45c18f 100644 --- a/packages/yarn-plugin/src/util/getCurrentBackstageVersion.test.ts +++ b/packages/yarn-plugin/src/util/getCurrentBackstageVersion.test.ts @@ -73,17 +73,15 @@ describe('getCurrentBackstageVersion', () => { }); it.each` - description | content - ${'is missing'} | ${{}} - ${'is invalid'} | ${{ 'backstage.json': '}{' }} - ${'has missing version'} | ${{ 'backstage.json': '{"a":"b"}' }} - ${'has invalid version'} | ${{ 'backstage.json': '{"version":"foobar"}' }} - `('throws if backstage.json $description', ({ content }) => { + description | content | message + ${'is missing'} | ${{}} | ${/valid version string not found.*no such file/i} + ${'is invalid'} | ${{ 'backstage.json': '}{' }} | ${/valid version string not found.*not valid json/i} + ${'has missing version'} | ${{ 'backstage.json': '{"a":"b"}' }} | ${/valid version string not found.*version field is missing/i} + ${'has invalid version'} | ${{ 'backstage.json': '{"version":"foobar"}' }} | ${/valid version string not found.*exists but is not valid semver/i} + `('throws if backstage.json $description', ({ content, message }) => { mockDir.addContent(content); - expect(() => getCurrentBackstageVersion()).toThrow( - /valid version string not found/i, - ); + expect(() => getCurrentBackstageVersion()).toThrow(message); }); it('caches repeated calls', () => { diff --git a/packages/yarn-plugin/src/util/getCurrentBackstageVersion.ts b/packages/yarn-plugin/src/util/getCurrentBackstageVersion.ts index 5280854fb3..be80d29376 100644 --- a/packages/yarn-plugin/src/util/getCurrentBackstageVersion.ts +++ b/packages/yarn-plugin/src/util/getCurrentBackstageVersion.ts @@ -18,6 +18,7 @@ import assert from 'assert'; import { valid as semverValid } from 'semver'; import { ppath, xfs } from '@yarnpkg/fslib'; import { BACKSTAGE_JSON } from '@backstage/cli-common'; +import { ForwardedError } from '@backstage/errors'; import { memoize } from './memoize'; import { getWorkspaceRoot } from './getWorkspaceRoot'; @@ -26,11 +27,16 @@ export const getCurrentBackstageVersion = memoize(() => { let backstageVersion: string | null = null; try { - backstageVersion = semverValid(xfs.readJsonSync(backstageJsonPath).version); + const backstageVersionRaw = xfs.readJsonSync(backstageJsonPath).version; + assert(backstageVersionRaw !== undefined, 'Version field is missing'); + backstageVersion = semverValid(backstageVersionRaw); - assert(backstageVersion !== null); - } catch { - throw new Error('Valid version string not found in backstage.json'); + assert(backstageVersion !== null, 'Version exists but is not valid semver'); + } catch (err) { + throw new ForwardedError( + 'Valid version string not found in backstage.json', + err, + ); } return backstageVersion; diff --git a/plugins/api-docs-module-protoc-gen-doc/knip-report.md b/plugins/api-docs-module-protoc-gen-doc/knip-report.md index 63b63f2f9b..5933dc809c 100644 --- a/plugins/api-docs-module-protoc-gen-doc/knip-report.md +++ b/plugins/api-docs-module-protoc-gen-doc/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :--------------- | :----------- | :------- | -| react-router-dom | package.json | error | -| react-dom | package.json | error | +| react-router-dom | plugins/api-docs-module-protoc-gen-doc/package.json | error | +| react-dom | plugins/api-docs-module-protoc-gen-doc/package.json | error | diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 25671d8050..4a40848ced 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-api-docs +## 0.12.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + +## 0.12.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.12.11-next.1 ### Patch Changes diff --git a/plugins/api-docs/knip-report.md b/plugins/api-docs/knip-report.md index 3c06f53535..406a44f365 100644 --- a/plugins/api-docs/knip-report.md +++ b/plugins/api-docs/knip-report.md @@ -4,14 +4,14 @@ | Name | Location | Severity | | :------------------- | :----------- | :------- | -| isomorphic-form-data | package.json | error | -| graphql-config | package.json | error | -| graphql-ws | package.json | error | +| isomorphic-form-data | plugins/api-docs/package.json | error | +| graphql-config | plugins/api-docs/package.json | error | +| graphql-ws | plugins/api-docs/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :---------------------- | :----------- | :------- | -| @backstage/core-app-api | package.json | error | -| @types/highlightjs | package.json | error | +| @backstage/core-app-api | plugins/api-docs/package.json | error | +| @types/highlightjs | plugins/api-docs/package.json | error | diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 4fcae79fba..febec4ff08 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.12.11-next.1", + "version": "0.12.11", "description": "A Backstage plugin that helps represent API entities in the frontend", "backstage": { "role": "frontend-plugin", diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index bd097b0005..02039c933e 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-app-backend +## 0.5.6 + +### Patch Changes + +- afd368e: Internal update to not expose the old `createRouter`. +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-app-node@0.1.37 + +## 0.5.6-next.1 + +### Patch Changes + +- afd368e: Internal update to not expose the old `createRouter`. +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-app-node@0.1.37-next.1 + ## 0.5.6-next.0 ### Patch Changes diff --git a/plugins/app-backend/knip-report.md b/plugins/app-backend/knip-report.md index 588eca0213..55ec2572b5 100644 --- a/plugins/app-backend/knip-report.md +++ b/plugins/app-backend/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/plugin-auth-node | package.json | error | -| yn | package.json | error | +| @backstage/plugin-auth-node | plugins/app-backend/package.json | error | +| yn | plugins/app-backend/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :------------------------- | :----------- | :------- | -| @backstage/backend-app-api | package.json | error | +| @backstage/backend-app-api | plugins/app-backend/package.json | error | diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index b81e709926..609cf1c543 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.5.6-next.0", + "version": "0.5.6", "description": "A Backstage backend plugin that serves the Backstage frontend app", "backstage": { "role": "backend-plugin", diff --git a/plugins/app-backend/src/index.ts b/plugins/app-backend/src/index.ts index 9f83f6e30a..3f8a0002a7 100644 --- a/plugins/app-backend/src/index.ts +++ b/plugins/app-backend/src/index.ts @@ -21,4 +21,3 @@ */ export { appPlugin as default } from './service/appPlugin'; -export * from './service/router'; diff --git a/plugins/app-node/CHANGELOG.md b/plugins/app-node/CHANGELOG.md index 2063c2db48..ea9532aca5 100644 --- a/plugins/app-node/CHANGELOG.md +++ b/plugins/app-node/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-app-node +## 0.1.37 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/backend-plugin-api@1.4.3 + +## 0.1.37-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + ## 0.1.37-next.0 ### Patch Changes diff --git a/plugins/app-node/package.json b/plugins/app-node/package.json index 07340379e9..f11a84bc55 100644 --- a/plugins/app-node/package.json +++ b/plugins/app-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-node", - "version": "0.1.37-next.0", + "version": "0.1.37", "description": "Node.js library for the app plugin", "backstage": { "role": "node-library", diff --git a/plugins/app-visualizer/CHANGELOG.md b/plugins/app-visualizer/CHANGELOG.md index 1270d1dab5..bc5ac063cc 100644 --- a/plugins/app-visualizer/CHANGELOG.md +++ b/plugins/app-visualizer/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-app-visualizer +## 0.1.23 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + ## 0.1.23-next.0 ### Patch Changes diff --git a/plugins/app-visualizer/package.json b/plugins/app-visualizer/package.json index f5260dec0c..4affb81b50 100644 --- a/plugins/app-visualizer/package.json +++ b/plugins/app-visualizer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-visualizer", - "version": "0.1.23-next.0", + "version": "0.1.23", "description": "Visualizes the Backstage app structure", "backstage": { "role": "frontend-plugin", diff --git a/plugins/app/CHANGELOG.md b/plugins/app/CHANGELOG.md index 56ea8057aa..c1026add86 100644 --- a/plugins/app/CHANGELOG.md +++ b/plugins/app/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-app +## 0.3.0 + +### Minor Changes + +- 99790db: Add implementation of OpenShift authentication provider. + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/integration-react@1.2.10 + ## 0.2.1-next.0 ### Patch Changes diff --git a/plugins/app/knip-report.md b/plugins/app/knip-report.md index 9361e03abc..7b3dcb2e34 100644 --- a/plugins/app/knip-report.md +++ b/plugins/app/knip-report.md @@ -1,16 +1,18 @@ # Knip report -## Unused dependencies (2) +## Unused dependencies (4) -| Name | Location | Severity | -| :--------------- | :----------- | :------- | -| @material-ui/lab | package.json | error | -| react-use | package.json | error | +| Name | Location | Severity | +| :------------------------ | :----------- | :------- | +| @backstage/version-bridge | plugins/app/package.json | error | +| @material-ui/lab | plugins/app/package.json | error | +| react-use | plugins/app/package.json | error | +| zod | plugins/app/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :------------------- | :----------- | :------- | -| @backstage/dev-utils | package.json | error | -| msw | package.json | error | +| @backstage/dev-utils | plugins/app/package.json | error | +| msw | plugins/app/package.json | error | diff --git a/plugins/app/package.json b/plugins/app/package.json index 229837c340..d17d903b0a 100644 --- a/plugins/app/package.json +++ b/plugins/app/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app", - "version": "0.2.1-next.0", + "version": "0.3.0", "backstage": { "role": "frontend-plugin", "pluginId": "app", diff --git a/plugins/app/report.api.md b/plugins/app/report.api.md index 24885bed4b..d12ada15b4 100644 --- a/plugins/app/report.api.md +++ b/plugins/app/report.api.md @@ -539,6 +539,21 @@ const appPlugin: OverridableFrontendPlugin< params: ApiFactory, ) => ExtensionBlueprintParams; }>; + 'api:app/openshift-auth': ExtensionDefinition<{ + kind: 'api'; + name: 'openshift-auth'; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: < + TApi, + TImpl extends TApi, + TDeps extends { [name in string]: unknown }, + >( + params: ApiFactory, + ) => ExtensionBlueprintParams; + }>; 'api:app/permission': ExtensionDefinition<{ kind: 'api'; name: 'permission'; diff --git a/plugins/app/src/defaultApis.ts b/plugins/app/src/defaultApis.ts index 0337f6c4f4..0b4eb5c825 100644 --- a/plugins/app/src/defaultApis.ts +++ b/plugins/app/src/defaultApis.ts @@ -35,6 +35,7 @@ import { createFetchApi, FetchMiddlewares, VMwareCloudAuth, + OpenShiftAuth, } from '../../../packages/core-app-api/src/apis/implementations'; import { @@ -56,6 +57,7 @@ import { bitbucketServerAuthApiRef, atlassianAuthApiRef, vmwareCloudAuthApiRef, + openshiftAuthApiRef, } from '@backstage/core-plugin-api'; import { ApiBlueprint, dialogApiRef } from '@backstage/frontend-plugin-api'; import { @@ -353,6 +355,26 @@ export const apis = [ }, }), }), + ApiBlueprint.make({ + name: 'openshift-auth', + params: defineParams => + defineParams({ + api: openshiftAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => { + return OpenShiftAuth.create({ + configApi, + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }); + }, + }), + }), ApiBlueprint.make({ name: 'permission', params: defineParams => diff --git a/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md b/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md index 98f6213d34..16d1637cd6 100644 --- a/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-atlassian-provider +## 0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-atlassian-provider/knip-report.md b/plugins/auth-backend-module-atlassian-provider/knip-report.md index 6624c86073..4604ddb386 100644 --- a/plugins/auth-backend-module-atlassian-provider/knip-report.md +++ b/plugins/auth-backend-module-atlassian-provider/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| passport | package.json | error | -| express | package.json | error | +| passport | plugins/auth-backend-module-atlassian-provider/package.json | error | +| express | plugins/auth-backend-module-atlassian-provider/package.json | error | diff --git a/plugins/auth-backend-module-atlassian-provider/package.json b/plugins/auth-backend-module-atlassian-provider/package.json index 9600af59ea..4fd74b74ca 100644 --- a/plugins/auth-backend-module-atlassian-provider/package.json +++ b/plugins/auth-backend-module-atlassian-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-atlassian-provider", - "version": "0.4.7-next.0", + "version": "0.4.7", "description": "The atlassian-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-auth0-provider/CHANGELOG.md b/plugins/auth-backend-module-auth0-provider/CHANGELOG.md index ce929a85e7..725b88735c 100644 --- a/plugins/auth-backend-module-auth0-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-auth0-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-auth0-provider +## 0.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-auth0-provider/package.json b/plugins/auth-backend-module-auth0-provider/package.json index 741c6ed8c6..8e06baa4db 100644 --- a/plugins/auth-backend-module-auth0-provider/package.json +++ b/plugins/auth-backend-module-auth0-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-auth0-provider", - "version": "0.2.7-next.0", + "version": "0.2.7", "description": "The auth0-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md b/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md index 9b6c019506..f2026fd2b9 100644 --- a/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-aws-alb-provider +## 0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.25.4 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-aws-alb-provider/knip-report.md b/plugins/auth-backend-module-aws-alb-provider/knip-report.md index 14abcfe083..6f67a12ef6 100644 --- a/plugins/auth-backend-module-aws-alb-provider/knip-report.md +++ b/plugins/auth-backend-module-aws-alb-provider/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :----------------------------- | :----------- | :------- | -| @backstage/plugin-auth-backend | package.json | error | +| @backstage/plugin-auth-backend | plugins/auth-backend-module-aws-alb-provider/package.json | error | diff --git a/plugins/auth-backend-module-aws-alb-provider/package.json b/plugins/auth-backend-module-aws-alb-provider/package.json index 801cd0e62f..d85b993f28 100644 --- a/plugins/auth-backend-module-aws-alb-provider/package.json +++ b/plugins/auth-backend-module-aws-alb-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-aws-alb-provider", - "version": "0.4.7-next.0", + "version": "0.4.7", "description": "The aws-alb provider module for the Backstage auth backend.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md b/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md index cf6931c11f..355d9ea55f 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-azure-easyauth-provider +## 0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.12-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-azure-easyauth-provider/knip-report.md b/plugins/auth-backend-module-azure-easyauth-provider/knip-report.md index d5c786591f..854da1aa31 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/knip-report.md +++ b/plugins/auth-backend-module-azure-easyauth-provider/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :----------------------- | :----------- | :------- | -| @backstage/catalog-model | package.json | error | +| @backstage/catalog-model | plugins/auth-backend-module-azure-easyauth-provider/package.json | error | diff --git a/plugins/auth-backend-module-azure-easyauth-provider/package.json b/plugins/auth-backend-module-azure-easyauth-provider/package.json index 98e665fbbf..534d721107 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/package.json +++ b/plugins/auth-backend-module-azure-easyauth-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-azure-easyauth-provider", - "version": "0.2.12-next.0", + "version": "0.2.12", "description": "The azure-easyauth-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md b/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md index c998f1cb8d..79a379c94c 100644 --- a/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-bitbucket-provider +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-bitbucket-provider/knip-report.md b/plugins/auth-backend-module-bitbucket-provider/knip-report.md index 6624c86073..816b276c84 100644 --- a/plugins/auth-backend-module-bitbucket-provider/knip-report.md +++ b/plugins/auth-backend-module-bitbucket-provider/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| passport | package.json | error | -| express | package.json | error | +| passport | plugins/auth-backend-module-bitbucket-provider/package.json | error | +| express | plugins/auth-backend-module-bitbucket-provider/package.json | error | diff --git a/plugins/auth-backend-module-bitbucket-provider/package.json b/plugins/auth-backend-module-bitbucket-provider/package.json index fe66aee5d5..e7826bcf08 100644 --- a/plugins/auth-backend-module-bitbucket-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-bitbucket-provider", - "version": "0.3.7-next.0", + "version": "0.3.7", "description": "The bitbucket-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-bitbucket-server-provider/CHANGELOG.md b/plugins/auth-backend-module-bitbucket-server-provider/CHANGELOG.md index fb160d4dcd..dad1d86780 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-bitbucket-server-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-bitbucket-server-provider +## 0.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-bitbucket-server-provider/knip-report.md b/plugins/auth-backend-module-bitbucket-server-provider/knip-report.md index 4386ad9c4a..5aaf6b7a92 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/knip-report.md +++ b/plugins/auth-backend-module-bitbucket-server-provider/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| passport | package.json | error | +| passport | plugins/auth-backend-module-bitbucket-server-provider/package.json | error | diff --git a/plugins/auth-backend-module-bitbucket-server-provider/package.json b/plugins/auth-backend-module-bitbucket-server-provider/package.json index c1ed77c3cd..7f830811e7 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-server-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-bitbucket-server-provider", - "version": "0.2.7-next.0", + "version": "0.2.7", "description": "The bitbucket-server-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md b/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md index 6cdb312961..3bd2055825 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-cloudflare-access-provider +## 0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-cloudflare-access-provider/package.json b/plugins/auth-backend-module-cloudflare-access-provider/package.json index ddcbde2425..537526dbdc 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/package.json +++ b/plugins/auth-backend-module-cloudflare-access-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-cloudflare-access-provider", - "version": "0.4.7-next.0", + "version": "0.4.7", "description": "The cloudflare-access-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md index 3e45cb6195..d587f6a999 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-gcp-iap-provider +## 0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-gcp-iap-provider/package.json b/plugins/auth-backend-module-gcp-iap-provider/package.json index 05d3db7892..9bf40aaf4d 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/package.json +++ b/plugins/auth-backend-module-gcp-iap-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-gcp-iap-provider", - "version": "0.4.7-next.0", + "version": "0.4.7", "description": "A GCP IAP auth provider module for the Backstage auth backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-github-provider/CHANGELOG.md b/plugins/auth-backend-module-github-provider/CHANGELOG.md index 149f6a79d0..0ea39f5b6d 100644 --- a/plugins/auth-backend-module-github-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-github-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-github-provider +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-github-provider/package.json b/plugins/auth-backend-module-github-provider/package.json index 8cd8a41111..d5854b1ff2 100644 --- a/plugins/auth-backend-module-github-provider/package.json +++ b/plugins/auth-backend-module-github-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-github-provider", - "version": "0.3.7-next.0", + "version": "0.3.7", "description": "The github-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md index 3eebbc89fb..afd0f2258f 100644 --- a/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-gitlab-provider +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-gitlab-provider/knip-report.md b/plugins/auth-backend-module-gitlab-provider/knip-report.md index 6624c86073..693a54de62 100644 --- a/plugins/auth-backend-module-gitlab-provider/knip-report.md +++ b/plugins/auth-backend-module-gitlab-provider/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| passport | package.json | error | -| express | package.json | error | +| passport | plugins/auth-backend-module-gitlab-provider/package.json | error | +| express | plugins/auth-backend-module-gitlab-provider/package.json | error | diff --git a/plugins/auth-backend-module-gitlab-provider/package.json b/plugins/auth-backend-module-gitlab-provider/package.json index fc55675419..b7cb8c5da3 100644 --- a/plugins/auth-backend-module-gitlab-provider/package.json +++ b/plugins/auth-backend-module-gitlab-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-gitlab-provider", - "version": "0.3.7-next.0", + "version": "0.3.7", "description": "The gitlab-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-google-provider/CHANGELOG.md b/plugins/auth-backend-module-google-provider/CHANGELOG.md index 1c464a3f2f..7c5651d120 100644 --- a/plugins/auth-backend-module-google-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-google-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-google-provider +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json index d9ee2a862a..f947b84b39 100644 --- a/plugins/auth-backend-module-google-provider/package.json +++ b/plugins/auth-backend-module-google-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-google-provider", - "version": "0.3.7-next.0", + "version": "0.3.7", "description": "A Google auth provider module for the Backstage auth backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-guest-provider/CHANGELOG.md b/plugins/auth-backend-module-guest-provider/CHANGELOG.md index b92fdf5c88..d57b0e967e 100644 --- a/plugins/auth-backend-module-guest-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-guest-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-guest-provider +## 0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.12-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-guest-provider/knip-report.md b/plugins/auth-backend-module-guest-provider/knip-report.md index d2b8db6f32..41211cd509 100644 --- a/plugins/auth-backend-module-guest-provider/knip-report.md +++ b/plugins/auth-backend-module-guest-provider/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :-------------- | :----------- | :------- | -| passport-oauth2 | package.json | error | +| passport-oauth2 | plugins/auth-backend-module-guest-provider/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :---------------------------- | :----------- | :------- | -| @backstage/backend-test-utils | package.json | error | -| express | package.json | error | +| @backstage/backend-test-utils | plugins/auth-backend-module-guest-provider/package.json | error | +| express | plugins/auth-backend-module-guest-provider/package.json | error | diff --git a/plugins/auth-backend-module-guest-provider/package.json b/plugins/auth-backend-module-guest-provider/package.json index 900cc6001c..7e6a657797 100644 --- a/plugins/auth-backend-module-guest-provider/package.json +++ b/plugins/auth-backend-module-guest-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-guest-provider", - "version": "0.2.12-next.0", + "version": "0.2.12", "description": "The guest-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md b/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md index 49e5d06c63..3a21bd1bf7 100644 --- a/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-microsoft-provider +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-microsoft-provider/package.json b/plugins/auth-backend-module-microsoft-provider/package.json index 7fe3e19bd4..a5f661b839 100644 --- a/plugins/auth-backend-module-microsoft-provider/package.json +++ b/plugins/auth-backend-module-microsoft-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-microsoft-provider", - "version": "0.3.7-next.0", + "version": "0.3.7", "description": "The microsoft-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md b/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md index 0091dfff3e..0caf910ee2 100644 --- a/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-oauth2-provider +## 0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-oauth2-provider/knip-report.md b/plugins/auth-backend-module-oauth2-provider/knip-report.md index 4386ad9c4a..731dd67f6c 100644 --- a/plugins/auth-backend-module-oauth2-provider/knip-report.md +++ b/plugins/auth-backend-module-oauth2-provider/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| passport | package.json | error | +| passport | plugins/auth-backend-module-oauth2-provider/package.json | error | diff --git a/plugins/auth-backend-module-oauth2-provider/package.json b/plugins/auth-backend-module-oauth2-provider/package.json index c5a3b58b7b..beeba6af8b 100644 --- a/plugins/auth-backend-module-oauth2-provider/package.json +++ b/plugins/auth-backend-module-oauth2-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oauth2-provider", - "version": "0.4.7-next.0", + "version": "0.4.7", "description": "The oauth2-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md b/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md index 77c2890c73..b64d3a2481 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-oauth2-proxy-provider +## 0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.12-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/knip-report.md b/plugins/auth-backend-module-oauth2-proxy-provider/knip-report.md index 14b2b4cc88..e034256a27 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/knip-report.md +++ b/plugins/auth-backend-module-oauth2-proxy-provider/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---------------------------- | :----------- | :------- | -| @backstage/backend-test-utils | package.json | error | +| @backstage/backend-test-utils | plugins/auth-backend-module-oauth2-proxy-provider/package.json | error | diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/package.json b/plugins/auth-backend-module-oauth2-proxy-provider/package.json index 370021bfd4..6edd907d51 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/package.json +++ b/plugins/auth-backend-module-oauth2-proxy-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oauth2-proxy-provider", - "version": "0.2.12-next.0", + "version": "0.2.12", "description": "The oauth2-proxy-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oidc-provider/CHANGELOG.md b/plugins/auth-backend-module-oidc-provider/CHANGELOG.md index 370a089c97..d21b18e15c 100644 --- a/plugins/auth-backend-module-oidc-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oidc-provider/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-auth-backend-module-oidc-provider +## 0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.25.4 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-oidc-provider/knip-report.md b/plugins/auth-backend-module-oidc-provider/knip-report.md index a7256f0ead..aa5b910b2d 100644 --- a/plugins/auth-backend-module-oidc-provider/knip-report.md +++ b/plugins/auth-backend-module-oidc-provider/knip-report.md @@ -4,14 +4,14 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| passport | package.json | error | -| zod | package.json | error | +| passport | plugins/auth-backend-module-oidc-provider/package.json | error | +| zod | plugins/auth-backend-module-oidc-provider/package.json | error | ## Unused devDependencies (3) | Name | Location | Severity | | :--------------------- | :----------- | :------- | -| express-promise-router | package.json | error | -| express-session | package.json | error | -| cookie-parser | package.json | error | +| express-promise-router | plugins/auth-backend-module-oidc-provider/package.json | error | +| express-session | plugins/auth-backend-module-oidc-provider/package.json | error | +| cookie-parser | plugins/auth-backend-module-oidc-provider/package.json | error | diff --git a/plugins/auth-backend-module-oidc-provider/package.json b/plugins/auth-backend-module-oidc-provider/package.json index 9339b5d8ce..d40ded83d7 100644 --- a/plugins/auth-backend-module-oidc-provider/package.json +++ b/plugins/auth-backend-module-oidc-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oidc-provider", - "version": "0.4.7-next.0", + "version": "0.4.7", "description": "The oidc-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-okta-provider/CHANGELOG.md b/plugins/auth-backend-module-okta-provider/CHANGELOG.md index 69f695b3c4..4db3b0639b 100644 --- a/plugins/auth-backend-module-okta-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-okta-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-okta-provider +## 0.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-okta-provider/knip-report.md b/plugins/auth-backend-module-okta-provider/knip-report.md index e761ead581..bec18b0f1a 100644 --- a/plugins/auth-backend-module-okta-provider/knip-report.md +++ b/plugins/auth-backend-module-okta-provider/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| passport | package.json | error | -| express | package.json | error | +| passport | plugins/auth-backend-module-okta-provider/package.json | error | +| express | plugins/auth-backend-module-okta-provider/package.json | error | ## Unlisted dependencies (1) | Name | Location | Severity | | :-------------- | :------------- | :------- | -| passport-oauth2 | src/types.d.ts | error | +| passport-oauth2 | plugins/auth-backend-module-okta-provider/src/types.d.ts | error | diff --git a/plugins/auth-backend-module-okta-provider/package.json b/plugins/auth-backend-module-okta-provider/package.json index 026daaa886..a1f8977142 100644 --- a/plugins/auth-backend-module-okta-provider/package.json +++ b/plugins/auth-backend-module-okta-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-okta-provider", - "version": "0.2.7-next.0", + "version": "0.2.7", "description": "The okta-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md b/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md index a93e56280d..95b2e88aec 100644 --- a/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-onelogin-provider +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-onelogin-provider/knip-report.md b/plugins/auth-backend-module-onelogin-provider/knip-report.md index 6624c86073..8b95b0648f 100644 --- a/plugins/auth-backend-module-onelogin-provider/knip-report.md +++ b/plugins/auth-backend-module-onelogin-provider/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| passport | package.json | error | -| express | package.json | error | +| passport | plugins/auth-backend-module-onelogin-provider/package.json | error | +| express | plugins/auth-backend-module-onelogin-provider/package.json | error | diff --git a/plugins/auth-backend-module-onelogin-provider/package.json b/plugins/auth-backend-module-onelogin-provider/package.json index 465c9eb5b8..a7b087aaa6 100644 --- a/plugins/auth-backend-module-onelogin-provider/package.json +++ b/plugins/auth-backend-module-onelogin-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-onelogin-provider", - "version": "0.3.7-next.0", + "version": "0.3.7", "description": "The onelogin-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-openshift-provider/.eslintrc.js b/plugins/auth-backend-module-openshift-provider/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend-module-openshift-provider/CHANGELOG.md b/plugins/auth-backend-module-openshift-provider/CHANGELOG.md new file mode 100644 index 0000000000..06f116c2ef --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/CHANGELOG.md @@ -0,0 +1,14 @@ +# @backstage/plugin-auth-backend-module-openshift-provider + +## 0.1.0 + +### Minor Changes + +- 5a84253: Add new `auth-backend-module-openshift-provider`. This authentication provider enables Backstage to sign in with OpenShift. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 diff --git a/plugins/auth-backend-module-openshift-provider/README.md b/plugins/auth-backend-module-openshift-provider/README.md new file mode 100644 index 0000000000..ae4700d561 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/README.md @@ -0,0 +1,5 @@ +# @backstage/plugin-auth-backend-module-openshift-provider + +The openshift-provider backend module for the auth plugin. + +_This plugin was created through the Backstage CLI_ diff --git a/plugins/auth-backend-module-openshift-provider/catalog-info.yaml b/plugins/auth-backend-module-openshift-provider/catalog-info.yaml new file mode 100644 index 0000000000..3db615244a --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth-backend-module-openshift-provider + title: '@backstage/plugin-auth-backend-module-openshift-provider' + description: The OpenShift backend module for the auth plugin. +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: auth-maintainers diff --git a/plugins/auth-backend-module-openshift-provider/config.d.ts b/plugins/auth-backend-module-openshift-provider/config.d.ts new file mode 100644 index 0000000000..1fdaeb1532 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/config.d.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { HumanDuration } from '@backstage/types'; + +export interface Config { + auth?: { + providers?: { + /** @visibility frontend */ + openshift?: { + [authEnv: string]: { + clientId: string; + /** + * @visibility secret + */ + clientSecret: string; + authorizationUrl: string; + tokenUrl: string; + callbackUrl?: string; + openshiftApiServerUrl: string; + signIn?: { + resolvers: Array<{ + resolver: 'displayNameMatchingUserEntityName'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + }>; + }; + sessionDuration?: HumanDuration | string; + }; + }; + }; + }; +} diff --git a/plugins/auth-backend-module-openshift-provider/dev/index.ts b/plugins/auth-backend-module-openshift-provider/dev/index.ts new file mode 100644 index 0000000000..99f6828292 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/dev/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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'; +import authPlugin from '@backstage/plugin-auth-backend'; +import authModuleOpenShiftProvider from '../src'; + +const backend = createBackend(); + +backend.add(authPlugin); +backend.add(authModuleOpenShiftProvider); + +backend.start(); diff --git a/plugins/auth-backend-module-openshift-provider/package.json b/plugins/auth-backend-module-openshift-provider/package.json new file mode 100644 index 0000000000..648f09bf24 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/package.json @@ -0,0 +1,55 @@ +{ + "name": "@backstage/plugin-auth-backend-module-openshift-provider", + "version": "0.1.0", + "description": "The OpenShift backend module for the auth plugin.", + "backstage": { + "role": "backend-plugin-module", + "pluginId": "auth", + "pluginPackage": "@backstage/plugin-auth-backend" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/auth-backend-module-openshift-provider" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "config.d.ts" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "@backstage/types": "workspace:^", + "passport-oauth2": "^1.8.0", + "zod": "^3.24.2" + }, + "devDependencies": { + "@backstage/backend-defaults": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/plugin-auth-backend": "workspace:^", + "express": "^4.18.2", + "msw": "^2.7.3", + "supertest": "^7.1.0" + }, + "configSchema": "config.d.ts" +} diff --git a/plugins/auth-backend-module-openshift-provider/report.api.md b/plugins/auth-backend-module-openshift-provider/report.api.md new file mode 100644 index 0000000000..aa429e1d47 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/report.api.md @@ -0,0 +1,28 @@ +## API Report File for "@backstage/plugin-auth-backend-module-openshift-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 { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node'; +import { PassportProfile } from '@backstage/plugin-auth-node'; + +// @public (undocumented) +const authModuleOpenshiftProvider: BackendFeature; +export default authModuleOpenshiftProvider; + +// @public (undocumented) +export const openshiftAuthenticator: OAuthAuthenticator< + OpenShiftAuthenticatorContext, + PassportProfile +>; + +// @public (undocumented) +export interface OpenShiftAuthenticatorContext { + // (undocumented) + helper: PassportOAuthAuthenticatorHelper; + // (undocumented) + openshiftApiServerUrl: string; +} +``` diff --git a/plugins/auth-backend-module-openshift-provider/src/authenticator.test.ts b/plugins/auth-backend-module-openshift-provider/src/authenticator.test.ts new file mode 100644 index 0000000000..28a7738d21 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/src/authenticator.test.ts @@ -0,0 +1,226 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { setupServer } from 'msw/node'; +import { + decodeOAuthState, + encodeOAuthState, +} from '@backstage/plugin-auth-node'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; +import { http, HttpResponse } from 'msw'; +import { openshiftAuthenticator } from './authenticator'; +import { ConfigReader } from '@backstage/config'; +import { + OAuthState, + OAuthAuthenticatorStartInput, + OAuthAuthenticatorAuthenticateInput, +} from '@backstage/plugin-auth-node'; +import express from 'express'; + +describe('openshiftAuthenticator', () => { + let implementation: any; + let oauthState: OAuthState; + + const mswServer = setupServer(); + registerMswTestHooks(mswServer); + + beforeEach(() => { + mswServer.use( + http.post('https://openshift.test/oauth/token', () => { + return HttpResponse.json({ + access_token: 'accessToken', + scope: 'user:full', + expires_in: 60 * 60 * 24, + }); + }), + http.get( + 'https://api.openshift.test/apis/user.openshift.io/v1/users/~', + async () => { + return HttpResponse.json({ + kind: 'User', + apiVersion: 'user.openshift.io/v1', + metadata: { + name: 'alice', + uid: 'ca993628-8817-4a3b-9811-be4a34c60bf4', + resourceVersion: '1', + creationTimestamp: '2022-01-11T13:10:45Z', + managedFields: [], + }, + fullName: 'Alice Adams', + identities: ['SSO:id'], + groups: ['system:authenticated', 'system:authenticated:oauth'], + }); + }, + ), + ); + + implementation = openshiftAuthenticator.initialize({ + callbackUrl: 'https://backstage.test/callback', + config: new ConfigReader({ + clientId: 'clientId', + clientSecret: 'clientSecret', + authorizationUrl: 'https://openshift.test/oauth/authorize', + tokenUrl: 'https://openshift.test/oauth/token', + openshiftApiServerUrl: 'https://api.openshift.test', + }), + }); + + oauthState = { + nonce: 'nonce', + env: 'env', + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('#start', () => { + let fakeSession: Record; + let startRequest: OAuthAuthenticatorStartInput; + + beforeEach(() => { + fakeSession = {}; + startRequest = { + state: encodeOAuthState(oauthState), + req: { + method: 'GET', + url: 'test', + session: fakeSession, + }, + } as unknown as OAuthAuthenticatorStartInput; + }); + + it('initiates authorization code grant', async () => { + const startResponse = await openshiftAuthenticator.start( + startRequest, + implementation, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('response_type')).toBe('code'); + }); + + it('passes client ID from config', async () => { + const startResponse = await openshiftAuthenticator.start( + startRequest, + implementation, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('client_id')).toBe('clientId'); + }); + + it('passes callback URL from config', async () => { + const startResponse = await openshiftAuthenticator.start( + startRequest, + implementation, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('redirect_uri')).toBe( + 'https://backstage.test/callback', + ); + }); + + it('encodes OAuth state in query param', async () => { + const startResponse = await openshiftAuthenticator.start( + startRequest, + implementation, + ); + const { searchParams } = new URL(startResponse.url); + const stateParam = searchParams.get('state'); + const decodedState = decodeOAuthState(stateParam!); + + expect(decodedState).toMatchObject(oauthState); + }); + }); + + describe('#authenticate', () => { + let handlerRequest: OAuthAuthenticatorAuthenticateInput; + + beforeEach(() => { + handlerRequest = { + req: { + method: 'GET', + query: { + code: 'authorization_code', + state: encodeOAuthState(oauthState), + }, + session: { + 'oauth2:openshift': { + state: encodeOAuthState(oauthState), + }, + }, + } as unknown as express.Request, + }; + }); + + it('exchanges authorization code for access token', async () => { + const authenticatorResult = await openshiftAuthenticator.authenticate( + handlerRequest, + implementation, + ); + const accessToken = authenticatorResult.session.accessToken; + + expect(accessToken).toEqual('accessToken'); + }); + + it('returns granted scope', async () => { + const authenticatorResult = await openshiftAuthenticator.authenticate( + handlerRequest, + implementation, + ); + const responseScope = authenticatorResult.session.scope; + + expect(responseScope).toEqual('user:full'); + }); + + it('returns a default session.tokentype field', async () => { + const authenticatorResult = await openshiftAuthenticator.authenticate( + handlerRequest, + implementation, + ); + const tokenType = authenticatorResult.session.tokenType; + + expect(tokenType).toEqual('bearer'); + }); + + it('returns displayName', async () => { + const authenticatorResult = await openshiftAuthenticator.authenticate( + handlerRequest, + implementation, + ); + + expect(authenticatorResult).toMatchObject({ + fullProfile: { + displayName: 'alice', + }, + }); + }); + + it('should store access token as refresh token', async () => { + const authenticatorResult = await openshiftAuthenticator.authenticate( + handlerRequest, + implementation, + ); + + expect(authenticatorResult.session.refreshToken).toBe( + authenticatorResult.session.accessToken, + ); + }); + }); +}); diff --git a/plugins/auth-backend-module-openshift-provider/src/authenticator.ts b/plugins/auth-backend-module-openshift-provider/src/authenticator.ts new file mode 100644 index 0000000000..0c9acb6287 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/src/authenticator.ts @@ -0,0 +1,184 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createOAuthAuthenticator, + PassportOAuthAuthenticatorHelper, + PassportOAuthDoneCallback, + PassportProfile, +} from '@backstage/plugin-auth-node'; +import { createHash } from 'node:crypto'; +import OAuth2Strategy from 'passport-oauth2'; +import { z } from 'zod'; + +/** @public */ +export interface OpenShiftAuthenticatorContext { + openshiftApiServerUrl: string; + helper: PassportOAuthAuthenticatorHelper; +} + +/** @private + * Schema for user.openshift.io/v1, + * see https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/user_and_group_apis/user-user-openshift-io-v1#user-user-openshift-io-v1 + */ +const OpenShiftUser = z.object({ + metadata: z.object({ + name: z.string(), + }), +}); + +/** @public */ +export const openshiftAuthenticator = createOAuthAuthenticator< + OpenShiftAuthenticatorContext, + PassportProfile +>({ + defaultProfileTransform: + PassportOAuthAuthenticatorHelper.defaultProfileTransform, + scopes: { + required: ['user:full'], + }, + initialize({ callbackUrl, config }) { + const clientId = config.getString('clientId'); + const clientSecret = config.getString('clientSecret'); + const authorizationUrl = config.getString('authorizationUrl'); + const tokenUrl = config.getString('tokenUrl'); + const openshiftApiServerUrl = config.getString('openshiftApiServerUrl'); + + // userUrl: `${openshiftApiServerUrl}/apis/user.openshift.io/v1/users/~`, + const strategy = new OAuth2Strategy( + { + clientID: clientId, + clientSecret: clientSecret, + callbackURL: callbackUrl, + authorizationURL: authorizationUrl, + tokenURL: tokenUrl, + passReqToCallback: false, + }, + ( + accessToken: any, + refreshToken: string, + params: any, + fullProfile: PassportProfile, + done: PassportOAuthDoneCallback, + ) => { + done(undefined, { fullProfile, params, accessToken }, { refreshToken }); + }, + ); + + strategy.userProfile = function userProfile( + accessToken: string, + done: (err?: unknown, profile?: any) => void, + ): void { + this._oauth2.useAuthorizationHeaderforGET(true); + + this._oauth2.get( + `${openshiftApiServerUrl}/apis/user.openshift.io/v1/users/~`, + accessToken, + (error, data, _) => { + if (error !== null && error.statusCode !== 200) { + done(new Error(`HTTP error! Status: ${error.statusCode}`)); + return; + } + + if (!data) { + done(new Error('No data provided!')); + return; + } + + if (typeof data !== 'string') { + done(new Error('Data of type Buffer is not supported!')); + return; + } + + const user = OpenShiftUser.parse(JSON.parse(data)); + done(null, { displayName: user.metadata.name }); + }, + ); + }; + + return { + openshiftApiServerUrl, + helper: PassportOAuthAuthenticatorHelper.from(strategy), + }; + }, + async start(input, { helper }) { + return helper.start(input, { + accessType: 'offline', + prompt: 'consent', + }); + }, + async authenticate(input, { helper }) { + // Same workaround as the GitHub provider; see https://github.com/backstage/backstage/issues/25383 + const { fullProfile, session } = await helper.authenticate(input); + session.refreshToken = session.accessToken; + session.refreshTokenExpiresInSeconds = session.expiresInSeconds; + return { fullProfile, session }; + }, + async refresh(input, { helper }) { + // Because the session is refreshed on login, this override is crucial, + // see https://github.com/backstage/backstage/issues/25383 + const accessToken = input.refreshToken; + + 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, + }, + }; + }, + async logout(input, { openshiftApiServerUrl, helper }) { + // Due to the implementation of createOAuthRouteHandlers, only the refresh token is set. + // In this provider, the refresh token actually IS the access token. + const accessToken = input.refreshToken; + if (!accessToken) { + throw new Error('access token/refresh token needs to be set for logout'); + } + + // Check if access token is still valid. + try { + await helper.fetchProfile(accessToken); + } catch { + // Invalid token, no need to delete OAuthAccessToken. + return; + } + + // Calculate token name, see: + // https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/oauth_apis/oauthaccesstoken-oauth-openshift-io-v1#apis-oauth-openshift-io-v1-oauthaccesstokens + const tokenName = createHash('sha256') + .update(accessToken.slice('sha256~'.length)) + .digest() + .toString('base64url'); + + const response = await fetch( + `${openshiftApiServerUrl}/apis/oauth.openshift.io/v1/oauthaccesstokens/sha256~${tokenName}`, + { method: 'DELETE', headers: { Authorization: `Bearer ${accessToken}` } }, + ); + + if (response.status === 401) { + throw new Error('unauthorized'); + } + }, +}); diff --git a/plugins/auth-backend-module-openshift-provider/src/index.ts b/plugins/auth-backend-module-openshift-provider/src/index.ts new file mode 100644 index 0000000000..4c8dd6cb22 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/src/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 openshift-provider backend module for the auth plugin. + * + * @packageDocumentation + */ +export { + openshiftAuthenticator, + type OpenShiftAuthenticatorContext, +} from './authenticator'; +export { authModuleOpenshiftProvider as default } from './module'; diff --git a/plugins/auth-backend-module-openshift-provider/src/module.ts b/plugins/auth-backend-module-openshift-provider/src/module.ts new file mode 100644 index 0000000000..18d96c4778 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/src/module.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { openshiftAuthenticator } from './authenticator'; +import { openshiftSignInResolvers } from './resolvers'; + +/** @public */ +export const authModuleOpenshiftProvider = createBackendModule({ + pluginId: 'auth', + moduleId: 'openshift-provider', + register(reg) { + reg.registerInit({ + deps: { providers: authProvidersExtensionPoint }, + async init({ providers }) { + providers.registerProvider({ + providerId: 'openshift', + factory: createOAuthProviderFactory({ + authenticator: openshiftAuthenticator, + signInResolverFactories: { + ...openshiftSignInResolvers, + }, + }), + }); + }, + }); + }, +}); diff --git a/plugins/auth-backend-module-openshift-provider/src/resolvers.ts b/plugins/auth-backend-module-openshift-provider/src/resolvers.ts new file mode 100644 index 0000000000..dee55ec4ca --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/src/resolvers.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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'; + +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { z } from 'zod'; + +export namespace openshiftSignInResolvers { + export const displayNameMatchingUserEntityName = createSignInResolverFactory({ + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { + return async ( + info: SignInInfo>, + ctx, + ) => { + const { displayName } = info.profile; + + if (!displayName) { + throw new Error( + `OpenShift user profile does not contain a displayName`, + ); + } + + const userRef = stringifyEntityRef({ + kind: 'User', + name: displayName, + namespace: DEFAULT_NAMESPACE, + }); + + return await ctx.signInWithCatalogUser( + { entityRef: userRef }, + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { entityRef: { name: displayName } } + : undefined, + }, + ); + }; + }, + }); +} diff --git a/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md b/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md index a5c687b6aa..a23d6cb733 100644 --- a/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-pinniped-provider +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-pinniped-provider/knip-report.md b/plugins/auth-backend-module-pinniped-provider/knip-report.md index aa669344ff..23b403971a 100644 --- a/plugins/auth-backend-module-pinniped-provider/knip-report.md +++ b/plugins/auth-backend-module-pinniped-provider/knip-report.md @@ -4,7 +4,7 @@ | Name | Location | Severity | | :-------------- | :----------- | :------- | -| express-session | package.json | error | -| cookie-parser | package.json | error | -| passport | package.json | error | +| express-session | plugins/auth-backend-module-pinniped-provider/package.json | error | +| cookie-parser | plugins/auth-backend-module-pinniped-provider/package.json | error | +| passport | plugins/auth-backend-module-pinniped-provider/package.json | error | diff --git a/plugins/auth-backend-module-pinniped-provider/package.json b/plugins/auth-backend-module-pinniped-provider/package.json index 2c5b1fe7b4..ad80ff2904 100644 --- a/plugins/auth-backend-module-pinniped-provider/package.json +++ b/plugins/auth-backend-module-pinniped-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-pinniped-provider", - "version": "0.3.7-next.0", + "version": "0.3.7", "description": "The pinniped-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md b/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md index f197712c5b..41ac1cafdd 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-vmware-cloud-provider +## 0.5.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.5.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-vmware-cloud-provider/knip-report.md b/plugins/auth-backend-module-vmware-cloud-provider/knip-report.md index 9f46420ca0..3d57808d27 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/knip-report.md +++ b/plugins/auth-backend-module-vmware-cloud-provider/knip-report.md @@ -4,11 +4,11 @@ | Name | Location | Severity | | :----------------------- | :----------- | :------- | -| @backstage/catalog-model | package.json | error | +| @backstage/catalog-model | plugins/auth-backend-module-vmware-cloud-provider/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :---------------- | :----------- | :------- | -| @backstage/errors | package.json | error | +| @backstage/errors | plugins/auth-backend-module-vmware-cloud-provider/package.json | error | diff --git a/plugins/auth-backend-module-vmware-cloud-provider/package.json b/plugins/auth-backend-module-vmware-cloud-provider/package.json index 23f5b7eeb5..0222da22a5 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/package.json +++ b/plugins/auth-backend-module-vmware-cloud-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-vmware-cloud-provider", - "version": "0.5.7-next.0", + "version": "0.5.7", "description": "The vmware-cloud-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 54dee8262d..be4b8bcaad 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-auth-backend +## 0.25.4 + +### Patch Changes + +- 1d47bf3: Implementing Dynamic Client Registration with the OIDC server. You can enable this by setting `auth.experimentalDynamicClientRegistration.enabled` in `app-config.yaml`. This is highly experimental, but feedback welcome. +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## 0.25.4-next.1 + +### Patch Changes + +- 1d47bf3: Implementing Dynamic Client Registration with the OIDC server. You can enable this by setting `auth.experimentalDynamicClientRegistration.enabled` in `app-config.yaml`. This is highly experimental, but feedback welcome. +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.25.4-next.0 ### Patch Changes diff --git a/plugins/auth-backend/migrations/20250909120000_oidc_client_registration.js b/plugins/auth-backend/migrations/20250909120000_oidc_client_registration.js new file mode 100644 index 0000000000..87391c3467 --- /dev/null +++ b/plugins/auth-backend/migrations/20250909120000_oidc_client_registration.js @@ -0,0 +1,159 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + // These tables make up the OIDC client registration flow. + // Clients are the top of the tree, that are created by the client registration flow. + await knex.schema.createTable('oidc_clients', table => { + table.comment( + 'OIDC clients that are registered via dynamic client registration', + ); + + table + .string('client_id') + .primary() + .notNullable() + .comment('The unique client ID of the client'); + + table + .string('client_secret') + .notNullable() + .comment('The client secret of the client'); + + table + .string('client_name') + .notNullable() + .comment('The name of the client, should be human readable'); + + table + .text('response_types', 'longtext') + .notNullable() + .comment('JSON array of supported response types'); + + table + .text('grant_types', 'longtext') + .notNullable() + .comment('JSON array of supported grant types'); + + table + .text('redirect_uris', 'longtext') + .notNullable() + .comment('Allowed redirect URIs as JSON array'); + + table.text('scope').nullable().comment('Default scopes for the client'); + + table + .text('metadata', 'longtext') + .nullable() + .comment('Additional client metadata as JSON'); + }); + + await knex.schema.createTable('oauth_authorization_sessions', table => { + table.comment('Core OAuth authorization sessions with shared context'); + + table + .string('id') + .primary() + .notNullable() + .comment('Unique session identifier'); + + table.string('client_id').notNullable().comment('OIDC client identifier'); + + table + .string('user_entity_ref') + .nullable() + .comment('Backstage user entity reference'); + + table + .text('redirect_uri', 'longtext') + .notNullable() + .comment('Client redirect URI'); + + table.text('scope').nullable().comment('Requested scopes space-separated'); + + table.string('state').nullable().comment('Client state parameter'); + + table.string('response_type').notNullable().comment('OAuth2 response type'); + + table.string('code_challenge').nullable().comment('PKCE code challenge'); + + table + .string('code_challenge_method') + .nullable() + .comment('PKCE code challenge method'); + + table.string('nonce').nullable().comment('OIDC nonce parameter'); + + table + .enum('status', ['pending', 'approved', 'rejected', 'expired']) + .defaultTo('pending') + .comment('Authorization session status'); + + table + .timestamp('expires_at', { useTz: true, precision: 0 }) + .notNullable() + .comment('Session expiration timestamp'); + + table.foreign('client_id').references('client_id').inTable('oidc_clients'); + table.index(['client_id', 'user_entity_ref']); + table.index(['status', 'expires_at']); + }); + + await knex.schema.createTable('oidc_authorization_codes', table => { + table.comment('OAuth authorization codes for code exchange flow'); + + table + .string('code') + .primary() + .notNullable() + .comment('Unique authorization code'); + + table + .string('session_id') + .notNullable() + .comment('Authorization session identifier'); + + table + .timestamp('expires_at', { useTz: true, precision: 0 }) + .notNullable() + .comment('Authorization code expiration timestamp'); + + table + .boolean('used') + .defaultTo(false) + .comment('Whether the authorization code has been used'); + + table + .foreign('session_id') + .references('id') + .inTable('oauth_authorization_sessions') + .onDelete('CASCADE'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.dropTable('oidc_authorization_codes'); + await knex.schema.dropTable('oauth_authorization_sessions'); + await knex.schema.dropTable('oidc_clients'); +}; diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 0dddd935e6..74c03204b6 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,11 +1,12 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.25.4-next.0", + "version": "0.25.4", "description": "A Backstage backend plugin that handles authentication", "backstage": { "role": "backend-plugin", "pluginId": "auth", "pluginPackages": [ + "@backstage/plugin-auth", "@backstage/plugin-auth-backend", "@backstage/plugin-auth-node", "@backstage/plugin-auth-react" @@ -60,6 +61,7 @@ "knex": "^3.0.0", "lodash": "^4.17.21", "luxon": "^3.0.0", + "matcher": "^4.0.0", "minimatch": "^9.0.0", "passport": "^0.7.0", "uuid": "^11.0.0" diff --git a/plugins/auth-backend/report.sql.md b/plugins/auth-backend/report.sql.md index b135414af6..7622a5750e 100644 --- a/plugins/auth-backend/report.sql.md +++ b/plugins/auth-backend/report.sql.md @@ -5,6 +5,59 @@ > [!WARNING] > Failed to migrate down from '20220321100910_timestamptz_again.js' +## Table `oauth_authorization_sessions` + +| Column | Type | Nullable | Max Length | Default | +| ----------------------- | -------------------------- | -------- | ---------- | ----------------- | +| `client_id` | `character varying` | false | 255 | - | +| `code_challenge` | `character varying` | true | 255 | - | +| `code_challenge_method` | `character varying` | true | 255 | - | +| `expires_at` | `timestamp with time zone` | false | - | - | +| `id` | `character varying` | false | 255 | - | +| `nonce` | `character varying` | true | 255 | - | +| `redirect_uri` | `text` | false | - | - | +| `response_type` | `character varying` | false | 255 | - | +| `scope` | `text` | true | - | - | +| `state` | `character varying` | true | 255 | - | +| `status` | `text` | true | - | `'pending'::text` | +| `user_entity_ref` | `character varying` | true | 255 | - | + +### Indices + +- `oauth_authorization_sessions_client_id_user_entity_ref_index` (`client_id`, `user_entity_ref`) +- `oauth_authorization_sessions_pkey` (`id`) unique primary +- `oauth_authorization_sessions_status_expires_at_index` (`status`, `expires_at`) + +## Table `oidc_authorization_codes` + +| Column | Type | Nullable | Max Length | Default | +| ------------ | -------------------------- | -------- | ---------- | ------- | +| `code` | `character varying` | false | 255 | - | +| `expires_at` | `timestamp with time zone` | false | - | - | +| `session_id` | `character varying` | false | 255 | - | +| `used` | `boolean` | true | - | `false` | + +### Indices + +- `oidc_authorization_codes_pkey` (`code`) unique primary + +## Table `oidc_clients` + +| Column | Type | Nullable | Max Length | Default | +| ---------------- | ------------------- | -------- | ---------- | ------- | +| `client_id` | `character varying` | false | 255 | - | +| `client_name` | `character varying` | false | 255 | - | +| `client_secret` | `character varying` | false | 255 | - | +| `grant_types` | `text` | false | - | - | +| `metadata` | `text` | true | - | - | +| `redirect_uris` | `text` | false | - | - | +| `response_types` | `text` | false | - | - | +| `scope` | `text` | true | - | - | + +### Indices + +- `oidc_clients_pkey` (`client_id`) unique primary + ## Table `sessions` | Column | Type | Nullable | Max Length | Default | diff --git a/plugins/auth-backend/src/authPlugin.ts b/plugins/auth-backend/src/authPlugin.ts index f3877d48cd..025d72fcb7 100644 --- a/plugins/auth-backend/src/authPlugin.ts +++ b/plugins/auth-backend/src/authPlugin.ts @@ -66,6 +66,7 @@ export const authPlugin = createBackendPlugin({ database: coreServices.database, discovery: coreServices.discovery, auth: coreServices.auth, + httpAuth: coreServices.httpAuth, catalog: catalogServiceRef, }, async init({ @@ -75,6 +76,7 @@ export const authPlugin = createBackendPlugin({ database, discovery, auth, + httpAuth, catalog, }) { const router = await createRouter({ @@ -86,6 +88,7 @@ export const authPlugin = createBackendPlugin({ catalog, providerFactories: Object.fromEntries(providers), ownershipResolver, + httpAuth, }); httpRouter.addAuthPolicy({ path: '/', diff --git a/plugins/auth-backend/src/database/OidcDatabase.test.ts b/plugins/auth-backend/src/database/OidcDatabase.test.ts new file mode 100644 index 0000000000..19285aec0a --- /dev/null +++ b/plugins/auth-backend/src/database/OidcDatabase.test.ts @@ -0,0 +1,293 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { AuthDatabase } from './AuthDatabase'; +import { OidcDatabase } from './OidcDatabase'; +import { resolvePackagePath } from '@backstage/backend-plugin-api'; + +jest.setTimeout(60_000); + +describe('Oidc Database', () => { + const databases = TestDatabases.create(); + + async function createOidcDatabase(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + + await knex.migrate.latest({ + directory: resolvePackagePath( + '@backstage/plugin-auth-backend', + 'migrations', + ), + }); + + return { + oidc: await OidcDatabase.create({ + database: AuthDatabase.create({ + getClient: async () => knex, + }), + }), + }; + } + + describe.each(databases.eachSupportedId())('%p', databaseId => { + describe('Clients', () => { + it('should create and return a client', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + await expect( + oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }), + ).resolves.toEqual({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: undefined, + expiresAt: undefined, + metadata: undefined, + }); + }); + + it('should return the client thats created in a list', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + await expect( + oidc.getClient({ clientId: 'test-client' }), + ).resolves.toEqual(client); + }); + + it('should return null if the client does not exist', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + await expect( + oidc.getClient({ clientId: 'test-client' }), + ).resolves.toBeNull(); + }); + }); + + describe('Authorization Sessions', () => { + it('should create and return an authorization session', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + userEntityRef: 'user:default/blam', + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + codeChallenge: 'test-challenge', + codeChallengeMethod: 'S256', + nonce: 'test-nonce', + expiresAt: new Date('2025-01-01T00:00:00Z'), + }); + + expect(session).toEqual( + expect.objectContaining({ + id: 'test-session', + clientId: client.clientId, + userEntityRef: 'user:default/blam', + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + codeChallenge: 'test-challenge', + codeChallengeMethod: 'S256', + nonce: 'test-nonce', + expiresAt: new Date('2025-01-01T00:00:00Z'), + status: 'pending', + }), + ); + }); + + it('should allow updating the authorization session', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + expiresAt: new Date('2025-01-01T00:00:00Z'), + }); + + await expect( + oidc.updateAuthorizationSession({ + id: 'test-session', + userEntityRef: 'user:default/blam', + status: 'approved', + }), + ).resolves.toEqual({ + ...session, + userEntityRef: 'user:default/blam', + status: 'approved', + }); + }); + }); + + describe('Authorization Codes', () => { + it('should create and return an authorization code', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + expiresAt: new Date('2025-01-01T00:00:00Z'), + }); + + const authCode = await oidc.createAuthorizationCode({ + code: 'test-code', + sessionId: session.id, + expiresAt: new Date('2025-01-01T00:00:00Z'), + }); + + expect(authCode).toEqual( + expect.objectContaining({ + code: 'test-code', + sessionId: session.id, + expiresAt: new Date('2025-01-01T00:00:00Z'), + }), + ); + }); + + it('should return authorization code with session data', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + userEntityRef: 'user:default/blam', + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + codeChallenge: 'test-challenge', + codeChallengeMethod: 'S256', + nonce: 'test-nonce', + expiresAt: new Date('2025-01-01T00:00:00Z'), + }); + + const authCode = await oidc.createAuthorizationCode({ + code: 'test-code', + sessionId: session.id, + expiresAt: new Date('2025-01-01T00:00:00Z'), + }); + + const authCodeFromDb = await oidc.getAuthorizationCode({ + code: 'test-code', + }); + const sessionFromDb = await oidc.getAuthorizationSession({ + id: authCodeFromDb!.sessionId, + }); + + expect(authCodeFromDb).toEqual(authCode); + expect(sessionFromDb).toEqual(session); + }); + + it('should allow updating the authorization code', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + expiresAt: new Date('2025-01-01T00:00:00Z'), + }); + + const authCode = await oidc.createAuthorizationCode({ + code: 'test-code', + sessionId: session.id, + expiresAt: new Date('2025-01-01T00:00:00Z'), + }); + + const updatedAuthCode = await oidc.updateAuthorizationCode({ + code: 'test-code', + used: true, + }); + + expect(updatedAuthCode).toEqual({ + ...authCode, + used: true, + }); + }); + }); + }); +}); diff --git a/plugins/auth-backend/src/database/OidcDatabase.ts b/plugins/auth-backend/src/database/OidcDatabase.ts new file mode 100644 index 0000000000..ebb6619400 --- /dev/null +++ b/plugins/auth-backend/src/database/OidcDatabase.ts @@ -0,0 +1,397 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Knex } from 'knex'; +import { AuthDatabase } from './AuthDatabase'; + +function toDate(value?: Date | string | number): Date | undefined { + if (!value) { + return undefined; + } + + return typeof value === 'string' || typeof value === 'number' + ? new Date(value) + : value; +} +type OidcClientRow = { + client_id: string; + client_secret: string; + client_name: string; + response_types: string; + grant_types: string; + redirect_uris: string; + scope: string | null; + metadata: string | null; +}; + +type OAuthAuthorizationSessionRow = { + id: string; + client_id: string; + user_entity_ref: string | null; + redirect_uri: string; + scope: string | null; + state: string | null; + response_type: string; + code_challenge: string | null; + code_challenge_method: string | null; + nonce: string | null; + status: 'pending' | 'approved' | 'rejected' | 'expired'; + expires_at: Date | string; +}; + +type OidcAuthorizationCodeRow = { + code: string; + session_id: string; + expires_at: Date | string; + used: boolean; +}; + +export type Client = { + clientId: string; + clientName: string; + clientSecret: string; + redirectUris: string[]; + responseTypes: string[]; + grantTypes: string[]; + scope?: string; + metadata?: Record; +}; + +export type AuthorizationSession = { + id: string; + clientId: string; + userEntityRef?: string; + redirectUri: string; + scope?: string; + state?: string; + responseType: string; + codeChallenge?: string; + codeChallengeMethod?: string; + nonce?: string; + status: 'pending' | 'approved' | 'rejected' | 'expired'; + expiresAt: Date; +}; + +export type ConsentRequest = { + id: string; + sessionId: string; + expiresAt: Date; +}; + +export type AuthorizationCode = { + code: string; + sessionId: string; + expiresAt: Date; + used: boolean; +}; + +export type AccessToken = { + tokenId: string; + sessionId: string; + expiresAt: Date; +}; + +/** + * This class provides database operations for OpenID Connect (OIDC) authentication flows. + * It manages OIDC clients, authorization codes, and access tokens in the database. + */ +export class OidcDatabase { + private constructor(private readonly db: Knex) {} + + static async create(options: { database: AuthDatabase }) { + const client = await options.database.get(); + return new OidcDatabase(client); + } + + async createClient(client: Client) { + await this.db('oidc_clients').insert({ + client_id: client.clientId, + client_secret: client.clientSecret, + client_name: client.clientName, + response_types: JSON.stringify(client.responseTypes), + grant_types: JSON.stringify(client.grantTypes), + redirect_uris: JSON.stringify(client.redirectUris), + scope: client.scope, + metadata: JSON.stringify(client.metadata), + }); + + return client; + } + + async getClient({ clientId }: { clientId: string }) { + const client = await this.db('oidc_clients') + .where('client_id', clientId) + .first(); + + if (!client) { + return null; + } + + return this.rowToClient(client) as Client; + } + + async createAuthorizationSession( + session: Omit, + ) { + await this.db( + 'oauth_authorization_sessions', + ).insert({ + id: session.id, + client_id: session.clientId, + user_entity_ref: session.userEntityRef, + redirect_uri: session.redirectUri, + scope: session.scope, + state: session.state, + response_type: session.responseType, + code_challenge: session.codeChallenge, + code_challenge_method: session.codeChallengeMethod, + nonce: session.nonce, + status: 'pending', + expires_at: session.expiresAt, + }); + + return { + ...session, + status: 'pending', + }; + } + + async updateAuthorizationSession( + session: Partial & { id: string }, + ) { + const row = this.authorizationSessionToRow(session); + const updatedFields = Object.fromEntries( + Object.entries(row).filter(([_, value]) => value !== undefined), + ); + + // MySQL and SQLite3 don't support RETURNING + if ( + this.db.client.config.client.includes('sqlite3') || + this.db.client.config.client.includes('mysql') + ) { + return await this.db.transaction(async trx => { + await trx('oauth_authorization_sessions') + .where('id', session.id) + .update(updatedFields); + + const updated = await trx( + 'oauth_authorization_sessions', + ) + .where('id', session.id) + .first(); + + if (!updated) { + throw new Error( + `Failed to retrieve updated authorization session with id ${session.id}`, + ); + } + + return this.rowToAuthorizationSession(updated) as AuthorizationSession; + }); + } + + const returnedRows = await this.db( + 'oauth_authorization_sessions', + ) + .where('id', session.id) + .update(updatedFields) + .returning('*'); + + if (returnedRows.length !== 1) { + throw new Error( + `Failed to retrieve updated authorization session with id ${session.id}`, + ); + } + + const [returnedSession] = returnedRows; + + return this.rowToAuthorizationSession( + returnedSession, + ) as AuthorizationSession; + } + + async getAuthorizationSession({ id }: { id: string }) { + const session = await this.db( + 'oauth_authorization_sessions', + ) + .where('id', id) + .first(); + + if (!session) { + return null; + } + + return this.rowToAuthorizationSession(session) as AuthorizationSession; + } + + async createAuthorizationCode( + authorizationCode: Omit, + ) { + await this.db('oidc_authorization_codes').insert({ + code: authorizationCode.code, + session_id: authorizationCode.sessionId, + expires_at: authorizationCode.expiresAt, + used: false, + }); + + return { + ...authorizationCode, + used: false, + }; + } + + async getAuthorizationCode({ code }: { code: string }) { + const authCode = await this.db( + 'oidc_authorization_codes', + ) + .where('code', code) + .first(); + + if (!authCode) { + return null; + } + + return this.rowToAuthorizationCode(authCode) as AuthorizationCode; + } + + async updateAuthorizationCode( + authorizationCode: Partial & { code: string }, + ) { + const row = this.authorizationCodeToRow(authorizationCode); + const updatedFields = Object.fromEntries( + Object.entries(row).filter(([_, value]) => value !== undefined), + ); + + // MySQL and SQLite3 don't support RETURNING + if ( + this.db.client.config.client.includes('sqlite3') || + this.db.client.config.client.includes('mysql') + ) { + return await this.db.transaction(async trx => { + await trx('oidc_authorization_codes') + .where('code', authorizationCode.code) + .update(updatedFields); + + const updated = await trx( + 'oidc_authorization_codes', + ) + .where('code', authorizationCode.code) + .first(); + + if (!updated) { + throw new Error( + `Failed to retrieve updated authorization code with code ${authorizationCode.code}`, + ); + } + + return this.rowToAuthorizationCode(updated) as AuthorizationCode; + }); + } + + const returnedRows = await this.db( + 'oidc_authorization_codes', + ) + .where('code', authorizationCode.code) + .update(updatedFields) + .returning('*'); + + if (returnedRows.length !== 1) { + throw new Error( + `Failed to retrieve updated authorization code with code ${authorizationCode.code}`, + ); + } + + const [returnedCode] = returnedRows; + + return this.rowToAuthorizationCode(returnedCode) as AuthorizationCode; + } + + private rowToClient(row: OidcClientRow): Client { + return { + clientId: row.client_id, + clientName: row.client_name, + clientSecret: row.client_secret, + redirectUris: row.redirect_uris + ? JSON.parse(row.redirect_uris) + : undefined, + responseTypes: row.response_types + ? JSON.parse(row.response_types) + : undefined, + grantTypes: row.grant_types ? JSON.parse(row.grant_types) : undefined, + scope: row.scope ?? undefined, + metadata: row.metadata ? JSON.parse(row.metadata) : undefined, + }; + } + + private authorizationSessionToRow( + session: Partial, + ): Partial { + return { + id: session.id, + client_id: session.clientId, + user_entity_ref: session.userEntityRef, + redirect_uri: session.redirectUri, + scope: session.scope, + state: session.state, + response_type: session.responseType, + code_challenge: session.codeChallenge, + code_challenge_method: session.codeChallengeMethod, + nonce: session.nonce, + status: session.status, + expires_at: toDate(session.expiresAt), + }; + } + + private rowToAuthorizationSession( + row: OAuthAuthorizationSessionRow, + ): Partial { + return { + id: row.id, + clientId: row.client_id, + userEntityRef: row.user_entity_ref ?? undefined, + redirectUri: row.redirect_uri, + scope: row.scope ?? undefined, + state: row.state ?? undefined, + responseType: row.response_type, + codeChallenge: row.code_challenge ?? undefined, + codeChallengeMethod: row.code_challenge_method ?? undefined, + nonce: row.nonce ?? undefined, + status: row.status, + expiresAt: toDate(row.expires_at), + }; + } + + private authorizationCodeToRow( + authorizationCode: Partial, + ): Partial { + return { + code: authorizationCode.code, + session_id: authorizationCode.sessionId, + expires_at: toDate(authorizationCode.expiresAt), + used: authorizationCode.used, + }; + } + + private rowToAuthorizationCode( + row: OidcAuthorizationCodeRow, + ): Partial { + return { + code: row.code, + sessionId: row.session_id, + expiresAt: toDate(row.expires_at), + used: Boolean(row.used), + }; + } +} diff --git a/plugins/auth-backend/src/migrations.test.ts b/plugins/auth-backend/src/migrations.test.ts index f9575c70fd..df7063351a 100644 --- a/plugins/auth-backend/src/migrations.test.ts +++ b/plugins/auth-backend/src/migrations.test.ts @@ -186,4 +186,122 @@ describe('migrations', () => { await knex.destroy(); }, ); + + it.each(databases.eachSupportedId())( + '20250909120000_oidc_client_registration.js, %p', + async databaseId => { + const knex = await databases.init(databaseId); + + await migrateUntilBefore( + knex, + '20250909120000_oidc_client_registration.js', + ); + await migrateUpOnce(knex); + + await knex + .insert({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + client_name: 'Test Client', + response_types: JSON.stringify(['code']), + grant_types: JSON.stringify(['authorization_code']), + redirect_uris: JSON.stringify(['https://example.com/callback']), + scope: 'openid profile', + metadata: JSON.stringify({ description: 'Test client' }), + }) + .into('oidc_clients'); + + await expect( + knex('oidc_clients').where('client_id', 'test-client-id').first(), + ).resolves.toEqual({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + client_name: 'Test Client', + response_types: JSON.stringify(['code']), + grant_types: JSON.stringify(['authorization_code']), + redirect_uris: JSON.stringify(['https://example.com/callback']), + scope: 'openid profile', + metadata: JSON.stringify({ description: 'Test client' }), + }); + + await knex + .insert({ + id: 'test-session-id', + client_id: 'test-client-id', + user_entity_ref: 'user:default/test-user', + redirect_uri: 'https://example.com/callback', + scope: 'openid', + state: 'test-state', + response_type: 'code', + code_challenge: 'test-challenge', + code_challenge_method: 'S256', + nonce: 'test-nonce', + status: 'pending', + expires_at: new Date(Date.now() + 3600000), + }) + .into('oauth_authorization_sessions'); + + await expect( + knex('oauth_authorization_sessions') + .where('id', 'test-session-id') + .first(), + ).resolves.toEqual( + expect.objectContaining({ + id: 'test-session-id', + client_id: 'test-client-id', + user_entity_ref: 'user:default/test-user', + redirect_uri: 'https://example.com/callback', + scope: 'openid', + state: 'test-state', + response_type: 'code', + code_challenge: 'test-challenge', + code_challenge_method: 'S256', + nonce: 'test-nonce', + status: 'pending', + }), + ); + + await knex + .insert({ + code: 'test-auth-code', + session_id: 'test-session-id', + expires_at: new Date(Date.now() + 600000), + used: false, + }) + .into('oidc_authorization_codes'); + + await expect( + knex('oidc_authorization_codes') + .where('code', 'test-auth-code') + .first(), + ).resolves.toEqual( + expect.objectContaining({ + code: 'test-auth-code', + session_id: 'test-session-id', + }), + ); + + await knex('oauth_authorization_sessions') + .where('id', 'test-session-id') + .del(); + + await expect( + knex('oidc_authorization_codes').where('session_id', 'test-session-id'), + ).resolves.toHaveLength(0); + + await migrateDownOnce(knex); + + const tables = [ + 'oidc_clients', + 'oauth_authorization_sessions', + 'oidc_authorization_codes', + ]; + + for (const table of tables) { + await expect(knex.schema.hasTable(table)).resolves.toBe(false); + } + + await knex.destroy(); + }, + ); }); diff --git a/plugins/auth-backend/src/service/OidcRouter.test.ts b/plugins/auth-backend/src/service/OidcRouter.test.ts index dbc6c88f93..de865a9c43 100644 --- a/plugins/auth-backend/src/service/OidcRouter.test.ts +++ b/plugins/auth-backend/src/service/OidcRouter.test.ts @@ -17,135 +17,794 @@ import { coreServices, createBackendPlugin, + resolvePackagePath, } from '@backstage/backend-plugin-api'; -import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; -import Router from 'express-promise-router'; +import { + mockServices, + startTestBackend, + TestDatabases, + TestDatabaseId, + mockCredentials, +} from '@backstage/backend-test-utils'; import request from 'supertest'; +import crypto from 'crypto'; import { OidcRouter } from './OidcRouter'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; +import { OidcDatabase } from '../database/OidcDatabase'; +import { AuthDatabase } from '../database/AuthDatabase'; +import { OidcService } from '../service/OidcService'; +import { TokenIssuer } from '../identity/types'; + +jest.setTimeout(60_000); describe('OidcRouter', () => { - describe('/v1/userinfo', () => { - it('should return user info for full tokens', async () => { - const auth = mockServices.auth.mock(); - const mockUserInfo = { - getUserInfo: jest.fn().mockResolvedValue({ - claims: { - sub: 'k/ns:n', - ent: ['k/ns:a', 'k/ns:b'], - }, - }), - } as unknown as UserInfoDatabase; + const MOCK_USER_TOKEN = 'mock-user-token'; + const MOCK_USER_ENTITY_REF = 'user:default/test-user'; + const databases = TestDatabases.create(); - const { server } = await startTestBackend({ - features: [ - createBackendPlugin({ - pluginId: 'auth', - register(reg) { - reg.registerInit({ - deps: { httpRouter: coreServices.httpRouter }, - async init({ httpRouter }) { - const router = Router(); + async function createRouter(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); - router.use( - OidcRouter.create({ - auth, - tokenIssuer: {} as any, - baseUrl: 'http://localhost:7000', - userInfo: mockUserInfo, - }).getRouter(), - ); - httpRouter.use(router); - httpRouter.addAuthPolicy({ - path: '/', - allow: 'unauthenticated', - }); - }, - }); - }, - }), - ], - }); - - auth.authenticate.mockResolvedValueOnce({} as any); - auth.isPrincipal.mockReturnValueOnce(true); - - await request(server) - .get('/api/auth/v1/userinfo') - .set( - 'Authorization', - `Bearer h.${btoa( - JSON.stringify({ sub: 'k/ns:n', ent: ['k/ns:a', 'k/ns:b'] }), - )}.s`, - ) - .expect(200, { - claims: { - sub: 'k/ns:n', - ent: ['k/ns:a', 'k/ns:b'], - }, - }); - - expect(mockUserInfo.getUserInfo).toHaveBeenCalledWith('k/ns:n'); + await knex.migrate.latest({ + directory: resolvePackagePath( + '@backstage/plugin-auth-backend', + 'migrations', + ), }); - it('should return user info for limited tokens', async () => { - const auth = mockServices.auth.mock(); - const mockUserInfo = { - getUserInfo: jest.fn().mockResolvedValue({ - claims: { - sub: 'k/ns:n', - ent: ['k/ns:a', 'k/ns:b'], + const authDatabase = AuthDatabase.create({ + getClient: async () => knex, + }); + + const oidcDatabase = await OidcDatabase.create({ + database: authDatabase, + }); + + const userInfoDatabase = await UserInfoDatabase.create({ + database: authDatabase, + }); + + const mockTokenIssuer = { + issueToken: jest.fn(), + listPublicKeys: jest.fn(), + } as unknown as jest.Mocked; + + const mockAuth = mockServices.auth.mock(); + const mockHttpAuth = mockServices.httpAuth.mock(); + const mockConfig = mockServices.rootConfig({ + data: { + auth: { + experimentalDynamicClientRegistration: { + enabled: true, }, - }), - } as unknown as UserInfoDatabase; + }, + }, + }); - const { server } = await startTestBackend({ - features: [ - createBackendPlugin({ - pluginId: 'auth', - register(reg) { - reg.registerInit({ - deps: { httpRouter: coreServices.httpRouter }, - async init({ httpRouter }) { - const router = Router(); + const oidcService = OidcService.create({ + auth: mockAuth, + tokenIssuer: mockTokenIssuer, + baseUrl: 'http://localhost:7000', + userInfo: userInfoDatabase, + oidc: oidcDatabase, + config: mockConfig, + }); - router.use( - OidcRouter.create({ - auth, - tokenIssuer: {} as any, - baseUrl: 'http://localhost:7000', - userInfo: mockUserInfo, - }).getRouter(), - ); - httpRouter.use(router); - httpRouter.addAuthPolicy({ - path: '/', - allow: 'unauthenticated', - }); - }, - }); - }, - }), - ], - }); + const oidcRouter = OidcRouter.create({ + auth: mockAuth, + tokenIssuer: mockTokenIssuer, + baseUrl: 'http://localhost:7000', + appUrl: 'http://localhost:3000', + logger: mockServices.logger.mock(), + userInfo: userInfoDatabase, + oidc: oidcDatabase, + httpAuth: mockHttpAuth, + config: mockConfig, + }); - auth.authenticate.mockResolvedValueOnce({} as any); - auth.isPrincipal.mockReturnValueOnce(true); + return { + router: oidcRouter, + mocks: { + httpAuth: mockHttpAuth, + auth: mockAuth, + oidc: oidcDatabase, + userInfo: userInfoDatabase, + service: oidcService, + tokenIssuer: mockTokenIssuer, + }, + }; + } - await request(server) - .get('/api/auth/v1/userinfo') - .set( - 'Authorization', - `Bearer h.${btoa(JSON.stringify({ sub: 'k/ns:n' }))}.s`, - ) - .expect(200, { + describe.each(databases.eachSupportedId())('%p', databaseId => { + describe('/v1/userinfo', () => { + it('should return user info for full tokens', async () => { + const { + mocks: { auth, userInfo }, + router, + } = await createRouter(databaseId); + + await userInfo.addUserInfo({ claims: { sub: 'k/ns:n', ent: ['k/ns:a', 'k/ns:b'], + exp: Math.floor(Date.now() / 1000) + 3600, }, }); - expect(mockUserInfo.getUserInfo).toHaveBeenCalledWith('k/ns:n'); + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + auth.isPrincipal.mockReturnValueOnce(true); + + const response = await request(server) + .get('/api/auth/v1/userinfo') + .set( + 'Authorization', + `Bearer h.${btoa( + JSON.stringify({ sub: 'k/ns:n', ent: ['k/ns:a', 'k/ns:b'] }), + )}.s`, + ) + .expect(200); + + expect(response.body).toEqual({ + claims: { + sub: 'k/ns:n', + ent: ['k/ns:a', 'k/ns:b'], + exp: expect.any(Number), + }, + }); + }); + + it('should return user info for limited tokens', async () => { + const { + mocks: { auth, userInfo }, + router, + } = await createRouter(databaseId); + + await userInfo.addUserInfo({ + claims: { + sub: 'k/ns:n', + ent: ['k/ns:a', 'k/ns:b'], + exp: Math.floor(Date.now() / 1000) + 3600, + }, + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + auth.isPrincipal.mockReturnValueOnce(true); + + const response = await request(server) + .get('/api/auth/v1/userinfo') + .set( + 'Authorization', + `Bearer h.${btoa(JSON.stringify({ sub: 'k/ns:n' }))}.s`, + ) + .expect(200); + + expect(response.body).toEqual({ + claims: { + sub: 'k/ns:n', + ent: ['k/ns:a', 'k/ns:b'], + exp: expect.any(Number), + }, + }); + }); + }); + + describe('auth flow', () => { + it('should register a client', async () => { + const { router } = await createRouter(databaseId); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const response = await request(server) + .post('/api/auth/v1/register') + .send({ + client_name: 'Test Client', + redirect_uris: ['https://example.com/callback'], + response_types: ['code'], + grant_types: ['authorization_code'], + scope: 'openid', + }) + .expect(201); + + expect(response.body).toEqual({ + client_id: expect.any(String), + client_secret: expect.any(String), + redirect_uris: ['https://example.com/callback'], + }); + }); + + it('should create an authorization session via authorization endpoint', async () => { + const { + mocks: { service }, + router, + } = await createRouter(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const response = await request(server) + .get('/api/auth/v1/authorize') + .query({ + client_id: client.clientId, + redirect_uri: 'https://example.com/callback', + response_type: 'code', + scope: 'openid', + state: 'test-state', + }) + .expect(302); + + expect(response.header.location).toMatch( + /^http:\/\/localhost:3000\/oauth2\/authorize\/[a-f0-9-]+$/, + ); + }); + + it('should get auth session details', async () => { + const { + mocks: { service }, + router, + } = await createRouter(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const response = await request(server) + .get(`/api/auth/v1/sessions/${authSession.id}`) + .expect(200); + + expect(response.body).toEqual({ + id: authSession.id, + clientName: 'Test Client', + scope: 'openid', + redirectUri: 'https://example.com/callback', + }); + }); + + it('should approve authorization session', async () => { + const { + mocks: { auth, service, httpAuth }, + router, + } = await createRouter(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + httpAuth.credentials.mockResolvedValueOnce( + mockCredentials.user('user:default/test-user'), + ); + + auth.isPrincipal.mockReturnValueOnce(true); + + const response = await request(server) + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) + .expect(200); + + expect(response.body).toEqual({ + redirectUrl: expect.stringMatching( + /^https:\/\/example\.com\/callback\?code=[\w-]+&state=test-state$/, + ), + }); + }); + + it('should reject auth session', async () => { + const { + mocks: { service, httpAuth, auth }, + router, + } = await createRouter(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + httpAuth.credentials.mockResolvedValueOnce( + mockCredentials.user('user:default/test-user'), + ); + + auth.isPrincipal.mockReturnValueOnce(true); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const response = await request(server) + .post(`/api/auth/v1/sessions/${authSession.id}/reject`) + .expect(200); + + expect(response.body).toEqual({ + redirectUrl: expect.stringMatching( + /^https:\/\/example\.com\/callback\?error=access_denied&error_description=User\+denied\+the\+request&state=test-state$/, + ), + }); + }); + }); + + describe('token exchange', () => { + it('should exchange authorization code for tokens', async () => { + const { + mocks: { auth, service, tokenIssuer, httpAuth }, + router, + } = await createRouter(databaseId); + + httpAuth.credentials.mockResolvedValueOnce( + mockCredentials.user('user:default/test-user'), + ); + + auth.isPrincipal.mockReturnValueOnce(true); + + tokenIssuer.issueToken.mockResolvedValue({ + token: 'mock-access-token', + }); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const approvalResponse = await request(server) + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) + .expect(200); + + const redirectUrl = new URL(approvalResponse.body.redirectUrl); + const authorizationCode = redirectUrl.searchParams.get('code'); + + expect(authorizationCode).toBeDefined(); + + const tokenResponse = await request(server) + .post('/api/auth/v1/token') + .send({ + grant_type: 'authorization_code', + code: authorizationCode, + redirect_uri: 'https://example.com/callback', + }) + .expect(200); + + expect(tokenResponse.body).toEqual({ + access_token: 'mock-access-token', + token_type: 'Bearer', + expires_in: 3600, + id_token: 'mock-access-token', + scope: 'openid', + }); + + expect(tokenIssuer.issueToken).toHaveBeenCalledWith({ + claims: { + sub: MOCK_USER_ENTITY_REF, + }, + }); + }); + + it('should exchange authorization code for tokens with PKCE', async () => { + const { + mocks: { auth, service, tokenIssuer, httpAuth }, + router, + } = await createRouter(databaseId); + + tokenIssuer.issueToken.mockResolvedValue({ + token: 'mock-access-token-pkce', + }); + + httpAuth.credentials.mockResolvedValueOnce( + mockCredentials.user('user:default/test-user-pkce'), + ); + + auth.isPrincipal.mockReturnValueOnce(true); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const codeVerifier = + 'test-code-verifier-123456789012345678901234567890123456789012345'; + const codeChallenge = codeVerifier; + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + codeChallenge, + codeChallengeMethod: 'plain', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const approvalResponse = await request(server) + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) + .expect(200); + + const redirectUrl = new URL(approvalResponse.body.redirectUrl); + const authorizationCode = redirectUrl.searchParams.get('code'); + + expect(authorizationCode).toBeDefined(); + + const tokenResponse = await request(server) + .post('/api/auth/v1/token') + .send({ + grant_type: 'authorization_code', + code: authorizationCode, + redirect_uri: 'https://example.com/callback', + code_verifier: codeVerifier, + }) + .expect(200); + + expect(tokenResponse.body).toEqual({ + access_token: 'mock-access-token-pkce', + token_type: 'Bearer', + expires_in: 3600, + id_token: 'mock-access-token-pkce', + scope: 'openid', + }); + + expect(tokenIssuer.issueToken).toHaveBeenCalledWith({ + claims: { + sub: 'user:default/test-user-pkce', + }, + }); + }); + + it('should reject token exchange with invalid authorization code', async () => { + const { router } = await createRouter(databaseId); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const tokenResponse = await request(server) + .post('/api/auth/v1/token') + .send({ + grant_type: 'authorization_code', + code: 'invalid-code', + redirect_uri: 'https://example.com/callback', + }) + .expect(401); + + expect(tokenResponse.body).toEqual({ + error: 'invalid_client', + error_description: 'Invalid authorization code', + }); + }); + + it('should exchange authorization code for tokens with PKCE S256', async () => { + const { + mocks: { auth, service, tokenIssuer, httpAuth }, + router, + } = await createRouter(databaseId); + + tokenIssuer.issueToken.mockResolvedValue({ + token: 'mock-access-token-s256', + }); + + httpAuth.credentials.mockResolvedValueOnce( + mockCredentials.user('user:default/test-user-s256'), + ); + + auth.isPrincipal.mockReturnValueOnce(true); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const codeVerifier = + 'test-code-verifier-s256-123456789012345678901234567890123456789'; + const codeChallenge = crypto + .createHash('sha256') + .update(codeVerifier) + .digest('base64url'); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + codeChallenge, + codeChallengeMethod: 'S256', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const approvalResponse = await request(server) + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) + .expect(200); + + const redirectUrl = new URL(approvalResponse.body.redirectUrl); + const authorizationCode = redirectUrl.searchParams.get('code'); + + expect(authorizationCode).toBeDefined(); + + const tokenResponse = await request(server) + .post('/api/auth/v1/token') + .send({ + grant_type: 'authorization_code', + code: authorizationCode, + redirect_uri: 'https://example.com/callback', + code_verifier: codeVerifier, + }) + .expect(200); + + expect(tokenResponse.body).toEqual({ + access_token: 'mock-access-token-s256', + token_type: 'Bearer', + expires_in: 3600, + id_token: 'mock-access-token-s256', + scope: 'openid', + }); + + expect(tokenIssuer.issueToken).toHaveBeenCalledWith({ + claims: { + sub: 'user:default/test-user-s256', + }, + }); + }); }); }); }); diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index 6ada071c44..9a3308b4eb 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -15,42 +15,72 @@ */ import Router from 'express-promise-router'; import { OidcService } from './OidcService'; -import { AuthenticationError } from '@backstage/errors'; -import { AuthService } from '@backstage/backend-plugin-api'; +import { AuthenticationError, isError } from '@backstage/errors'; +import { + AuthService, + HttpAuthService, + LoggerService, + RootConfigService, +} from '@backstage/backend-plugin-api'; import { TokenIssuer } from '../identity/types'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; +import { OidcDatabase } from '../database/OidcDatabase'; +import { json } from 'express'; export class OidcRouter { - private constructor(private readonly oidc: OidcService) {} + private constructor( + private readonly oidc: OidcService, + private readonly logger: LoggerService, + private readonly auth: AuthService, + private readonly appUrl: string, + private readonly httpAuth: HttpAuthService, + private readonly config: RootConfigService, + ) {} static create(options: { auth: AuthService; tokenIssuer: TokenIssuer; baseUrl: string; + appUrl: string; + logger: LoggerService; userInfo: UserInfoDatabase; + oidc: OidcDatabase; + httpAuth: HttpAuthService; + config: RootConfigService; }) { - return new OidcRouter(OidcService.create(options)); + return new OidcRouter( + OidcService.create(options), + options.logger, + options.auth, + options.appUrl, + options.httpAuth, + options.config, + ); } public getRouter() { const router = Router(); + router.use(json()); + + // OpenID Provider Configuration endpoint + // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig + // Returns the OpenID Provider Configuration document containing metadata about the provider router.get('/.well-known/openid-configuration', (_req, res) => { res.json(this.oidc.getConfiguration()); }); + // JSON Web Key Set endpoint + // https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.10.1.1 + // Returns the public keys used to verify JWTs issued by this provider router.get('/.well-known/jwks.json', async (_req, res) => { const { keys } = await this.oidc.listPublicKeys(); res.json({ keys }); }); - router.get('/v1/token', (_req, res) => { - res.status(501).send('Not Implemented'); - }); - - // This endpoint doesn't use the regular HttpAuthoidc, since the contract - // is specifically for the header to be communicated in the Authorization - // header, regardless of token type + // UserInfo endpoint + // https://openid.net/specs/openid-connect-core-1_0.html#UserInfo + // Returns claims about the authenticated user using an access token router.get('/v1/userinfo', async (req, res) => { const matches = req.headers.authorization?.match(/^Bearer[ ]+(\S+)$/i); const token = matches?.[1]; @@ -68,6 +98,337 @@ export class OidcRouter { res.json(userInfo); }); + if ( + this.config.getOptionalBoolean( + 'auth.experimentalDynamicClientRegistration.enabled', + ) + ) { + // Authorization endpoint + // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + // Handles the initial authorization request from the client, validates parameters, + // and redirects to the Authorization Session page for user approval + router.get('/v1/authorize', async (req, res) => { + // todo(blam): maybe add zod types for validating input + const { + client_id: clientId, + redirect_uri: redirectUri, + response_type: responseType, + scope, + state, + nonce, + code_challenge: codeChallenge, + code_challenge_method: codeChallengeMethod, + } = req.query; + + if (!clientId || !redirectUri || !responseType) { + this.logger.error(`Failed to authorize: Missing required parameters`); + return res.status(400).json({ + error: 'invalid_request', + error_description: + 'Missing required parameters: client_id, redirect_uri, response_type', + }); + } + + try { + const result = await this.oidc.createAuthorizationSession({ + clientId: clientId as string, + redirectUri: redirectUri as string, + responseType: responseType as string, + scope: scope as string | undefined, + state: state as string | undefined, + nonce: nonce as string | undefined, + codeChallenge: codeChallenge as string | undefined, + codeChallengeMethod: codeChallengeMethod as string | undefined, + }); + + // todo(blam): maybe this URL could be overridable by config if + // the plugin is mounted somewhere else? + // support slashes in baseUrl? + const authSessionRedirectUrl = new URL( + `./oauth2/authorize/${result.id}`, + ensureTrailingSlash(this.appUrl), + ); + + return res.redirect(authSessionRedirectUrl.toString()); + } catch (error) { + const errorParams = new URLSearchParams(); + errorParams.append( + 'error', + isError(error) ? error.name : 'server_error', + ); + errorParams.append( + 'error_description', + isError(error) ? error.message : 'Unknown error', + ); + if (state) { + errorParams.append('state', state as string); + } + + const redirectUrl = new URL(redirectUri as string); + redirectUrl.search = errorParams.toString(); + return res.redirect(redirectUrl.toString()); + } + }); + + // Authorization Session request details endpoint + // Returns Authorization Session request details for the frontend + router.get('/v1/sessions/:sessionId', async (req, res) => { + const { sessionId } = req.params; + + if (!sessionId) { + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing Authorization Session ID', + }); + } + + try { + const session = await this.oidc.getAuthorizationSession({ + sessionId, + }); + + return res.json({ + id: session.id, + clientName: session.clientName, + scope: session.scope, + redirectUri: session.redirectUri, + }); + } catch (error) { + const description = isError(error) ? error.message : 'Unknown error'; + this.logger.error( + `Failed to get authorization session: ${description}`, + error, + ); + return res.status(404).json({ + error: 'not_found', + error_description: description, + }); + } + }); + + // Authorization Session approval endpoint + // Handles user approval of Authorization Session requests and generates authorization codes + router.post('/v1/sessions/:sessionId/approve', async (req, res) => { + const { sessionId } = req.params; + + if (!sessionId) { + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing authorization session ID', + }); + } + + try { + const httpCredentials = await this.httpAuth.credentials(req); + + if (!this.auth.isPrincipal(httpCredentials, 'user')) { + return res.status(401).json({ + error: 'unauthorized', + error_description: 'Authentication required', + }); + } + + const { userEntityRef } = httpCredentials.principal; + + const result = await this.oidc.approveAuthorizationSession({ + sessionId, + userEntityRef, + }); + + return res.json({ + redirectUrl: result.redirectUrl, + }); + } catch (error) { + const description = isError(error) ? error.message : 'Unknown error'; + this.logger.error( + `Failed to approve authorization session: ${description}`, + error, + ); + return res.status(400).json({ + error: 'invalid_request', + error_description: description, + }); + } + }); + + // Authorization Session rejection endpoint + // Handles user rejection of Authorization Session requests and redirects with error + router.post('/v1/sessions/:sessionId/reject', async (req, res) => { + const { sessionId } = req.params; + + if (!sessionId) { + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing authorization session ID', + }); + } + + const httpCredentials = await this.httpAuth.credentials(req); + + if (!this.auth.isPrincipal(httpCredentials, 'user')) { + return res.status(401).json({ + error: 'unauthorized', + error_description: 'Authentication required', + }); + } + + const { userEntityRef } = httpCredentials.principal; + try { + const session = await this.oidc.getAuthorizationSession({ + sessionId, + }); + + await this.oidc.rejectAuthorizationSession({ + sessionId, + userEntityRef, + }); + + const errorParams = new URLSearchParams(); + errorParams.append('error', 'access_denied'); + errorParams.append('error_description', 'User denied the request'); + if (session.state) { + errorParams.append('state', session.state); + } + + const redirectUrl = new URL(session.redirectUri); + redirectUrl.search = errorParams.toString(); + + return res.json({ + redirectUrl: redirectUrl.toString(), + }); + } catch (error) { + const description = isError(error) ? error.message : 'Unknown error'; + this.logger.error( + `Failed to reject authorization session: ${description}`, + error, + ); + + return res.status(400).json({ + error: 'invalid_request', + error_description: description, + }); + } + }); + + // Token endpoint + // https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest + // Exchanges authorization codes for access tokens and ID tokens + router.post('/v1/token', async (req, res) => { + // todo(blam): maybe add zod types for validating input + const { + grant_type: grantType, + code, + redirect_uri: redirectUri, + code_verifier: codeVerifier, + } = req.body; + + if (!grantType || !code || !redirectUri) { + this.logger.error( + `Failed to exchange code for token: Missing required parameters`, + ); + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing required parameters', + }); + } + + try { + const result = await this.oidc.exchangeCodeForToken({ + code, + redirectUri, + codeVerifier, + grantType, + }); + + return res.json({ + access_token: result.accessToken, + token_type: result.tokenType, + expires_in: result.expiresIn, + id_token: result.idToken, + scope: result.scope, + }); + } catch (error) { + const description = isError(error) ? error.message : 'Unknown error'; + this.logger.error( + `Failed to exchange code for token: ${description}`, + error, + ); + + if (isError(error)) { + if (error.name === 'AuthenticationError') { + return res.status(401).json({ + error: 'invalid_client', + error_description: error.message, + }); + } + if (error.name === 'InputError') { + return res.status(400).json({ + error: 'invalid_request', + error_description: error.message, + }); + } + } + + return res.status(500).json({ + error: 'server_error', + error_description: description, + }); + } + }); + + // Dynamic Client Registration endpoint + // https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration + // Allows clients to register themselves dynamically with the provider + router.post('/v1/register', async (req, res) => { + // todo(blam): maybe add zod types for validating input + const { + client_name: clientName, + redirect_uris: redirectUris, + response_types: responseTypes, + grant_types: grantTypes, + scope, + } = req.body; + + if (!redirectUris?.length) { + res.status(400).json({ + error: 'invalid_request', + error_description: 'redirect_uris is required', + }); + return; + } + + try { + const client = await this.oidc.registerClient({ + clientName, + redirectUris, + responseTypes, + grantTypes, + scope, + }); + + res.status(201).json({ + client_id: client.clientId, + redirect_uris: client.redirectUris, + client_secret: client.clientSecret, + }); + } catch (e) { + const description = isError(e) ? e.message : 'Unknown error'; + this.logger.error(`Failed to register client: ${description}`, e); + + res.status(500).json({ + error: 'server_error', + error_description: `Failed to register client: ${description}`, + }); + } + }); + } + return router; } } +function ensureTrailingSlash(appUrl: string): string | URL | undefined { + if (appUrl.endsWith('/')) { + return appUrl; + } + return `${appUrl}/`; +} diff --git a/plugins/auth-backend/src/service/OidcService.test.ts b/plugins/auth-backend/src/service/OidcService.test.ts new file mode 100644 index 0000000000..e4e1673397 --- /dev/null +++ b/plugins/auth-backend/src/service/OidcService.test.ts @@ -0,0 +1,789 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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, + TestDatabaseId, + TestDatabases, +} from '@backstage/backend-test-utils'; +import { OidcService } from './OidcService'; +import { + BackstageCredentials, + BackstageServicePrincipal, + BackstageUserPrincipal, + resolvePackagePath, +} from '@backstage/backend-plugin-api'; +import { AuthDatabase } from '../database/AuthDatabase'; +import { OidcDatabase } from '../database/OidcDatabase'; +import { UserInfoDatabase } from '../database/UserInfoDatabase'; +import crypto from 'crypto'; +import { AnyJWK, TokenIssuer } from '../identity/types'; + +jest.setTimeout(60_000); + +describe('OidcService', () => { + const databases = TestDatabases.create(); + + async function createOidcService(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + + await knex.migrate.latest({ + directory: resolvePackagePath( + '@backstage/plugin-auth-backend', + 'migrations', + ), + }); + + const oidcDatabase = await OidcDatabase.create({ + database: AuthDatabase.create({ + getClient: async () => knex, + }), + }); + + const mockAuth = mockServices.auth.mock(); + const mockTokenIssuer = { + issueToken: jest.fn(), + listPublicKeys: jest.fn(), + } as jest.Mocked; + + const mockUserInfo = { + addUserInfo: jest.fn(), + getUserInfo: jest.fn(), + } as unknown as jest.Mocked; + + const mockConfig = mockServices.rootConfig.mock(); + + return { + service: OidcService.create({ + auth: mockAuth, + tokenIssuer: mockTokenIssuer, + baseUrl: 'http://mock-base-url', + userInfo: mockUserInfo, + oidc: oidcDatabase, + config: mockConfig, + }), + mocks: { + auth: mockAuth, + tokenIssuer: mockTokenIssuer, + userInfo: mockUserInfo, + config: mockConfig, + }, + }; + } + + describe.each(databases.eachSupportedId())('%p', databaseId => { + describe('getConfiguration', () => { + it('should return OIDC configuration', async () => { + const { service } = await createOidcService(databaseId); + + const config = service.getConfiguration(); + + expect(config).toEqual({ + issuer: 'http://mock-base-url', + token_endpoint: 'http://mock-base-url/v1/token', + userinfo_endpoint: 'http://mock-base-url/v1/userinfo', + jwks_uri: 'http://mock-base-url/.well-known/jwks.json', + response_types_supported: ['code', 'id_token'], + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: [ + 'RS256', + 'RS384', + 'RS512', + 'ES256', + 'ES384', + 'ES512', + 'PS256', + 'PS384', + 'PS512', + 'EdDSA', + ], + scopes_supported: ['openid'], + token_endpoint_auth_methods_supported: [ + 'client_secret_basic', + 'client_secret_post', + ], + claims_supported: ['sub', 'ent'], + grant_types_supported: ['authorization_code'], + authorization_endpoint: 'http://mock-base-url/v1/authorize', + registration_endpoint: 'http://mock-base-url/v1/register', + code_challenge_methods_supported: ['S256', 'plain'], + }); + }); + }); + + describe('listPublicKeys', () => { + it('should return public keys from token issuer', async () => { + const { service, mocks } = await createOidcService(databaseId); + const mockKeys = [{ kid: 'key-1', use: 'sig' }] as AnyJWK[]; + mocks.tokenIssuer.listPublicKeys.mockResolvedValue({ keys: mockKeys }); + + const { keys } = await service.listPublicKeys(); + + expect(keys).toEqual(mockKeys); + expect(mocks.tokenIssuer.listPublicKeys).toHaveBeenCalledTimes(1); + }); + }); + + describe('getUserInfo', () => { + it('should return user info for valid token', async () => { + const { service, mocks } = await createOidcService(databaseId); + const mockCredentials: BackstageCredentials = { + principal: { + type: 'user', + userEntityRef: 'user:default/test', + }, + $$type: '@backstage/BackstageCredentials', + }; + const mockUserInfo = { sub: 'user:default/test', name: 'Test User' }; + + mocks.auth.authenticate.mockResolvedValue(mockCredentials); + mocks.auth.isPrincipal.mockReturnValue(true); + mocks.userInfo.getUserInfo.mockResolvedValue({ + claims: mockUserInfo, + }); + + const mockToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvdGVzdCJ9.signature'; + + const userInfo = await service.getUserInfo({ token: mockToken }); + + expect(userInfo).toEqual({ + claims: mockUserInfo, + }); + + expect(mocks.auth.authenticate).toHaveBeenCalledWith(mockToken, { + allowLimitedAccess: true, + }); + + expect(mocks.userInfo.getUserInfo).toHaveBeenCalledWith( + 'user:default/test', + ); + }); + + it('should throw error for non-user principal', async () => { + const { service, mocks } = await createOidcService(databaseId); + const mockCredentials: BackstageCredentials = + { + principal: { + type: 'service', + subject: 'test-service', + }, + $$type: '@backstage/BackstageCredentials', + }; + + mocks.auth.authenticate.mockResolvedValue(mockCredentials); + mocks.auth.isPrincipal.mockReturnValue(false); + + const mockToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvdGVzdCJ9.signature'; + + await expect(service.getUserInfo({ token: mockToken })).rejects.toThrow( + 'Userinfo endpoint must be called with a token that represents a user principal', + ); + }); + }); + + describe('registerClient', () => { + it('should create a new client with generated credentials', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + expect(client).toEqual( + expect.objectContaining({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }), + ); + expect(client.clientId).toBeDefined(); + expect(client.clientSecret).toBeDefined(); + }); + + it('should throw an error for invalid redirect URI', async () => { + const { + service, + mocks: { config }, + } = await createOidcService(databaseId); + + config.getOptionalStringArray.mockReturnValue([ + 'https://example.com/*', + ]); + + await expect( + service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://invalid.com/callback'], + }), + ).rejects.toThrow('Invalid redirect_uri'); + }); + + it('should create a new client with valid redirect URI', async () => { + const { + service, + mocks: { config }, + } = await createOidcService(databaseId); + + config.getOptionalStringArray.mockReturnValue(['cursor:*']); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['cursor://callback/asd?asd=asd'], + }); + + expect(client).toEqual( + expect.objectContaining({ + redirectUris: ['cursor://callback/asd?asd=asd'], + }), + ); + }); + + it('should create a client with default values', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + }); + + expect(client).toEqual( + expect.objectContaining({ + clientName: 'Test Client', + redirectUris: [], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }), + ); + }); + }); + + describe('createAuthorizationSession', () => { + it('should create a authorization session for valid client', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + expect(authSession).toEqual({ + id: expect.any(String), + clientName: 'Test Client', + scope: 'openid', + redirectUri: 'https://example.com/callback', + }); + }); + + it('should throw error for invalid client', async () => { + const { service } = await createOidcService(databaseId); + + await expect( + service.createAuthorizationSession({ + clientId: 'invalid-client', + redirectUri: 'https://example.com/callback', + responseType: 'code', + }), + ).rejects.toThrow('Invalid client_id'); + }); + + it('should throw error for invalid redirect URI', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + await expect( + service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://invalid.com/callback', + responseType: 'code', + }), + ).rejects.toThrow('Invalid redirect_uri'); + }); + + it('should throw error for unsupported response type', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + await expect( + service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'token', + }), + ).rejects.toThrow('Only authorization code flow is supported'); + }); + + it('should handle PKCE parameters', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + codeChallenge: 'test-challenge', + codeChallengeMethod: 'S256', + }); + + expect(authSession.id).toBeDefined(); + }); + + it('should throw error for invalid PKCE method', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + await expect( + service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + codeChallenge: 'test-challenge', + codeChallengeMethod: 'invalid', + }), + ).rejects.toThrow('Invalid code_challenge_method'); + }); + }); + + describe('approveAuthorizationSession', () => { + it('should approve a valid authorization session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + state: 'test-state', + }); + + const result = await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + expect(result.redirectUrl).toMatch( + /^https:\/\/example\.com\/callback\?code=.+&state=test-state$/, + ); + }); + + it('should throw error for invalid authorization session', async () => { + const { service } = await createOidcService(databaseId); + + await expect( + service.approveAuthorizationSession({ + sessionId: 'invalid-session', + userEntityRef: 'user:default/test', + }), + ).rejects.toThrow('Invalid authorization session'); + }); + + it('should throw error when trying to approve an already approved session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + await expect( + service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); + + it('should throw error when trying to approve an already rejected session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.rejectAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + await expect( + service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); + }); + + describe('getAuthorizationSession', () => { + it('should return authorization session details', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + const details = await service.getAuthorizationSession({ + sessionId: authSession.id, + }); + + expect(details).toEqual( + expect.objectContaining({ + id: authSession.id, + clientId: client.clientId, + clientName: 'Test Client', + redirectUri: 'https://example.com/callback', + scope: 'openid', + state: 'test-state', + responseType: 'code', + }), + ); + }); + + it('should throw error when trying to get an already approved session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + await expect( + service.getAuthorizationSession({ + sessionId: authSession.id, + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); + + it('should throw error when trying to get an already rejected session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.rejectAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + await expect( + service.getAuthorizationSession({ + sessionId: authSession.id, + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); + }); + + describe('rejectAuthorizationSession', () => { + it('should reject a authorization session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.rejectAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + await expect( + service.getAuthorizationSession({ + sessionId: authSession.id, + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); + + it('should throw error for invalid authorization session', async () => { + const { service } = await createOidcService(databaseId); + + await expect( + service.rejectAuthorizationSession({ + sessionId: 'invalid-session', + userEntityRef: 'user:default/test', + }), + ).rejects.toThrow('Invalid authorization session'); + }); + + it('should throw error when trying to reject an already approved session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + await expect( + service.rejectAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); + + it('should throw error when trying to reject an already rejected session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.rejectAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + await expect( + service.rejectAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); + }); + + describe('exchangeCodeForToken', () => { + it('should exchange valid code for tokens', async () => { + const { service, mocks } = await createOidcService(databaseId); + const mockToken = 'mock-jwt-token'; + mocks.tokenIssuer.issueToken.mockResolvedValue({ token: mockToken }); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + }); + + const authResult = await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + const code = new URL(authResult.redirectUrl).searchParams.get('code')!; + + const tokenResult = await service.exchangeCodeForToken({ + code, + redirectUri: 'https://example.com/callback', + grantType: 'authorization_code', + }); + + expect(tokenResult).toEqual({ + accessToken: mockToken, + tokenType: 'Bearer', + expiresIn: 3600, + idToken: mockToken, + scope: 'openid', + }); + }); + + it('should throw error for invalid grant type', async () => { + const { service } = await createOidcService(databaseId); + + await expect( + service.exchangeCodeForToken({ + code: 'test-code', + redirectUri: 'https://example.com/callback', + grantType: 'client_credentials', + }), + ).rejects.toThrow('Unsupported grant type'); + }); + + it('should handle PKCE verification', async () => { + const { service, mocks } = await createOidcService(databaseId); + const mockToken = 'mock-jwt-token'; + mocks.tokenIssuer.issueToken.mockResolvedValue({ token: mockToken }); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const codeVerifier = 'test-code-verifier'; + const codeChallenge = crypto + .createHash('sha256') + .update(codeVerifier) + .digest('base64url'); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + codeChallenge, + codeChallengeMethod: 'S256', + }); + + const authResult = await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + const code = new URL(authResult.redirectUrl).searchParams.get('code')!; + + const tokenResult = await service.exchangeCodeForToken({ + code, + redirectUri: 'https://example.com/callback', + grantType: 'authorization_code', + codeVerifier, + }); + + expect(tokenResult.accessToken).toBe(mockToken); + }); + + it('should throw error for invalid PKCE verifier', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const codeChallenge = 'test-challenge'; + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + codeChallenge, + codeChallengeMethod: 'S256', + }); + + const authResult = await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + const code = new URL(authResult.redirectUrl).searchParams.get('code')!; + + await expect( + service.exchangeCodeForToken({ + code, + redirectUri: 'https://example.com/callback', + grantType: 'authorization_code', + codeVerifier: 'invalid-verifier', + }), + ).rejects.toThrow('Invalid code verifier'); + }); + }); + }); +}); diff --git a/plugins/auth-backend/src/service/OidcService.ts b/plugins/auth-backend/src/service/OidcService.ts index 5024b2cc8c..b4c6bb122b 100644 --- a/plugins/auth-backend/src/service/OidcService.ts +++ b/plugins/auth-backend/src/service/OidcService.ts @@ -13,11 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AuthService } from '@backstage/backend-plugin-api'; +import { AuthService, RootConfigService } from '@backstage/backend-plugin-api'; import { TokenIssuer } from '../identity/types'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; -import { InputError } from '@backstage/errors'; +import { + InputError, + AuthenticationError, + NotFoundError, +} from '@backstage/errors'; import { decodeJwt } from 'jose'; +import crypto from 'crypto'; +import { OidcDatabase } from '../database/OidcDatabase'; +import { DateTime } from 'luxon'; +import matcher from 'matcher'; export class OidcService { private constructor( @@ -25,6 +33,8 @@ export class OidcService { private readonly tokenIssuer: TokenIssuer, private readonly baseUrl: string, private readonly userInfo: UserInfoDatabase, + private readonly oidc: OidcDatabase, + private readonly config: RootConfigService, ) {} static create(options: { @@ -32,12 +42,16 @@ export class OidcService { tokenIssuer: TokenIssuer; baseUrl: string; userInfo: UserInfoDatabase; + oidc: OidcDatabase; + config: RootConfigService; }) { return new OidcService( options.auth, options.tokenIssuer, options.baseUrl, options.userInfo, + options.oidc, + options.config, ); } @@ -47,7 +61,7 @@ export class OidcService { token_endpoint: `${this.baseUrl}/v1/token`, userinfo_endpoint: `${this.baseUrl}/v1/userinfo`, jwks_uri: `${this.baseUrl}/.well-known/jwks.json`, - response_types_supported: ['id_token'], + response_types_supported: ['code', 'id_token'], subject_types_supported: ['public'], id_token_signing_alg_values_supported: [ 'RS256', @@ -62,9 +76,15 @@ export class OidcService { 'EdDSA', ], scopes_supported: ['openid'], - token_endpoint_auth_methods_supported: [], + token_endpoint_auth_methods_supported: [ + 'client_secret_basic', + 'client_secret_post', + ], claims_supported: ['sub', 'ent'], - grant_types_supported: [], + grant_types_supported: ['authorization_code'], + authorization_endpoint: `${this.baseUrl}/v1/authorize`, + registration_endpoint: `${this.baseUrl}/v1/register`, + code_challenge_methods_supported: ['S256', 'plain'], }; } @@ -89,4 +109,321 @@ export class OidcService { } return await this.userInfo.getUserInfo(userEntityRef); } + + public async registerClient(opts: { + responseTypes?: string[]; + grantTypes?: string[]; + clientName: string; + redirectUris?: string[]; + scope?: string; + }) { + const generatedClientId = crypto.randomUUID(); + const generatedClientSecret = crypto.randomUUID(); + + const allowedRedirectUriPatterns = this.config.getOptionalStringArray( + 'auth.experimentalDynamicClientRegistration.allowedRedirectUriPatterns', + ) ?? ['*']; + + for (const redirectUri of opts.redirectUris ?? []) { + if ( + !allowedRedirectUriPatterns.some(pattern => + matcher.isMatch(redirectUri, pattern), + ) + ) { + throw new InputError('Invalid redirect_uri'); + } + } + + return await this.oidc.createClient({ + clientId: generatedClientId, + clientName: opts.clientName, + clientSecret: generatedClientSecret, + redirectUris: opts.redirectUris ?? [], + responseTypes: opts.responseTypes ?? ['code'], + grantTypes: opts.grantTypes ?? ['authorization_code'], + scope: opts.scope, + }); + } + + public async createAuthorizationSession(opts: { + clientId: string; + redirectUri: string; + responseType: string; + scope?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + }) { + const { + clientId, + redirectUri, + responseType, + scope, + state, + nonce, + codeChallenge, + codeChallengeMethod, + } = opts; + + if (responseType !== 'code') { + throw new InputError('Only authorization code flow is supported'); + } + + const client = await this.oidc.getClient({ clientId }); + if (!client) { + throw new InputError('Invalid client_id'); + } + + if (!client.redirectUris.includes(redirectUri)) { + throw new InputError('Invalid redirect_uri'); + } + + if (codeChallenge) { + if ( + !codeChallengeMethod || + !['S256', 'plain'].includes(codeChallengeMethod) + ) { + throw new InputError('Invalid code_challenge_method'); + } + } + + const sessionId = crypto.randomUUID(); + const sessionExpiresAt = DateTime.now().plus({ hours: 1 }).toJSDate(); + + await this.oidc.createAuthorizationSession({ + id: sessionId, + clientId, + redirectUri, + responseType, + scope, + state, + codeChallenge, + codeChallengeMethod, + nonce, + expiresAt: sessionExpiresAt, + }); + + return { + id: sessionId, + clientName: client.clientName, + scope, + redirectUri, + }; + } + + public async approveAuthorizationSession(opts: { + sessionId: string; + userEntityRef: string; + }) { + const { sessionId, userEntityRef } = opts; + + const session = await this.oidc.getAuthorizationSession({ + id: sessionId, + }); + + if (!session) { + throw new NotFoundError('Invalid authorization session'); + } + + if (DateTime.fromJSDate(session.expiresAt) < DateTime.now()) { + throw new InputError('Authorization session expired'); + } + + if (session.status !== 'pending') { + throw new NotFoundError('Authorization session not found or expired'); + } + + await this.oidc.updateAuthorizationSession({ + id: session.id, + userEntityRef, + status: 'approved', + }); + + const authorizationCode = crypto.randomBytes(32).toString('base64url'); + const codeExpiresAt = DateTime.now().plus({ minutes: 10 }).toJSDate(); + + await this.oidc.createAuthorizationCode({ + code: authorizationCode, + sessionId: session.id, + expiresAt: codeExpiresAt, + }); + + const redirectUrl = new URL(session.redirectUri); + + redirectUrl.searchParams.append('code', authorizationCode); + if (session.state) { + redirectUrl.searchParams.append('state', session.state); + } + + return { + redirectUrl: redirectUrl.toString(), + }; + } + + public async getAuthorizationSession(opts: { sessionId: string }) { + const session = await this.oidc.getAuthorizationSession({ + id: opts.sessionId, + }); + + if (!session) { + throw new NotFoundError('Invalid authorization session'); + } + + if (DateTime.fromJSDate(session.expiresAt) < DateTime.now()) { + throw new InputError('Authorization session expired'); + } + + if (session.status !== 'pending') { + throw new NotFoundError('Authorization session not found or expired'); + } + + const client = await this.oidc.getClient({ clientId: session.clientId }); + if (!client) { + throw new InputError('Invalid client_id'); + } + + return { + id: session.id, + clientId: session.clientId, + clientName: client.clientName, + redirectUri: session.redirectUri, + scope: session.scope, + state: session.state, + responseType: session.responseType, + codeChallenge: session.codeChallenge, + codeChallengeMethod: session.codeChallengeMethod, + nonce: session.nonce, + expiresAt: session.expiresAt, + status: session.status, + }; + } + + public async rejectAuthorizationSession(opts: { + sessionId: string; + userEntityRef: string; + }) { + const { sessionId, userEntityRef } = opts; + + const session = await this.oidc.getAuthorizationSession({ + id: sessionId, + }); + + if (!session) { + throw new NotFoundError('Invalid authorization session'); + } + + if (DateTime.fromJSDate(session.expiresAt) < DateTime.now()) { + throw new InputError('Authorization session expired'); + } + + if (session.status !== 'pending') { + throw new NotFoundError('Authorization session not found or expired'); + } + + await this.oidc.updateAuthorizationSession({ + id: session.id, + status: 'rejected', + userEntityRef, + }); + } + + public async exchangeCodeForToken(params: { + code: string; + redirectUri: string; + codeVerifier?: string; + grantType: string; + }) { + const { code, redirectUri, codeVerifier, grantType } = params; + + if (grantType !== 'authorization_code') { + throw new InputError('Unsupported grant type'); + } + + const authCode = await this.oidc.getAuthorizationCode({ code }); + if (!authCode) { + throw new AuthenticationError('Invalid authorization code'); + } + + if (DateTime.fromJSDate(authCode.expiresAt) < DateTime.now()) { + throw new AuthenticationError('Authorization code expired'); + } + + if (authCode.used) { + throw new AuthenticationError('Authorization code already used'); + } + + const session = await this.oidc.getAuthorizationSession({ + id: authCode.sessionId, + }); + + if (!session) { + throw new NotFoundError('Invalid authorization session'); + } + + if (session.redirectUri !== redirectUri) { + throw new AuthenticationError('Redirect URI mismatch'); + } + + if (session.status !== 'approved') { + throw new AuthenticationError('Authorization not approved'); + } + + if (!session.userEntityRef) { + throw new AuthenticationError('No user associated with authorization'); + } + + if (session.codeChallenge) { + if (!codeVerifier) { + throw new AuthenticationError('Code verifier required for PKCE'); + } + + if ( + !this.verifyPkce( + session.codeChallenge, + codeVerifier, + session.codeChallengeMethod, + ) + ) { + throw new AuthenticationError('Invalid code verifier'); + } + } + + await this.oidc.updateAuthorizationCode({ + code, + used: true, + }); + + const { token } = await this.tokenIssuer.issueToken({ + claims: { + sub: session.userEntityRef, + }, + }); + + return { + accessToken: token, + tokenType: 'Bearer', + expiresIn: 3600, + idToken: token, + scope: session.scope || 'openid', + }; + } + + private verifyPkce( + codeChallenge: string, + codeVerifier: string, + method?: string, + ): boolean { + if (!method || method === 'plain') { + return codeChallenge === codeVerifier; + } + + if (method === 'S256') { + const hash = crypto.createHash('sha256').update(codeVerifier).digest(); + const base64urlHash = hash.toString('base64url'); + return codeChallenge === base64urlHash; + } + + return false; + } } diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 5012c90106..0f0d5b2830 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -21,6 +21,7 @@ import { AuthService, DatabaseService, DiscoveryService, + HttpAuthService, LoggerService, RootConfigService, } from '@backstage/backend-plugin-api'; @@ -40,6 +41,7 @@ import { StaticTokenIssuer } from '../identity/StaticTokenIssuer'; import { StaticKeyStore } from '../identity/StaticKeyStore'; import { bindProviderRouters, ProviderFactories } from '../providers/router'; import { OidcRouter } from './OidcRouter'; +import { OidcDatabase } from '../database/OidcDatabase'; interface RouterOptions { logger: LoggerService; @@ -51,6 +53,7 @@ interface RouterOptions { providerFactories?: ProviderFactories; catalog: CatalogService; ownershipResolver?: AuthOwnershipResolver; + httpAuth: HttpAuthService; } export async function createRouter( @@ -63,6 +66,7 @@ export async function createRouter( database: db, tokenFactoryAlgorithm, providerFactories = {}, + httpAuth, } = options; const router = Router(); @@ -147,11 +151,18 @@ export async function createRouter( userInfo, }); + const oidc = await OidcDatabase.create({ database }); + const oidcRouter = OidcRouter.create({ auth: options.auth, tokenIssuer, baseUrl: authUrl, + appUrl, userInfo, + oidc, + logger, + httpAuth, + config, }); router.use(oidcRouter.getRouter()); diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index 48554f6932..d784004cd3 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-auth-node +## 0.6.7 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- 3aff9e1: Changes OAuth cookies from domain-scoped to host-only by avoid setting the domain attribute in the default cookie configurer. +- Updated dependencies + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## 0.6.7-next.1 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + ## 0.6.7-next.0 ### Patch Changes diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index 23cff9720c..5c61000f64 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,10 +1,11 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.6.7-next.0", + "version": "0.6.7", "backstage": { "role": "node-library", "pluginId": "auth", "pluginPackages": [ + "@backstage/plugin-auth", "@backstage/plugin-auth-backend", "@backstage/plugin-auth-node", "@backstage/plugin-auth-react" diff --git a/plugins/auth-react/CHANGELOG.md b/plugins/auth-react/CHANGELOG.md index 1dc720ab67..88440f314b 100644 --- a/plugins/auth-react/CHANGELOG.md +++ b/plugins/auth-react/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-auth-react +## 0.1.19 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + +## 0.1.19-next.1 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/core-components@0.17.6-next.1 + ## 0.1.19-next.0 ### Patch Changes diff --git a/plugins/auth-react/knip-report.md b/plugins/auth-react/knip-report.md index df86118651..ed8bd3b071 100644 --- a/plugins/auth-react/knip-report.md +++ b/plugins/auth-react/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-- | :----------- | :------- | -| msw | package.json | error | +| msw | plugins/auth-react/package.json | error | diff --git a/plugins/auth-react/package.json b/plugins/auth-react/package.json index 4335bbc03a..77b01bfdd1 100644 --- a/plugins/auth-react/package.json +++ b/plugins/auth-react/package.json @@ -1,11 +1,12 @@ { "name": "@backstage/plugin-auth-react", - "version": "0.1.19-next.0", + "version": "0.1.19", "description": "Web library for the auth plugin", "backstage": { "role": "web-library", "pluginId": "auth", "pluginPackages": [ + "@backstage/plugin-auth", "@backstage/plugin-auth-backend", "@backstage/plugin-auth-node", "@backstage/plugin-auth-react" diff --git a/plugins/auth/.eslintrc.js b/plugins/auth/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth/CHANGELOG.md b/plugins/auth/CHANGELOG.md new file mode 100644 index 0000000000..7092c0bf3c --- /dev/null +++ b/plugins/auth/CHANGELOG.md @@ -0,0 +1,24 @@ +# @backstage/plugin-auth + +## 0.1.0 + +### Minor Changes + +- 54ddfef: Initial publish of the `auth` frontend package + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-components@0.18.0 + +## 0.1.0-next.0 + +### Minor Changes + +- 54ddfef: Initial publish of the `auth` frontend package + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.17.6-next.1 diff --git a/plugins/auth/README.md b/plugins/auth/README.md new file mode 100644 index 0000000000..440d7d294a --- /dev/null +++ b/plugins/auth/README.md @@ -0,0 +1,16 @@ +# @backstage/plugin-auth + +A Backstage frontend plugin that provides user interface components for authentication flows, specifically for OpenID Connect (OIDC) consent management. + +## Installation + +This plugin is designed to work with the `@backstage/plugin-auth-backend` package that provides OIDC provider functionality. + +```bash +# From your Backstage app directory +yarn --cwd packages/app add @backstage/plugin-auth +``` + +## Usage + +The plugin provides the route `/oauth2/authorize/:sessionId` for approving of oauth2 sessions for clients. You should see an approval flow for any sessions created through the `auth-backend`. diff --git a/plugins/auth/catalog-info.yaml b/plugins/auth/catalog-info.yaml new file mode 100644 index 0000000000..66d835a198 --- /dev/null +++ b/plugins/auth/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth + title: '@backstage/plugin-auth' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: auth-maintainers diff --git a/plugins/auth/dev/index.tsx b/plugins/auth/dev/index.tsx new file mode 100644 index 0000000000..b0762416c6 --- /dev/null +++ b/plugins/auth/dev/index.tsx @@ -0,0 +1,28 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApp } from '@backstage/frontend-defaults'; +import { createRoot } from 'react-dom/client'; + +import plugin from '../src'; + +const app = createApp({ + features: [plugin], +}); + +const container = document.getElementById('root'); +const root = createRoot(container!); +root.render(app.createRoot()); diff --git a/plugins/auth/package.json b/plugins/auth/package.json new file mode 100644 index 0000000000..dffb0eb8ff --- /dev/null +++ b/plugins/auth/package.json @@ -0,0 +1,84 @@ +{ + "name": "@backstage/plugin-auth", + "version": "0.1.0", + "backstage": { + "role": "frontend-plugin", + "pluginId": "auth", + "pluginPackages": [ + "@backstage/plugin-auth", + "@backstage/plugin-auth-backend", + "@backstage/plugin-auth-node", + "@backstage/plugin-auth-react" + ] + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/auth" + }, + "license": "Apache-2.0", + "sideEffects": false, + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + }, + "main": "src/index.ts", + "types": "src/index.ts", + "typesVersions": { + "*": { + "package.json": [ + "package.json" + ] + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/core-components": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.61", + "react-use": "^17.2.4" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/frontend-defaults": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.0.0", + "@types/react": "^18.0.0", + "msw": "^1.0.0", + "react": "^18.0.2", + "react-dom": "^18.0.2", + "react-router-dom": "^6.3.0" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0", + "react-router-dom": "^6.3.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } +} diff --git a/plugins/auth/report.api.md b/plugins/auth/report.api.md new file mode 100644 index 0000000000..4c7d2d4a11 --- /dev/null +++ b/plugins/auth/report.api.md @@ -0,0 +1,52 @@ +## API Report File for "@backstage/plugin-auth" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; +import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; +import { RouteRef } from '@backstage/frontend-plugin-api'; + +// @public (undocumented) +const _default: OverridableFrontendPlugin< + { + root: RouteRef; + }, + {}, + { + 'page:auth': ExtensionDefinition<{ + kind: 'page'; + name: undefined; + config: { + path: string | undefined; + }; + configInput: { + path?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef + | ExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >; + inputs: {}; + params: { + defaultPath?: [Error: `Use the 'path' param instead`]; + path: string; + loader: () => Promise; + routeRef?: RouteRef; + }; + }>; + } +>; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/auth/src/components/ConsentPage/ConsentPage.tsx b/plugins/auth/src/components/ConsentPage/ConsentPage.tsx new file mode 100644 index 0000000000..0f071025a2 --- /dev/null +++ b/plugins/auth/src/components/ConsentPage/ConsentPage.tsx @@ -0,0 +1,246 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useParams } from 'react-router-dom'; + +import { + Box, + Button, + Card, + CardContent, + CardActions, + Typography, + makeStyles, + Divider, +} from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import CheckCircleIcon from '@material-ui/icons/CheckCircle'; +import CancelIcon from '@material-ui/icons/Cancel'; +import AppsIcon from '@material-ui/icons/Apps'; +import WarningIcon from '@material-ui/icons/Warning'; +import { + Header, + Page, + Content, + Progress, + EmptyState, + ResponseErrorPanel, +} from '@backstage/core-components'; +import { useConsentSession } from './useConsentSession'; + +const useStyles = makeStyles(theme => ({ + authCard: { + maxWidth: 600, + margin: '0 auto', + marginTop: theme.spacing(4), + }, + appHeader: { + display: 'flex', + alignItems: 'center', + marginBottom: theme.spacing(2), + }, + appIcon: { + marginRight: theme.spacing(2), + fontSize: 40, + }, + appName: { + fontSize: '1.5rem', + fontWeight: 'bold', + }, + securityWarning: { + margin: theme.spacing(2, 0), + }, + buttonContainer: { + display: 'flex', + justifyContent: 'space-between', + gap: theme.spacing(2), + padding: theme.spacing(2), + }, + callbackUrl: { + fontFamily: 'monospace', + backgroundColor: theme.palette.background.default, + padding: theme.spacing(1), + borderRadius: theme.shape.borderRadius, + wordBreak: 'break-all', + fontSize: '0.875rem', + }, + scopeList: { + backgroundColor: theme.palette.background.default, + borderRadius: theme.shape.borderRadius, + padding: theme.spacing(1), + }, +})); + +const ConsentPageLayout = ({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) => ( + +
+ {children} + +); + +export const ConsentPage = () => { + const classes = useStyles(); + const { sessionId } = useParams<{ sessionId: string }>(); + const { state, handleAction } = useConsentSession({ sessionId }); + + if (!sessionId) { + return ( + + + + ); + } + + if (state.status === 'loading') { + return ( + + + + + + ); + } + + if (state.status === 'error') { + return ( + + + + ); + } + + if (state.status === 'completed') { + return ( + + + + + {state.action === 'approve' ? ( + + ) : ( + + )} + + {state.action === 'approve' + ? 'Authorization Approved' + : 'Authorization Denied'} + + + {state.action === 'approve' + ? 'You have successfully authorized the application to access your Backstage account.' + : 'You have denied the application access to your Backstage account.'} + + + Redirecting to the application... + + + + + + ); + } + + const session = state.session; + const isSubmitting = state.status === 'submitting'; + const appName = session.clientName ?? session.clientId; + + return ( + + + + + + + {appName} + + wants to access your Backstage account + + + + + + + } + className={classes.securityWarning} + > + + Security Notice: By authorizing this application, + you are granting it access to your Backstage account. The + application will receive an access token that allows it to act on + your behalf. + + + + Callback URL: + + {session.redirectUri} + + + + + + Make sure you trust this application and recognize the callback + URL above. Only authorize applications you trust. + + + + + + + + + + + ); +}; diff --git a/plugins/auth/src/components/ConsentPage/index.ts b/plugins/auth/src/components/ConsentPage/index.ts new file mode 100644 index 0000000000..9fdd44c96d --- /dev/null +++ b/plugins/auth/src/components/ConsentPage/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { ConsentPage } from './ConsentPage'; diff --git a/plugins/auth/src/components/ConsentPage/useConsentSession.ts b/plugins/auth/src/components/ConsentPage/useConsentSession.ts new file mode 100644 index 0000000000..a19faae6fe --- /dev/null +++ b/plugins/auth/src/components/ConsentPage/useConsentSession.ts @@ -0,0 +1,144 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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, + alertApiRef, + fetchApiRef, + discoveryApiRef, +} from '@backstage/frontend-plugin-api'; +import { useCallback } from 'react'; +import useAsync from 'react-use/esm/useAsync'; +import useAsyncFn from 'react-use/esm/useAsyncFn'; +import { isError } from '@backstage/errors'; + +interface Session { + id: string; + clientName?: string; + clientId: string; + redirectUri: string; + scopes?: string[]; + responseType?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + expiresAt?: string; +} + +type ConsentState = + | { status: 'loading' } + | { status: 'error'; error: string } + | { status: 'loaded'; session: Session } + | { status: 'submitting'; session: Session; action: 'approve' | 'reject' } + | { status: 'completed'; action: 'approve' | 'reject' }; + +export const useConsentSession = (opts: { sessionId?: string }) => { + const alertApi = useApi(alertApiRef); + const fetchApi = useApi(fetchApiRef); + const discoveryApi = useApi(discoveryApiRef); + const { sessionId } = opts; + + const sessionState = useAsync(async () => { + if (!sessionId) { + throw new Error('Session ID is missing'); + } + + const baseUrl = await discoveryApi.getBaseUrl('auth'); + const response = await fetchApi.fetch( + `${baseUrl}/v1/sessions/${sessionId}`, + ); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return (await response.json()) as Session; + }, [sessionId]); + + const [actionState, handleActionInternal] = useAsyncFn( + async (action: 'approve' | 'reject', session: Session) => { + const baseUrl = await discoveryApi.getBaseUrl('auth'); + const response = await fetchApi.fetch( + `${baseUrl}/v1/sessions/${session.id}/${action}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const result = await response.json(); + + if (result.redirectUrl) { + window.location.href = result.redirectUrl; + } + + return { action, redirectUrl: result.redirectUrl }; + }, + [discoveryApi, fetchApi], + ); + + const getConsentState = (): ConsentState => { + if (actionState.value) { + return { status: 'completed', action: actionState.value.action }; + } + if (actionState.loading && sessionState.value) { + return { + status: 'submitting', + session: sessionState.value, + action: 'approve', // This will be set properly when called + }; + } + if (sessionState.error) { + return { + status: 'error', + error: isError(sessionState.error) + ? sessionState.error.message + : 'Failed to load consent request', + }; + } + if (sessionState.value) { + return { status: 'loaded', session: sessionState.value }; + } + return { status: 'loading' }; + }; + + const state = getConsentState(); + return { + state, + handleAction: useCallback( + async (action: 'approve' | 'reject') => { + if (state.status !== 'loaded') return; + + try { + await handleActionInternal(action, state.session); + } catch (err) { + alertApi.post({ + message: isError(err) ? err.message : `Failed to ${action} consent`, + severity: 'error', + }); + } + }, + [state, handleActionInternal, alertApi], + ), + }; +}; diff --git a/plugins/auth/src/components/Router.tsx b/plugins/auth/src/components/Router.tsx new file mode 100644 index 0000000000..fbb5f9aaa9 --- /dev/null +++ b/plugins/auth/src/components/Router.tsx @@ -0,0 +1,29 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Routes, Route } from 'react-router-dom'; +import { ConsentPage } from './ConsentPage'; + +/** + * Router component for the auth plugin + * @public + */ +export const Router = () => { + return ( + + } /> + + ); +}; diff --git a/plugins/auth/src/index.ts b/plugins/auth/src/index.ts new file mode 100644 index 0000000000..717cdd4672 --- /dev/null +++ b/plugins/auth/src/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { default } from './plugin'; diff --git a/plugins/auth/src/plugin.test.ts b/plugins/auth/src/plugin.test.ts new file mode 100644 index 0000000000..a50b718e5d --- /dev/null +++ b/plugins/auth/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { default as authPlugin } from './plugin'; + +describe('auth', () => { + it('should export plugin', () => { + expect(authPlugin).toBeDefined(); + }); +}); diff --git a/plugins/auth/src/plugin.tsx b/plugins/auth/src/plugin.tsx new file mode 100644 index 0000000000..4ed5d57223 --- /dev/null +++ b/plugins/auth/src/plugin.tsx @@ -0,0 +1,36 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + createFrontendPlugin, + PageBlueprint, +} from '@backstage/frontend-plugin-api'; +import { rootRouteRef } from './routes'; + +export const AuthPage = PageBlueprint.make({ + params: { + path: '/oauth2', + routeRef: rootRouteRef, + loader: () => import('./components/Router').then(m => ), + }, +}); + +export default createFrontendPlugin({ + pluginId: 'auth', + extensions: [AuthPage], + routes: { + root: rootRouteRef, + }, +}); diff --git a/plugins/auth/src/routes.ts b/plugins/auth/src/routes.ts new file mode 100644 index 0000000000..09e6a48da4 --- /dev/null +++ b/plugins/auth/src/routes.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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/frontend-plugin-api'; + +export const rootRouteRef = createRouteRef(); diff --git a/plugins/auth/src/setupTests.ts b/plugins/auth/src/setupTests.ts new file mode 100644 index 0000000000..b57590b525 --- /dev/null +++ b/plugins/auth/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; diff --git a/plugins/bitbucket-cloud-common/CHANGELOG.md b/plugins/bitbucket-cloud-common/CHANGELOG.md index 51cb463738..31345b723c 100644 --- a/plugins/bitbucket-cloud-common/CHANGELOG.md +++ b/plugins/bitbucket-cloud-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-bitbucket-cloud-common +## 0.3.2 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + ## 0.3.2-next.0 ### Patch Changes diff --git a/plugins/bitbucket-cloud-common/knip-report.md b/plugins/bitbucket-cloud-common/knip-report.md index ead5329ef3..1a4ba56b63 100644 --- a/plugins/bitbucket-cloud-common/knip-report.md +++ b/plugins/bitbucket-cloud-common/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :---------------------------------- | :----------- | :------- | -| @openapitools/openapi-generator-cli | package.json | error | -| ts-morph | package.json | error | +| @openapitools/openapi-generator-cli | plugins/bitbucket-cloud-common/package.json | error | +| ts-morph | plugins/bitbucket-cloud-common/package.json | error | diff --git a/plugins/bitbucket-cloud-common/package.json b/plugins/bitbucket-cloud-common/package.json index cece3f6aad..1c72ef31b9 100644 --- a/plugins/bitbucket-cloud-common/package.json +++ b/plugins/bitbucket-cloud-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bitbucket-cloud-common", - "version": "0.3.2-next.0", + "version": "0.3.2", "description": "Common functionalities for bitbucket-cloud plugins", "backstage": { "role": "common-library", diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index 3ba5538fc5..0836bee10d 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.4.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## 0.4.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/integration-aws-node@0.1.17 + ## 0.4.15-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/knip-report.md b/plugins/catalog-backend-module-aws/knip-report.md index c637c25d2f..ed7557334b 100644 --- a/plugins/catalog-backend-module-aws/knip-report.md +++ b/plugins/catalog-backend-module-aws/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :---------------------------- | :----------- | :------- | -| @aws-sdk/credential-providers | package.json | error | -| winston | package.json | error | +| @aws-sdk/credential-providers | plugins/catalog-backend-module-aws/package.json | error | +| winston | plugins/catalog-backend-module-aws/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-aws/package.json | error | diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 9f98b788a7..3f21de8eb1 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", - "version": "0.4.15-next.0", + "version": "0.4.15", "description": "A Backstage catalog backend module that helps integrate towards AWS", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md index 31364b56b3..842d0498cf 100644 --- a/plugins/catalog-backend-module-azure/CHANGELOG.md +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-catalog-backend-module-azure +## 0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## 0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.3.9-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-azure/knip-report.md b/plugins/catalog-backend-module-azure/knip-report.md index cb5315967a..622077aa40 100644 --- a/plugins/catalog-backend-module-azure/knip-report.md +++ b/plugins/catalog-backend-module-azure/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-azure/package.json | error | diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index 51956ca1a5..17c66699a5 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-azure", - "version": "0.3.9-next.0", + "version": "0.3.9", "description": "A Backstage catalog backend module that helps integrate towards Azure", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md index 7f31323ddd..5021408d44 100644 --- a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-backend-module-backstage-openapi +## 0.5.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + +## 0.5.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.5.6-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-backstage-openapi/package.json b/plugins/catalog-backend-module-backstage-openapi/package.json index db55d563de..a17bf50b47 100644 --- a/plugins/catalog-backend-module-backstage-openapi/package.json +++ b/plugins/catalog-backend-module-backstage-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-backstage-openapi", - "version": "0.5.6-next.0", + "version": "0.5.6", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md index edb0a14cf3..45d3c5ed5b 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-catalog-backend-module-bitbucket-cloud +## 0.5.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-bitbucket-cloud-common@0.3.2 + +## 0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.5.3-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-cloud/knip-report.md b/plugins/catalog-backend-module-bitbucket-cloud/knip-report.md index 12b22ec44f..a6400f9cd2 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/knip-report.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/knip-report.md @@ -4,11 +4,11 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @backstage/catalog-client | package.json | error | +| @backstage/catalog-client | plugins/catalog-backend-module-bitbucket-cloud/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-bitbucket-cloud/package.json | error | diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index 56ab813052..68dfbaa619 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-cloud", - "version": "0.5.3-next.0", + "version": "0.5.3", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket Cloud", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md index 42964bad70..1df0cda05b 100644 --- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-catalog-backend-module-bitbucket-server +## 0.5.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## 0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.5.3-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-server/knip-report.md b/plugins/catalog-backend-module-bitbucket-server/knip-report.md index 12b22ec44f..067155bd06 100644 --- a/plugins/catalog-backend-module-bitbucket-server/knip-report.md +++ b/plugins/catalog-backend-module-bitbucket-server/knip-report.md @@ -4,11 +4,11 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @backstage/catalog-client | package.json | error | +| @backstage/catalog-client | plugins/catalog-backend-module-bitbucket-server/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-bitbucket-server/package.json | error | diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index a3cf31ee9a..ce3ec49ae8 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.5.3-next.0", + "version": "0.5.3", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-gcp/CHANGELOG.md b/plugins/catalog-backend-module-gcp/CHANGELOG.md index 9cf3bcb98f..4d06b61dbe 100644 --- a/plugins/catalog-backend-module-gcp/CHANGELOG.md +++ b/plugins/catalog-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-gcp +## 0.3.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/backend-plugin-api@1.4.3 + +## 0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.3.12-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-gcp/knip-report.md b/plugins/catalog-backend-module-gcp/knip-report.md index 14b2b4cc88..6ca9506931 100644 --- a/plugins/catalog-backend-module-gcp/knip-report.md +++ b/plugins/catalog-backend-module-gcp/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---------------------------- | :----------- | :------- | -| @backstage/backend-test-utils | package.json | error | +| @backstage/backend-test-utils | plugins/catalog-backend-module-gcp/package.json | error | diff --git a/plugins/catalog-backend-module-gcp/package.json b/plugins/catalog-backend-module-gcp/package.json index ac095bfc4a..8fb1acd48f 100644 --- a/plugins/catalog-backend-module-gcp/package.json +++ b/plugins/catalog-backend-module-gcp/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gcp", - "version": "0.3.12-next.0", + "version": "0.3.12", "description": "A Backstage catalog backend module that helps integrate towards GCP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gerrit/CHANGELOG.md b/plugins/catalog-backend-module-gerrit/CHANGELOG.md index eaf94c852a..097e4f344f 100644 --- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-catalog-backend-module-gerrit +## 0.3.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## 0.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.3.6-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-gerrit/knip-report.md b/plugins/catalog-backend-module-gerrit/knip-report.md index cb5315967a..7a02c83e07 100644 --- a/plugins/catalog-backend-module-gerrit/knip-report.md +++ b/plugins/catalog-backend-module-gerrit/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-gerrit/package.json | error | diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index 62f17f1b26..1c4d7e8aa7 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.3.6-next.0", + "version": "0.3.6", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-gitea/CHANGELOG.md b/plugins/catalog-backend-module-gitea/CHANGELOG.md index 036dd6b477..3274f6c9f1 100644 --- a/plugins/catalog-backend-module-gitea/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitea/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-catalog-backend-module-gitea +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.1.4-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitea/package.json b/plugins/catalog-backend-module-gitea/package.json index 3aaf419c82..c9dacc7b64 100644 --- a/plugins/catalog-backend-module-gitea/package.json +++ b/plugins/catalog-backend-module-gitea/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitea", - "version": "0.1.4-next.0", + "version": "0.1.4", "license": "Apache-2.0", "description": "The gitea backend module for the catalog plugin.", "main": "src/index.ts", diff --git a/plugins/catalog-backend-module-github-org/CHANGELOG.md b/plugins/catalog-backend-module-github-org/CHANGELOG.md index d6110b1f5a..6198a1942e 100644 --- a/plugins/catalog-backend-module-github-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-github-org/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-backend-module-github-org +## 0.3.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/plugin-catalog-backend-module-github@0.11.0 + - @backstage/backend-plugin-api@1.4.3 + +## 0.3.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-catalog-backend-module-github@0.11.0-next.1 + ## 0.3.14-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-github-org/knip-report.md b/plugins/catalog-backend-module-github-org/knip-report.md index cb5315967a..d71302f8ae 100644 --- a/plugins/catalog-backend-module-github-org/knip-report.md +++ b/plugins/catalog-backend-module-github-org/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-github-org/package.json | error | diff --git a/plugins/catalog-backend-module-github-org/package.json b/plugins/catalog-backend-module-github-org/package.json index 04029f57b9..f9c1f516fb 100644 --- a/plugins/catalog-backend-module-github-org/package.json +++ b/plugins/catalog-backend-module-github-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github-org", - "version": "0.3.14-next.0", + "version": "0.3.14", "description": "The github-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md index b412ef8780..718a0035e8 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-catalog-backend-module-github +## 0.11.0 + +### Minor Changes + +- 03bdc68: Added support for discovery by app + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## 0.11.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.11.0-next.0 ### Minor Changes diff --git a/plugins/catalog-backend-module-github/knip-report.md b/plugins/catalog-backend-module-github/knip-report.md index c200862961..35b39536c9 100644 --- a/plugins/catalog-backend-module-github/knip-report.md +++ b/plugins/catalog-backend-module-github/knip-report.md @@ -4,21 +4,21 @@ | Name | Location | Severity | | :-------------------------------- | :----------- | :------- | -| @backstage/plugin-catalog-backend | package.json | error | -| @backstage/catalog-client | package.json | error | +| @backstage/plugin-catalog-backend | plugins/catalog-backend-module-github/package.json | error | +| @backstage/catalog-client | plugins/catalog-backend-module-github/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-github/package.json | error | ## Unlisted dependencies (4) | Name | Location | Severity | | :---------------------- | :-------------------------------------------- | :------- | -| @octokit/webhooks-types | src/providers/GithubMultiOrgEntityProvider.ts | error | -| @octokit/webhooks-types | src/providers/GithubEntityProvider.test.ts | error | -| @octokit/webhooks-types | src/providers/GithubOrgEntityProvider.ts | error | -| @octokit/webhooks-types | src/providers/GithubEntityProvider.ts | error | +| @octokit/webhooks-types | plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts | error | +| @octokit/webhooks-types | plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts | error | +| @octokit/webhooks-types | plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts | error | +| @octokit/webhooks-types | plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts | error | diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index a61deeb214..5187103233 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github", - "version": "0.11.0-next.0", + "version": "0.11.0", "description": "A Backstage catalog backend module that helps integrate towards GitHub", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md index afd3317620..022eb1445a 100644 --- a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-backend-module-gitlab-org +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-gitlab@0.7.3 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + +## 0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-catalog-backend-module-gitlab@0.7.3-next.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab-org/knip-report.md b/plugins/catalog-backend-module-gitlab-org/knip-report.md index cb5315967a..43328f5e48 100644 --- a/plugins/catalog-backend-module-gitlab-org/knip-report.md +++ b/plugins/catalog-backend-module-gitlab-org/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-gitlab-org/package.json | error | diff --git a/plugins/catalog-backend-module-gitlab-org/package.json b/plugins/catalog-backend-module-gitlab-org/package.json index 05b9e75a0c..09f6ee7811 100644 --- a/plugins/catalog-backend-module-gitlab-org/package.json +++ b/plugins/catalog-backend-module-gitlab-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab-org", - "version": "0.2.13-next.0", + "version": "0.2.13", "description": "The gitlab-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md index 958c3b364a..2610055302 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.7.3 + +### Patch Changes + +- ea80e76: When possible, requests a more limited set of results from the Gitlab projects API, which can reduce the amount of network traffic required to sync with Gitlab. +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## 0.7.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.7.3-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab/knip-report.md b/plugins/catalog-backend-module-gitlab/knip-report.md index cb5315967a..8c7b1cc0a0 100644 --- a/plugins/catalog-backend-module-gitlab/knip-report.md +++ b/plugins/catalog-backend-module-gitlab/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-gitlab/package.json | error | diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 7d8fe59327..c18be1028b 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab", - "version": "0.7.3-next.0", + "version": "0.7.3", "description": "A Backstage catalog backend module that helps integrate towards GitLab", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts index d75d0e224e..774da46553 100644 --- a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts @@ -59,6 +59,7 @@ function setupFakeServer( page: number; include_subgroups: boolean; archived: boolean; + simple?: boolean; }) => { data: GitLabProject[]; nextPage?: number; @@ -78,10 +79,12 @@ 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 simple = req.url.searchParams.get('simple'); const response = listProjectsCallback({ page: parseInt(page!, 10), include_subgroups: include_subgroups === 'true', archived: archived === 'true', + simple: simple === 'true', }); // Filter the fake results based on the `last_activity_after` parameter @@ -471,6 +474,110 @@ describe('GitlabDiscoveryProcessor', () => { }); expect(result2).toHaveLength(1); }); + + it('sets simple=true when skipForkedRepos is false', async () => { + const processor = getProcessor({ + options: { skipForkedRepos: false }, + }); + setupFakeServer( + PROJECTS_URL, + _ => { + return { + data: [ + { + id: 1, + archived: false, + default_branch: 'main', + last_activity_at: '2021-08-05T11:03:05.774Z', + web_url: 'https://gitlab.fake/1', + path_with_namespace: '1', + }, + ], + }; + }, + request => { + // Verify that simple=true is set in the request + expect(request.url.searchParams.get('simple')).toBe('true'); + }, + ); + + const result: any[] = []; + await processor.readLocation(PROJECT_LOCATION, false, e => { + result.push(e); + }); + expect(result).toHaveLength(1); + }); + + it('does not set simple when skipForkedRepos is true', async () => { + const processor = getProcessor({ + options: { skipForkedRepos: true }, + }); + setupFakeServer( + PROJECTS_URL, + _ => { + return { + data: [ + { + id: 1, + archived: false, + default_branch: 'main', + last_activity_at: '2021-08-05T11:03:05.774Z', + web_url: 'https://gitlab.fake/1', + path_with_namespace: '1', + // Include forked_from_project to test fork filtering + forked_from_project: { + id: 100, + name: 'original-project', + }, + }, + ], + }; + }, + request => { + // Verify that simple parameter is not set + expect(request.url.searchParams.get('simple')).toBeNull(); + }, + ); + + const result: any[] = []; + await processor.readLocation(PROJECT_LOCATION, false, e => { + result.push(e); + }); + // Should be empty because forked repo is skipped + expect(result).toHaveLength(0); + }); + + it('sets default parameters correctly (archived=false, simple=true)', async () => { + const processor = getProcessor(); // Uses defaults: skipForkedRepos=false, includeArchivedRepos=false + setupFakeServer( + PROJECTS_URL, + _ => { + return { + data: [ + { + id: 1, + archived: false, + default_branch: 'main', + last_activity_at: '2021-08-05T11:03:05.774Z', + web_url: 'https://gitlab.fake/1', + path_with_namespace: '1', + }, + ], + }; + }, + request => { + // Verify default parameters: archived=false and simple=true + expect(request.url.searchParams.get('archived')).toBe('false'); + expect(request.url.searchParams.get('simple')).toBe('true'); + }, + ); + + const result: any[] = []; + await processor.readLocation(PROJECT_LOCATION, false, e => { + result.push(e); + }); + expect(result).toHaveLength(1); + }); }); describe('handles failure', () => { diff --git a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts index ab31a44bbc..126bb07f03 100644 --- a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts @@ -115,6 +115,11 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { // that the options doesn't include the key so that the API doesn't receive an empty query parameter. ...(lastActivity && { last_activity_after: lastActivity }), ...(!this.includeArchivedRepos && { archived: false }), + // Only use simple=true when we don't need to skip forked repos. + // The simple=true parameter reduces response size by returning fewer fields, + // but it excludes the 'forked_from_project' field which is required for fork detection. + // Therefore, we can only optimize with simple=true when skipForkedRepos is false. + ...(!this.skipForkedRepos && { simple: true }), }; const projects = paginated(options => client.listProjects(options), opts); diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts index daea71060f..9b3bac6399 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts @@ -187,6 +187,55 @@ describe('GitLabClient', () => { expect(allProjects).toHaveLength(mock.all_projects_response.length); }); + + it('should pass simple parameter to API when provided', async () => { + const client = new GitLabClient({ + config: readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), + logger: mockServices.logger.mock(), + }); + + // Mock the pagedRequest method to verify parameters + const mockPagedRequest = jest.fn().mockResolvedValue({ + items: [], + nextPage: undefined, + }); + (client as any).pagedRequest = mockPagedRequest; + + await client.listProjects({ simple: true }); + + expect(mockPagedRequest).toHaveBeenCalledWith('/projects', { + simple: true, + }); + }); + + it('should pass simple parameter to group projects API when provided', async () => { + const client = new GitLabClient({ + config: readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), + logger: mockServices.logger.mock(), + }); + + // Mock the pagedRequest method to verify parameters + const mockPagedRequest = jest.fn().mockResolvedValue({ + items: [], + nextPage: undefined, + }); + (client as any).pagedRequest = mockPagedRequest; + + await client.listProjects({ group: 'test-group', simple: true }); + + expect(mockPagedRequest).toHaveBeenCalledWith( + '/groups/test-group/projects', + { + group: 'test-group', + simple: true, + include_subgroups: true, + }, + ); + }); }); describe('listUsers', () => { diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts index e58227839e..3178e2c0d1 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts @@ -43,6 +43,7 @@ interface ListProjectOptions extends CommonListOptions { group?: string; membership?: boolean; topics?: string; + simple?: boolean; } interface UserListOptions extends CommonListOptions { diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts index f052434035..31ac7df6f1 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts @@ -147,6 +147,21 @@ describe('GitlabDiscoveryEntityProvider - refresh', () => { 'GitlabDiscoveryEntityProvider:test-id', ); + // Mock the GitLabClient listProjects method to verify default parameters + const originalListProjects = (provider as any).gitLabClient.listProjects; + const mockListProjects = jest.fn().mockImplementation(async options => { + // Verify default parameters: archived=false and simple=true (since skipForkedRepos=false by default) + expect(options).toMatchObject({ + group: 'group1', + per_page: 50, + archived: false, + simple: true, // Should be set since skipForkedRepos defaults to false + }); + // Call the original method to maintain test behavior + return originalListProjects.call((provider as any).gitLabClient, options); + }); + (provider as any).gitLabClient.listProjects = mockListProjects; + await provider.connect(entityProviderConnection); const taskDef = schedule.getTasks()[0]; @@ -675,3 +690,115 @@ describe('GitlabDiscoveryEntityProvider - events', () => { expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); }); }); + +describe('GitlabDiscoveryEntityProvider - simple parameter', () => { + it('should pass simple=true when skipForkedRepos is false', async () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'example.com', + apiBaseUrl: 'https://example.com/api/v4', + token: 'test-token', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'example.com', + group: 'test-group', + skipForkedRepos: false, + }, + }, + }, + }, + }); + + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + + const provider = GitlabDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + + // Mock the GitLabClient listProjects method to verify parameters + const mockListProjects = jest.fn().mockResolvedValue({ + items: [], + nextPage: undefined, + }); + + (provider as any).gitLabClient.listProjects = mockListProjects; + + await provider.connect(entityProviderConnection); + await provider.refresh(logger); + + expect(mockListProjects).toHaveBeenCalledWith({ + group: 'test-group', + page: undefined, + per_page: 50, + archived: false, + simple: true, // Should be set when skipForkedRepos is false + }); + }); + + it('should not pass simple when skipForkedRepos is true', async () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'example.com', + apiBaseUrl: 'https://example.com/api/v4', + token: 'test-token', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'example.com', + group: 'test-group', + skipForkedRepos: true, + }, + }, + }, + }, + }); + + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + + const provider = GitlabDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + + // Mock the GitLabClient listProjects method to verify parameters + const mockListProjects = jest.fn().mockResolvedValue({ + items: [], + nextPage: undefined, + }); + + (provider as any).gitLabClient.listProjects = mockListProjects; + + await provider.connect(entityProviderConnection); + await provider.refresh(logger); + + expect(mockListProjects).toHaveBeenCalledWith({ + group: 'test-group', + page: undefined, + per_page: 50, + archived: false, + // simple should not be present when skipForkedRepos is true + }); + }); +}); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index 08bd498615..11b6a0ce04 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -326,6 +326,11 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { ...(!this.config.includeArchivedRepos && { archived: false }), ...(this.config.membership && { membership: true }), ...(this.config.topics && { topics: this.config.topics }), + // Only use simple=true when we don't need to skip forked repos. + // The simple=true parameter reduces response size by returning fewer fields, + // but it excludes the 'forked_from_project' field which is required for fork detection. + // Therefore, we can only optimize with simple=true when skipForkedRepos is false. + ...(!this.config.skipForkedRepos && { simple: true }), }, ); diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md index 9139c3e3f1..aa22c5aad7 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md +++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-catalog-backend-module-incremental-ingestion +## 0.7.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## 0.7.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.7.4-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-incremental-ingestion/knip-report.md b/plugins/catalog-backend-module-incremental-ingestion/knip-report.md index 21e59cdcea..e67f6704ce 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/knip-report.md +++ b/plugins/catalog-backend-module-incremental-ingestion/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---------------------------------- | :----------- | :------- | -| @backstage/plugin-permission-common | package.json | error | +| @backstage/plugin-permission-common | plugins/catalog-backend-module-incremental-ingestion/package.json | error | diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json index 5a2acc53fc..ffc675fcff 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/package.json +++ b/plugins/catalog-backend-module-incremental-ingestion/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-incremental-ingestion", - "version": "0.7.4-next.0", + "version": "0.7.4", "description": "An entity provider for streaming large asset sources into the catalog", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index 83b25b4f4c..2a1dbf6d62 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.11.9 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## 0.11.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.11.9-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 9e32b21bb3..91ac03c93a 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", - "version": "0.11.9-next.0", + "version": "0.11.9", "description": "A Backstage catalog backend module that helps integrate towards LDAP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-logs/CHANGELOG.md b/plugins/catalog-backend-module-logs/CHANGELOG.md index 9c42d29587..fbc0b1dfa7 100644 --- a/plugins/catalog-backend-module-logs/CHANGELOG.md +++ b/plugins/catalog-backend-module-logs/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-logs +## 0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + ## 0.1.14-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-logs/package.json b/plugins/catalog-backend-module-logs/package.json index be3ce91abd..3b27e9cd79 100644 --- a/plugins/catalog-backend-module-logs/package.json +++ b/plugins/catalog-backend-module-logs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-logs", - "version": "0.1.14-next.0", + "version": "0.1.14", "description": "A module that subscribes to catalog related events and logs them.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index b48d58fbd2..8ab315e9dd 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.8.0 + +### Minor Changes + +- 577f0ed: **BREAKING**: Encode query filters for requests made to msgraph. If you currently have manually encoded characters in a filter, this is a breaking change and must be updated to avoid requests being double encoded. + + ```diff + user: + - filter: department in('MARKETING', 'RESEARCH %26 DEVELOPMENT') + + filter: department in('MARKETING', 'RESEARCH & DEVELOPMENT') + ``` + +### Patch Changes + +- 7597781: Ensure that msgraph parent group stays same in case the group has multiple parents +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/backend-plugin-api@1.4.3 + +## 0.8.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.8.0-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/knip-report.md b/plugins/catalog-backend-module-msgraph/knip-report.md index cb5315967a..1b4a6feaab 100644 --- a/plugins/catalog-backend-module-msgraph/knip-report.md +++ b/plugins/catalog-backend-module-msgraph/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-msgraph/package.json | error | diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index facfd228a3..f2f8a5d442 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", - "version": "0.8.0-next.1", + "version": "0.8.0", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index 33ae04bd76..bd58881728 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -1102,6 +1102,14 @@ describe('read microsoft graph', () => { name: 'c', }, }); + const groupD = group({ + metadata: { + annotations: { + 'graph.microsoft.com/group-id': 'group-id-d', + }, + name: 'd', + }, + }); const user1 = user({ metadata: { annotations: { @@ -1118,16 +1126,17 @@ describe('read microsoft graph', () => { name: 'user2', }, }); - const groups = [rootGroup, groupA, groupB, groupC]; + const groups = [rootGroup, groupA, groupB, groupC, groupD]; const users = [user1, user2]; const groupMember = new Map>(); groupMember.set('group-id-b', new Set(['group-id-c'])); + groupMember.set('group-id-d', new Set(['group-id-c'])); const groupMemberOf = new Map>(); groupMemberOf.set('user-id-1', new Set(['group-id-a'])); groupMemberOf.set('user-id-2', new Set(['group-id-c'])); // We have a root groups - // We have three groups: a, b, c. c is child of b + // We have three groups: a, b, c. c is child of b and d, b should be picked as it's first // we have two users: u1, u2. u1 is member of a, u2 is member of c resolveRelations(rootGroup, groups, users, groupMember, groupMemberOf); @@ -1147,6 +1156,11 @@ describe('read microsoft graph', () => { expect(groupC.spec.parent).toEqual('group:default/b'); expect(groupC.spec.children).toEqual(expect.arrayContaining([])); + expect(groupD.spec.parent).toEqual('group:default/root'); + expect(groupD.spec.children).toEqual( + expect.arrayContaining(['group:default/c']), + ); + expect(user1.spec.memberOf).toEqual( expect.arrayContaining(['group:default/a']), ); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 4761c45ac7..8bc7e6899b 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -359,7 +359,14 @@ export function resolveRelations( } }); + // TODO: Until we have better support for multiple parents in the model, + // the order of the parents is important as changing it causes + // unnecessary entity stitching randomly. retrieveItems(parentGroups, id).forEach(p => { + // Only set the parent if it doesn't exist yet + if (group.spec.parent) { + return; + } const parentGroup = groupMap.get(p); if (parentGroup) { // TODO: Only having a single parent group might not match every companies model, but fine for now. @@ -546,5 +553,5 @@ function retrieveItems( target: Map>, key: string, ): Set { - return target.get(key) ?? new Set(); + return new Set([...(target.get(key) ?? [])].sort()); } diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md index 6c82466060..6cef384c74 100644 --- a/plugins/catalog-backend-module-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-backend-module-openapi +## 0.2.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## 0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.2.14-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-openapi/knip-report.md b/plugins/catalog-backend-module-openapi/knip-report.md index 559313ad8c..91d1d44c3e 100644 --- a/plugins/catalog-backend-module-openapi/knip-report.md +++ b/plugins/catalog-backend-module-openapi/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------ | :----------- | :------- | -| openapi-types | package.json | error | +| openapi-types | plugins/catalog-backend-module-openapi/package.json | error | diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index 3902aafcb7..155c24115d 100644 --- a/plugins/catalog-backend-module-openapi/package.json +++ b/plugins/catalog-backend-module-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-openapi", - "version": "0.2.14-next.0", + "version": "0.2.14", "description": "A Backstage catalog backend module that helps with OpenAPI specifications", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md index 141e2d52ad..14fa817eba 100644 --- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md +++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-backend-module-puppetdb +## 0.2.14 + +### Patch Changes + +- afd368e: **BREAKING ALPHA**: The module has been moved from the `/alpha` export to the root of the package. +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## 0.2.14-next.1 + +### Patch Changes + +- afd368e: **BREAKING ALPHA**: The module has been moved from the `/alpha` export to the root of the package. +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.2.14-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-puppetdb/knip-report.md b/plugins/catalog-backend-module-puppetdb/knip-report.md index acef584675..ac2ab908f0 100644 --- a/plugins/catalog-backend-module-puppetdb/knip-report.md +++ b/plugins/catalog-backend-module-puppetdb/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-puppetdb/package.json | error | diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index bcbdcbef48..ced5df4773 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-puppetdb", - "version": "0.2.14-next.0", + "version": "0.2.14", "description": "A Backstage catalog backend module that helps integrate towards PuppetDB", "backstage": { "role": "backend-plugin-module", @@ -24,16 +24,12 @@ "license": "Apache-2.0", "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", "./package.json": "./package.json" }, "main": "src/index.ts", "types": "src/index.ts", "typesVersions": { "*": { - "alpha": [ - "src/alpha.ts" - ], "package.json": [ "package.json" ] diff --git a/plugins/catalog-backend-module-puppetdb/report-alpha.api.md b/plugins/catalog-backend-module-puppetdb/report-alpha.api.md deleted file mode 100644 index 211add3895..0000000000 --- a/plugins/catalog-backend-module-puppetdb/report-alpha.api.md +++ /dev/null @@ -1,13 +0,0 @@ -## API Report File for "@backstage/plugin-catalog-backend-module-puppetdb" - -> 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'; - -// @alpha -const catalogModulePuppetDbEntityProvider: BackendFeature; -export default catalogModulePuppetDbEntityProvider; - -// (No @packageDocumentation comment for this package) -``` diff --git a/plugins/catalog-backend-module-puppetdb/report.api.md b/plugins/catalog-backend-module-puppetdb/report.api.md index f6fa5bfc83..6b846b29dd 100644 --- a/plugins/catalog-backend-module-puppetdb/report.api.md +++ b/plugins/catalog-backend-module-puppetdb/report.api.md @@ -3,6 +3,7 @@ > 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 { Config } from '@backstage/config'; import { EntityProvider } from '@backstage/plugin-catalog-node'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; @@ -16,6 +17,10 @@ import { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugi // @public export const ANNOTATION_PUPPET_CERTNAME = 'puppet.com/certname'; +// @public +const catalogModulePuppetDbEntityProvider: BackendFeature; +export default catalogModulePuppetDbEntityProvider; + // @public export const DEFAULT_PROVIDER_ID = 'default'; diff --git a/plugins/catalog-backend-module-puppetdb/src/index.ts b/plugins/catalog-backend-module-puppetdb/src/index.ts index b5c5f9d209..9b10bd630f 100644 --- a/plugins/catalog-backend-module-puppetdb/src/index.ts +++ b/plugins/catalog-backend-module-puppetdb/src/index.ts @@ -22,3 +22,4 @@ export * from './providers'; export * from './puppet'; +export { default } from './module'; diff --git a/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.ts b/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.ts index aa524f2409..1ab95c90d8 100644 --- a/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.ts +++ b/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.ts @@ -24,7 +24,7 @@ import { PuppetDbEntityProvider } from '../providers/PuppetDbEntityProvider'; /** * Registers the `PuppetDbEntityProvider` with the catalog processing extension point. * - * @alpha + * @public */ export const catalogModulePuppetDbEntityProvider = createBackendModule({ pluginId: 'catalog', diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md index 4a6c195039..a654ff5c3d 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md +++ b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-backend-module-scaffolder-entity-model +## 0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-common@1.7.1 + +## 0.2.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.2.12-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/package.json b/plugins/catalog-backend-module-scaffolder-entity-model/package.json index 6e040f8078..beed6b3028 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/package.json +++ b/plugins/catalog-backend-module-scaffolder-entity-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-scaffolder-entity-model", - "version": "0.2.12-next.0", + "version": "0.2.12", "description": "Adds support for the scaffolder specific entity model (e.g. the Template kind) to the catalog backend plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md index 28436537b0..1c4b8f8363 100644 --- a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md +++ b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-catalog-backend-module-unprocessed +## 0.6.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## 0.6.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.6.4-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-unprocessed/knip-report.md b/plugins/catalog-backend-module-unprocessed/knip-report.md index fe81af1cb9..b4dbca6967 100644 --- a/plugins/catalog-backend-module-unprocessed/knip-report.md +++ b/plugins/catalog-backend-module-unprocessed/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :----------------------------- | :----------- | :------- | -| @backstage/plugin-catalog-node | package.json | error | -| @backstage/plugin-auth-node | package.json | error | +| @backstage/plugin-catalog-node | plugins/catalog-backend-module-unprocessed/package.json | error | +| @backstage/plugin-auth-node | plugins/catalog-backend-module-unprocessed/package.json | error | diff --git a/plugins/catalog-backend-module-unprocessed/package.json b/plugins/catalog-backend-module-unprocessed/package.json index a3de0c75a4..32bcfe904f 100644 --- a/plugins/catalog-backend-module-unprocessed/package.json +++ b/plugins/catalog-backend-module-unprocessed/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-unprocessed", - "version": "0.6.4-next.0", + "version": "0.6.4", "description": "Backstage Catalog module to view unprocessed entities", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index ad535f16fd..54be12f4cb 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,69 @@ # @backstage/plugin-catalog-backend +## 3.1.0 + +### Minor Changes + +- 9b40a55: Add support for specifying an entity `spec.type` in `catalog.rules` and `catalog.locations.rules` within the catalog configuration. + + For example, this enables allowing all `Template` entities with the type `website`: + + ```diff + catalog: + rules: + - allow: + - Component + - API + - Resource + - System + - Domain + - Location + + - allow: + + - kind: Template + + spec.type: website + locations: + - type: url + pattern: https://github.com/org/*\/blob/master/*.yaml + ``` + +### Patch Changes + +- 37b4eaf: The 'get-catalog-entity' action now throws a ConflictError instead of generic Error if multiple entities are found, so MCP call doesn't fail with 500. +- 2bbd24f: Order catalog processors by priority. + + This change enables the ordering of catalog processors by their priority, + allowing for more control over the catalog processing sequence. + The default priority is set to 20, and processors can be assigned a custom + priority to influence their execution order. Lower number indicates higher priority. + The priority can be set by implementing the `getPriority` method in the processor class + or by adding a `catalog.processors..priority` configuration + in the `app-config.yaml` file. The configuration takes precedence over the method. + +- e934a27: Updating `catalog:get-catalog-entity` action to be `readOnly` and non destructive +- 0efcc97: Updated generated schemas +- 2204f5b: Prevent deadlock in catalog deferred stitching +- 58874c4: Add support to disable catalog providers and processors via configuration +- a4c82ad: Only run provider orphan cleanup if the engine is started in the first place +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + +## 3.0.2-next.1 + +### Patch Changes + +- 2204f5b: Prevent deadlock in catalog deferred stitching +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 3.0.2-next.0 ### Patch Changes diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 6329bcb111..f1b72e5b45 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -38,8 +38,11 @@ export interface Config { * Allow entities of these particular kinds. * * E.g. ["Component", "API", "Template", "Location"] + * + * You can also specify the type of the entity by using an object with `kind` and optional `spec.type` properties. + * E.g. [{ kind: "Component", 'spec.type': "service" }] */ - allow: Array; + allow: Array; /** * Limit this rule to a specific location * @@ -218,5 +221,43 @@ export interface Config { * housing catalog-info files. */ processingInterval?: HumanDuration | false; + + /** + * Catalog provide specific configuration. + * Additional configuration for providers are specified in the catalog + * modules. + */ + providers?: { + /** + * Name is the provider ID, e.g. "bitbucketServer" + */ + [name: string]: { + /** + * Whether the provider is enabled or not. Defaults to true. + */ + enabled?: boolean; + }; + }; + /** + * Configuration for entity processors. Additional configuration for + * processors are specified in the catalog modules. + */ + processors?: { + /** + * Name is the processor ID, e.g. "catalog-processor" + */ + [name: string]: { + /** + * Whether the processor is enabled or not. Defaults to true. + */ + enabled?: boolean; + /** + * The priority of the processor, which is used to determine the order in which + * processors are run. The default priority is 20, and lower value means + * that the processor runs earlier. + */ + priority?: number; + }; + }; }; } diff --git a/plugins/catalog-backend/knip-report.md b/plugins/catalog-backend/knip-report.md index d547deea65..b221ae3cf4 100644 --- a/plugins/catalog-backend/knip-report.md +++ b/plugins/catalog-backend/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------- | :----------- | :------- | -| better-sqlite3 | package.json | error | +| better-sqlite3 | plugins/catalog-backend/package.json | error | diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index d825f29356..a880cc224f 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "3.0.2-next.0", + "version": "3.1.0", "description": "The Backstage backend plugin that provides the Backstage catalog", "backstage": { "role": "backend-plugin", diff --git a/plugins/catalog-backend/src/actions/createGetCatalogEntityAction.ts b/plugins/catalog-backend/src/actions/createGetCatalogEntityAction.ts index a1c7cefe78..b9f98291fd 100644 --- a/plugins/catalog-backend/src/actions/createGetCatalogEntityAction.ts +++ b/plugins/catalog-backend/src/actions/createGetCatalogEntityAction.ts @@ -28,6 +28,11 @@ export const createGetCatalogEntityAction = ({ actionsRegistry.register({ name: 'get-catalog-entity', title: 'Get Catalog Entity', + attributes: { + destructive: false, + readOnly: true, + idempotent: true, + }, description: ` This allows you to get a single entity from the software catalog. Each entity in the software catalog has a unique name, kind, and namespace. The default namespace is "default". diff --git a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts index 1afa0b9c30..ff450e9ccf 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts @@ -435,4 +435,139 @@ describe('markForStitching', () => { } }, ); + + it.each(databases.eachSupportedId())( + 'reproduces deadlock scenario when concurrent transactions update overlapping entity sets %p', + async databaseId => { + const knex = await databases.init(databaseId); + await applyDatabaseMigrations(knex); + + // Setup test data with multiple entities + const entityRefs = [ + 'k:ns/entity-a', + 'k:ns/entity-b', + 'k:ns/entity-c', + 'k:ns/entity-d', + 'k:ns/entity-e', + 'k:ns/entity-f', + ]; + + await knex('refresh_state').insert( + entityRefs.map((ref, i) => ({ + entity_id: `${i + 1}`, + entity_ref: ref, + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: knex.fn.now(), + last_discovery_at: knex.fn.now(), + next_stitch_at: null, + next_stitch_ticket: null, + })), + ); + + // This test attempts to reproduce the deadlock by running concurrent transactions + // that update overlapping sets of entities in different orders + const errorResults = []; + + for (let attempt = 0; attempt < 10; attempt++) { + // Transaction 1: Update entities A, B, C, D, E + const transaction1 = knex.transaction(async trx => { + await markForStitching({ + knex: trx, + strategy: { + mode: 'deferred', + pollingInterval: { seconds: 1 }, + stitchTimeout: { seconds: 1 }, + }, + entityRefs: [ + 'k:ns/entity-a', + 'k:ns/entity-b', + 'k:ns/entity-c', + 'k:ns/entity-d', + 'k:ns/entity-e', + ], + }); + + // Add a small delay to increase chance of collision + await new Promise(resolve => setTimeout(resolve, 10)); + + await markForStitching({ + knex: trx, + strategy: { + mode: 'deferred', + pollingInterval: { seconds: 1 }, + stitchTimeout: { seconds: 1 }, + }, + entityRefs: ['k:ns/entity-f'], + }); + }); + + // Transaction 2: Update entities F, E, D, C, B (reverse order) + const transaction2 = knex.transaction(async trx => { + await markForStitching({ + knex: trx, + strategy: { + mode: 'deferred', + pollingInterval: { seconds: 1 }, + stitchTimeout: { seconds: 1 }, + }, + entityRefs: [ + 'k:ns/entity-f', + 'k:ns/entity-e', + 'k:ns/entity-d', + 'k:ns/entity-c', + 'k:ns/entity-b', + ], + }); + + // Add a small delay to increase chance of collision + await new Promise(resolve => setTimeout(resolve, 10)); + + await markForStitching({ + knex: trx, + strategy: { + mode: 'deferred', + pollingInterval: { seconds: 1 }, + stitchTimeout: { seconds: 1 }, + }, + entityRefs: ['k:ns/entity-a'], + }); + }); + + // Run both transactions concurrently to create potential deadlock + errorResults.push( + Promise.allSettled([transaction1, transaction2]).then(results => + results + .filter(r => r.status === 'rejected') + .map(r => (r as PromiseRejectedResult).reason), + ), + ); + } + + const allResults = await Promise.all(errorResults); + + const deadlockErrors = allResults + .flat() + .filter( + error => + error?.code === '40P01' || + error?.message?.includes('deadlock detected') || + error?.message?.includes('deadlock'), + ); + expect(deadlockErrors).toEqual([]); + + // Verify final state - all entities should have been marked for stitching + const finalState = await knex('refresh_state') + .select('entity_ref', 'next_stitch_at', 'next_stitch_ticket') + .whereNotNull('next_stitch_at') + .orderBy('entity_ref'); + + expect(finalState.length).toBeGreaterThan(0); + finalState.forEach(row => { + expect(row.next_stitch_at).not.toBeNull(); + expect(row.next_stitch_ticket).not.toBeNull(); + }); + }, + ); }); diff --git a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts index ecc364a9cc..484d6e4ed6 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts @@ -17,9 +17,34 @@ import { Knex } from 'knex'; import splitToChunks from 'lodash/chunk'; import { v4 as uuid } from 'uuid'; +import { ErrorLike, isError } from '@backstage/errors'; import { StitchingStrategy } from '../../../stitching/types'; +import { setTimeout as sleep } from 'timers/promises'; import { DbFinalEntitiesRow, DbRefreshStateRow } from '../../tables'; +const UPDATE_CHUNK_SIZE = 100; // Smaller chunks reduce contention +const DEADLOCK_RETRY_ATTEMPTS = 3; +const DEADLOCK_BASE_DELAY_MS = 25; + +// PostgreSQL deadlock error code +const POSTGRES_DEADLOCK_SQLSTATE = '40P01'; + +/** + * Checks if the given error is a deadlock error for the database engine in use. + */ +function isDeadlockError( + knex: Knex | Knex.Transaction, + e: unknown, +): e is ErrorLike { + if (knex.client.config.client.includes('pg')) { + // PostgreSQL deadlock detection + return isError(e) && e.code === POSTGRES_DEADLOCK_SQLSTATE; + } + + // Add more database engine checks here as needed + return false; +} + /** * Marks a number of entities for stitching some time in the near * future. @@ -32,9 +57,8 @@ export async function markForStitching(options: { entityRefs?: Iterable; entityIds?: Iterable; }): Promise { - // Splitting to chunks just to cover pathological cases that upset the db - const entityRefs = split(options.entityRefs); - const entityIds = split(options.entityIds); + const entityRefs = sortSplit(options.entityRefs); + const entityIds = sortSplit(options.entityIds); const knex = options.knex; const mode = options.strategy.mode; @@ -51,13 +75,15 @@ export async function markForStitching(options: { .select('entity_id') .whereIn('entity_ref', chunk), ); - await knex - .table('refresh_state') - .update({ - result_hash: 'force-stitching', - next_update_at: knex.fn.now(), - }) - .whereIn('entity_ref', chunk); + await retryOnDeadlock(async () => { + await knex + .table('refresh_state') + .update({ + result_hash: 'force-stitching', + next_update_at: knex.fn.now(), + }) + .whereIn('entity_ref', chunk); + }, knex); } for (const chunk of entityIds) { @@ -67,44 +93,74 @@ export async function markForStitching(options: { hash: 'force-stitching', }) .whereIn('entity_id', chunk); - await knex - .table('refresh_state') - .update({ - result_hash: 'force-stitching', - next_update_at: knex.fn.now(), - }) - .whereIn('entity_id', chunk); + await retryOnDeadlock(async () => { + await knex + .table('refresh_state') + .update({ + result_hash: 'force-stitching', + next_update_at: knex.fn.now(), + }) + .whereIn('entity_id', chunk); + }, knex); } } else if (mode === 'deferred') { // It's OK that this is shared across refresh state rows; it just needs to // be uniquely generated for every new stitch request. const ticket = uuid(); + // Update by primary key in deterministic order to avoid deadlocks for (const chunk of entityRefs) { - await knex('refresh_state') - .update({ - next_stitch_at: knex.fn.now(), - next_stitch_ticket: ticket, - }) - .whereIn('entity_ref', chunk); + await retryOnDeadlock(async () => { + await knex('refresh_state') + .update({ + next_stitch_at: knex.fn.now(), + next_stitch_ticket: ticket, + }) + .whereIn('entity_ref', chunk); + }, knex); } for (const chunk of entityIds) { - await knex('refresh_state') - .update({ - next_stitch_at: knex.fn.now(), - next_stitch_ticket: ticket, - }) - .whereIn('entity_id', chunk); + await retryOnDeadlock(async () => { + await knex('refresh_state') + .update({ + next_stitch_at: knex.fn.now(), + next_stitch_ticket: ticket, + }) + .whereIn('entity_id', chunk); + }, knex); } } else { throw new Error(`Unknown stitching strategy mode ${mode}`); } } -function split(input: Iterable | undefined): string[][] { +function sortSplit(input: Iterable | undefined): string[][] { if (!input) { return []; } - return splitToChunks(Array.isArray(input) ? input : [...input], 200); + const array = Array.isArray(input) ? input.slice() : [...input]; + array.sort(); + return splitToChunks(array, UPDATE_CHUNK_SIZE); +} + +async function retryOnDeadlock( + fn: () => Promise, + knex: Knex | Knex.Transaction, + retries = DEADLOCK_RETRY_ATTEMPTS, + baseMs = DEADLOCK_BASE_DELAY_MS, +): Promise { + let attempt = 0; + for (;;) { + try { + return await fn(); + } catch (e: unknown) { + if (isDeadlockError(knex, e) && attempt < retries) { + await sleep(baseMs * Math.pow(2, attempt)); + attempt++; + continue; + } + throw e; + } + } } diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts index 19c231884c..a28ceb0d48 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts @@ -27,8 +27,14 @@ const entity = { kind: 'Group', } as Entity, component: { - kind: 'component', + kind: 'Component', } as Entity, + componentWithType: { + kind: 'Component', + spec: { + type: 'service', + }, + } as unknown as Entity, location: { kind: 'Location', } as Entity, @@ -164,6 +170,9 @@ describe('DefaultCatalogRulesEnforcer', () => { expect(enforcer.isAllowed(entity.user, location.x)).toBe(false); expect(enforcer.isAllowed(entity.group, location.y)).toBe(false); expect(enforcer.isAllowed(entity.component, location.z)).toBe(true); + expect(enforcer.isAllowed(entity.componentWithType, location.z)).toBe( + true, + ); expect(enforcer.isAllowed(entity.location, location.z)).toBe(true); }); @@ -268,6 +277,33 @@ describe('DefaultCatalogRulesEnforcer', () => { }); }); + it('should only allow components with a specific type', () => { + const enforcer = DefaultCatalogRulesEnforcer.fromConfig( + new ConfigReader({ + catalog: { + rules: [ + { + allow: [{ kind: 'Component', 'spec.type': 'service' }], + locations: [{ type: 'url', pattern: 'https://github.com/b/**' }], + }, + ], + }, + }), + ); + + expect(enforcer.isAllowed(entity.component, location.w)).toBe(false); + expect(enforcer.isAllowed(entity.component, location.y)).toBe(false); + expect(enforcer.isAllowed(entity.component, location.z)).toBe(false); + + expect(enforcer.isAllowed(entity.componentWithType, location.w)).toBe(true); + expect(enforcer.isAllowed(entity.componentWithType, location.y)).toBe( + false, + ); + expect(enforcer.isAllowed(entity.componentWithType, location.z)).toBe( + false, + ); + }); + it('should allow locations with a hidden folder', () => { const enforcer = DefaultCatalogRulesEnforcer.fromConfig( new ConfigReader({ diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index 8fb237df30..15e620a443 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -19,6 +19,7 @@ import { Entity } from '@backstage/catalog-model'; import path from 'path'; import { LocationSpec } from '@backstage/plugin-catalog-common'; import { minimatch } from 'minimatch'; +import { z } from 'zod'; /** * Rules to apply to catalog entities. @@ -26,9 +27,7 @@ import { minimatch } from 'minimatch'; * An undefined list of matchers means match all, an empty list of matchers means match none. */ export type CatalogRule = { - allow: Array<{ - kind: string; - }>; + allow: CatalogRuleAllow[]; locations?: Array<{ exact?: string; type: string; @@ -36,6 +35,11 @@ export type CatalogRule = { }>; }; +type CatalogRuleAllow = { + kind: string; + 'spec.type'?: string; +}; + /** * Decides whether an entity from a given location is allowed to enter the * catalog, according to some rule set. @@ -44,6 +48,16 @@ export type CatalogRulesEnforcer = { isAllowed(entity: Entity, location: LocationSpec): boolean; }; +const allowRuleParser = z.array( + z + .object({ + kind: z.string(), + 'spec.type': z.string().optional(), + }) + .or(z.string()) + .transform(val => (typeof val === 'string' ? { kind: val } : val)), +); + /** * Implements the default catalog rule set, consuming the config keys * `catalog.rules` and `catalog.locations.[].rules`. @@ -78,6 +92,9 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { * catalog: * rules: * - allow: [Component, API] + * - allow: + * - kind: Resource + * 'spec.type': database * - allow: [Template] * locations: * - type: url @@ -105,7 +122,7 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { const globalRules = config .getConfigArray('catalog.rules') .map(ruleConf => ({ - allow: ruleConf.getStringArray('allow').map(kind => ({ kind })), + allow: allowRuleParser.parse(ruleConf.get('allow')), locations: ruleConf .getOptionalConfigArray('locations') ?.map(locationConfig => { @@ -199,16 +216,31 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { return false; } - private matchEntity(entity: Entity, matchers?: { kind: string }[]): boolean { + private matchEntity(entity: Entity, matchers?: CatalogRuleAllow[]): boolean { if (!matchers) { return true; } for (const matcher of matchers) { - if (entity?.kind?.toLowerCase() !== matcher.kind.toLowerCase()) { + if ( + entity.kind?.toLocaleLowerCase('en-US') !== + matcher.kind.toLocaleLowerCase('en-US') + ) { continue; } + if (matcher['spec.type']) { + if (typeof entity.spec?.type !== 'string') { + continue; + } + if ( + matcher['spec.type'].toLocaleLowerCase('en-US') !== + entity.spec.type.toLocaleLowerCase('en-US') + ) { + continue; + } + } + return true; } diff --git a/plugins/catalog-backend/src/schema/openapi.yaml b/plugins/catalog-backend/src/schema/openapi.yaml index baaba44d48..e4282f1700 100644 --- a/plugins/catalog-backend/src/schema/openapi.yaml +++ b/plugins/catalog-backend/src/schema/openapi.yaml @@ -6,8 +6,9 @@ info: The API surface consists of a few distinct groups of functionality. Each has a dedicated section below. - > **Note:** This page only describes some of the most commonly used parts of the - > API, and is a work in progress. + :::note Note + This page only describes some of the most commonly used parts of the API, and is a work in progress. + ::: All of the URL paths in this article are assumed to be on top of some base URL pointing at your catalog installation. For example, if the path given in a diff --git a/plugins/catalog-backend/src/schema/openapi/generated/apis/Api.server.ts b/plugins/catalog-backend/src/schema/openapi/generated/apis/Api.server.ts index b758377e5f..c10518a08e 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/apis/Api.server.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/apis/Api.server.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/apis/index.ts b/plugins/catalog-backend/src/schema/openapi/generated/apis/index.ts index a3cdbbebd2..8d81cbaf39 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/apis/index.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/index.ts b/plugins/catalog-backend/src/schema/openapi/generated/index.ts index 69c39313c6..dec4b8804e 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/index.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationEntityField.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationEntityField.model.ts index ad2b5aecd1..65d2a2db65 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationEntityField.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationEntityField.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts index fb01ae8e8e..e7563a5c41 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts index e760d9cf94..dca7b943cc 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts index c1085a2a7b..b91f979678 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts index 66fc46bc0f..40d67804b7 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocation201Response.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocation201Response.model.ts index 27e557639a..75575079c4 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocation201Response.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocation201Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocationRequest.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocationRequest.model.ts index 5351ac289b..183aedc1f8 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocationRequest.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocationRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts index 1a962367d1..7b02ad53d9 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts index 11c42a6aee..ea67ecb2e5 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponsePageInfo.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponsePageInfo.model.ts index 07b4137c77..8e0fab6f68 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponsePageInfo.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponsePageInfo.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/Entity.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/Entity.model.ts index f386f3dd17..fed0b96399 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/Entity.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/Entity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts index a94f784f05..e48164ea7c 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts index 40594a1515..b82248b5e2 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacet.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacet.model.ts index f0f89f6eae..36551c0b66 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacet.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacet.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts index e013f0b5a8..43b10e0965 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityLink.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityLink.model.ts index 53d7d1f9ff..3c406bb785 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityLink.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityLink.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityMeta.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityMeta.model.ts index cc8dd555a6..ea2dedac5c 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityMeta.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityMeta.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityRelation.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityRelation.model.ts index fe98a84481..79313ca658 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityRelation.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityRelation.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorError.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorError.model.ts index fe5811628d..e0265e95d7 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts index d44dcb66d9..3eb5e15740 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts index 91c120483d..edbcc32df7 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts index 9160f0823b..2308831357 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts index a9f1af1c53..d5f1f1260d 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/Location.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/Location.model.ts index d7e4a22de4..9c6e8fbc96 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/Location.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/Location.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/LocationInput.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/LocationInput.model.ts index 9cdc4c2c13..f86296a14b 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/LocationInput.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/LocationInput.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/LocationSpec.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/LocationSpec.model.ts index 1d56d8f3b0..9286b27d01 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/LocationSpec.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/LocationSpec.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/ModelError.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/ModelError.model.ts index 5526d703e6..958fde7d0b 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/ModelError.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/NullableEntity.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/NullableEntity.model.ts index aa6135ded4..fccc37650e 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/NullableEntity.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/NullableEntity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts index b3913ccd85..345adaedea 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts index df5ce3811b..da5d7dce1c 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts index 6598f8df96..fc2437efc5 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityRelation.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityRelation.model.ts index 9c7dafb534..6c70398610 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityRelation.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityRelation.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/RefreshEntityRequest.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/RefreshEntityRequest.model.ts index 617f35288b..b26b7f1c45 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/RefreshEntityRequest.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/RefreshEntityRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts index fac9525afa..532b6fa9e3 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts index 8718d7938d..81cf98faac 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntityRequest.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntityRequest.model.ts index c3be04818d..c411697d94 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntityRequest.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntityRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/index.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/index.ts index d6c81becb5..0e0d46df01 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/index.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/router.ts b/plugins/catalog-backend/src/schema/openapi/generated/router.ts index d32ef0a4de..fddec71822 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/router.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/router.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ export const spec = { title: 'catalog', version: '1', description: - 'The API surface consists of a few distinct groups of functionality. Each has a\ndedicated section below.\n\n> **Note:** This page only describes some of the most commonly used parts of the\n> API, and is a work in progress.\n\nAll of the URL paths in this article are assumed to be on top of some base URL\npointing at your catalog installation. For example, if the path given in a\nsection below is `/entities`, and the catalog is located at\n`http://localhost:7007/api/catalog` during local development, the full URL would\nbe `http://localhost:7007/api/catalog/entities`. The actual URL may vary from\none organization to the other, especially in production, but is commonly your\n`backend.baseUrl` in your app config, plus `/api/catalog` at the end.\n\nSome or all of the endpoints may accept or require an `Authorization` header\nwith a `Bearer` token, which should then be the Backstage token returned by the\n[`identity API`](https://backstage.io/docs/reference/core-plugin-api.identityapiref).\n', + 'The API surface consists of a few distinct groups of functionality. Each has a\ndedicated section below.\n\n:::note Note \n This page only describes some of the most commonly used parts of the API, and is a work in progress.\n:::\n\nAll of the URL paths in this article are assumed to be on top of some base URL\npointing at your catalog installation. For example, if the path given in a\nsection below is `/entities`, and the catalog is located at\n`http://localhost:7007/api/catalog` during local development, the full URL would\nbe `http://localhost:7007/api/catalog/entities`. The actual URL may vary from\none organization to the other, especially in production, but is commonly your\n`backend.baseUrl` in your app config, plus `/api/catalog` at the end.\n\nSome or all of the endpoints may accept or require an `Authorization` header\nwith a `Bearer` token, which should then be the Backstage token returned by the\n[`identity API`](https://backstage.io/docs/reference/core-plugin-api.identityapiref).\n', license: { name: 'Apache-2.0', url: 'http://www.apache.org/licenses/LICENSE-2.0.html', diff --git a/plugins/catalog-backend/src/schema/openapi/index.ts b/plugins/catalog-backend/src/schema/openapi/index.ts index db98243cbf..196aad553a 100644 --- a/plugins/catalog-backend/src/schema/openapi/index.ts +++ b/plugins/catalog-backend/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index e697c6e77f..dcd677152e 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -568,9 +568,11 @@ export class CatalogBuilder { const locationStore = new DefaultLocationStore(dbClient); const configLocationProvider = new ConfigLocationEntityProvider(config); - const entityProviders = lodash.uniqBy( - [...this.entityProviders, locationStore, configLocationProvider], - provider => provider.getProviderName(), + const entityProviders = this.filterProviders( + lodash.uniqBy( + [...this.entityProviders, locationStore, configLocationProvider], + provider => provider.getProviderName(), + ), ); const processingEngine = new DefaultCatalogProcessingEngine({ @@ -725,7 +727,52 @@ export class CatalogBuilder { this.checkMissingExternalProcessors(processors); - return processors; + const filteredProcessors = this.filterProcessors(processors); + + // Lastly sort the processors by priority. Config can override the + // priority of a processor to allow control of 3rd party processors. + filteredProcessors.sort((a, b) => { + const getProcessorPriority = (processor: CatalogProcessor) => { + try { + return ( + config.getOptionalNumber( + `catalog.processors.${processor.getProcessorName()}.priority`, + ) ?? + processor.getPriority?.() ?? + 20 + ); + } catch (_) { + // In case the processor config is not an object, just return default priority + return 20; + } + }; + + const aPriority = getProcessorPriority(a); + const bPriority = getProcessorPriority(b); + return aPriority - bPriority; + }); + + return filteredProcessors; + } + + private filterProcessors(processors: CatalogProcessor[]) { + const { config } = this.env; + const processorsConfig = config.getOptionalConfig('catalog.processors'); + if (!processorsConfig) { + return processors; + } + + return processors.filter(p => { + try { + const processorConfig = processorsConfig.getOptionalConfig( + p.getProcessorName(), + ); + return processorConfig?.getOptionalBoolean('enabled') ?? true; + } catch (_) { + // In case the processor config is not an object, just include the processor + return true; + } + }); } // TODO(Rugvip): These old processors are removed, for a while we'll be throwing @@ -838,6 +885,26 @@ export class CatalogBuilder { ); } + private filterProviders(providers: EntityProvider[]) { + const { config } = this.env; + const providersConfig = config.getOptionalConfig('catalog.providers'); + if (!providersConfig) { + return providers; + } + + return providers.filter(p => { + try { + const providerConfig = providersConfig.getOptionalConfig( + p.getProviderName(), + ); + return providerConfig?.getOptionalBoolean('enabled') ?? true; + } catch (_) { + // In case the provider config is not an object, just include the provider + return true; + } + }); + } + private static getDefaultProcessingInterval( config: Config, ): ProcessingIntervalFunction { diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index a756916c38..8d19916dab 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-catalog-graph +## 0.5.0 + +### Minor Changes + +- ae6b606: Support custom relations by using an API to define known relations and which to show by default + Fixes "simplified" bug (#30121) which caused graphs not to be simplified + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + +## 0.4.23-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.4.23-next.1 ### Patch Changes diff --git a/plugins/catalog-graph/README.md b/plugins/catalog-graph/README.md index b9400e8e8b..b411d4d868 100644 --- a/plugins/catalog-graph/README.md +++ b/plugins/catalog-graph/README.md @@ -50,18 +50,6 @@ To use the catalog graph plugin, you have to add some things to your Backstage a } @@ -88,7 +76,7 @@ To use the catalog graph plugin, you have to add some things to your Backstage a ``` -### Customization +### Customizing the UI Copy the default implementation `DefaultRenderNode.tsx` and add more classes to the styles: @@ -151,6 +139,44 @@ Once you have your custom implementation, you can follow these steps to modify t ``` +### Custom relations + +Implementers with added custom relations can add them to the catalog graph plugin by overriding the default API. This also allows some relations to not be selected by default. + +In `packages/app/src/apis.ts`, import the api ref and create the API as: + +```ts +import { + ALL_RELATIONS, + ALL_RELATION_PAIRS, + catalogGraphApiRef, + DefaultCatalogGraphApi, +} from '@backstage/plugin-catalog-graph'; + +// ... + + createApiFactory({ + api: catalogGraphApiRef, + deps: {}, + factory: () => + new DefaultCatalogGraphApi({ + // The relations to support + knownRelations: [...ALL_RELATIONS, 'myRelationOf', 'myRelationFor'], + // The relation pairs to support + knownRelationPairs: [ + ...ALL_RELATION_PAIRS, + ['myRelationOf', 'myRelationFor'], + ], + // Select what relations to be shown by default, either by including them, + // or excluding some from all known relations: + defaultRelationTypes: { + // Don't show/select these by default + exclude: ['myRelationOf', 'myRelationFor'], + }, + }), + }), +``` + ## Development Run `yarn` in the root of this plugin to install all dependencies and then `yarn start` to run a [development version](./dev/index.tsx) of this plugin. diff --git a/plugins/catalog-graph/knip-report.md b/plugins/catalog-graph/knip-report.md index 6b1996899f..92dd638989 100644 --- a/plugins/catalog-graph/knip-report.md +++ b/plugins/catalog-graph/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------ | :----------- | :------- | -| p-limit | package.json | error | +| p-limit | plugins/catalog-graph/package.json | error | diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index dd3271f468..86bd930095 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.4.23-next.1", + "version": "0.5.0", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-graph", diff --git a/plugins/catalog-graph/report-alpha.api.md b/plugins/catalog-graph/report-alpha.api.md index 3ac88050ba..fe852d8cf6 100644 --- a/plugins/catalog-graph/report-alpha.api.md +++ b/plugins/catalog-graph/report-alpha.api.md @@ -84,9 +84,9 @@ const _default: OverridableFrontendPlugin< relations?: string[] | undefined; maxDepth?: number | undefined; kinds?: string[] | undefined; - unidirectional?: boolean | undefined; mergeRelations?: boolean | undefined; relationPairs?: [string, string][] | undefined; + unidirectional?: boolean | undefined; } & { filter?: EntityPredicate | undefined; type?: 'content' | 'summary' | 'info' | undefined; @@ -157,9 +157,9 @@ const _default: OverridableFrontendPlugin< rootEntityRefs?: string[] | undefined; maxDepth?: number | undefined; kinds?: string[] | undefined; - unidirectional?: boolean | undefined; mergeRelations?: boolean | undefined; relationPairs?: [string, string][] | undefined; + unidirectional?: boolean | undefined; selectedRelations?: string[] | undefined; selectedKinds?: string[] | undefined; showFilters?: boolean | undefined; diff --git a/plugins/catalog-graph/report.api.md b/plugins/catalog-graph/report.api.md index 25479bd9b7..8194209cf5 100644 --- a/plugins/catalog-graph/report.api.md +++ b/plugins/catalog-graph/report.api.md @@ -3,6 +3,7 @@ > 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 { CompoundEntityRef } from '@backstage/catalog-model'; import { DependencyGraphTypes } from '@backstage/core-components'; @@ -19,6 +20,19 @@ import { RouteRef } from '@backstage/core-plugin-api'; // @public export const ALL_RELATION_PAIRS: RelationPairs; +// @public +export const ALL_RELATIONS: string[]; + +// @public +export interface CatalogGraphApi { + readonly defaultRelations: string[]; + readonly knownRelationPairs: [string, string][]; + readonly knownRelations: string[]; +} + +// @public +export const catalogGraphApiRef: ApiRef; + // @public export const CatalogGraphPage: ( props: { @@ -62,6 +76,42 @@ export type CustomLabelClassKey = 'text' | 'secondary'; // @public (undocumented) export type CustomNodeClassKey = 'node' | 'text' | 'clickable'; +// @public +export class DefaultCatalogGraphApi implements CatalogGraphApi { + constructor(options?: DefaultCatalogGraphApiOptions); + // (undocumented) + readonly defaultRelations: string[]; + // (undocumented) + readonly knownRelationPairs: [string, string][]; + // (undocumented) + readonly knownRelations: string[]; +} + +// @public +export interface DefaultCatalogGraphApiOptions { + // (undocumented) + readonly defaultRelationTypes?: DefaultRelations; + // (undocumented) + readonly knownRelationPairs?: RelationPairs; + // (undocumented) + readonly knownRelations?: string[]; +} + +// @public +export type DefaultRelations = + | DefaultRelationsInclude + | DefaultRelationsExclude; + +// @public (undocumented) +export type DefaultRelationsExclude = { + exclude: string[]; +}; + +// @public (undocumented) +export type DefaultRelationsInclude = { + include: string[]; +}; + // @public export enum Direction { BOTTOM_TOP = 'BT', @@ -135,4 +185,19 @@ export type EntityRelationsGraphProps = { // @public export type RelationPairs = [string, string][]; + +// @public +export interface TransformationContext { + // (undocumented) + edges: EntityEdge[]; + // (undocumented) + maxDepth: number; + nodeDistances: Map; + // (undocumented) + nodes: EntityNode[]; + // (undocumented) + rootEntityRefs: string[]; + // (undocumented) + unidirectional: boolean; +} ``` diff --git a/plugins/catalog-graph/src/api/CatalogGraphApi.ts b/plugins/catalog-graph/src/api/CatalogGraphApi.ts new file mode 100644 index 0000000000..33d994e0c7 --- /dev/null +++ b/plugins/catalog-graph/src/api/CatalogGraphApi.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApiRef } from '@backstage/core-plugin-api'; + +/** + * Utility API reference for the {@link CatalogGraphApi}. + * + * @public + */ +export const catalogGraphApiRef = createApiRef({ + id: 'plugin.catalog-graph.service', +}); + +/** @public */ +export type DefaultRelationsInclude = { include: string[] }; + +/** @public */ +export type DefaultRelationsExclude = { exclude: string[] }; + +/** + * Default relations. Can either be a list of relations to use by default, or + * a list of relations to _not_ use, i.e. include all other relations. + * + * @public + */ +export type DefaultRelations = + | DefaultRelationsInclude + | DefaultRelationsExclude; + +/** + * API for driving catalog imports. + * + * @public + */ +export interface CatalogGraphApi { + /** All known relations */ + readonly knownRelations: string[]; + + /** All known relation pairs */ + readonly knownRelationPairs: [string, string][]; + + /** The default relations to show in the graph */ + readonly defaultRelations: string[]; +} diff --git a/plugins/catalog-graph/src/api/DefaultCatalogGraphApi.test.ts b/plugins/catalog-graph/src/api/DefaultCatalogGraphApi.test.ts new file mode 100644 index 0000000000..63186868fd --- /dev/null +++ b/plugins/catalog-graph/src/api/DefaultCatalogGraphApi.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DefaultCatalogGraphApi } from './DefaultCatalogGraphApi'; + +describe('DefaultCatalogGraphApi', () => { + it('default config', async () => { + const { defaultRelations } = new DefaultCatalogGraphApi(); + + expect(defaultRelations.includes('')).toBe(false); + expect(defaultRelations.includes('fooRelation')).toBe(false); + expect(defaultRelations.includes('ownedBy')).toBe(true); + expect(defaultRelations.includes('hasPart')).toBe(true); + }); + + it('empty include config', async () => { + const { defaultRelations } = new DefaultCatalogGraphApi({ + defaultRelationTypes: { include: [] }, + }); + + expect(defaultRelations.includes('')).toBe(false); + expect(defaultRelations.includes('fooRelation')).toBe(false); + expect(defaultRelations.includes('ownedBy')).toBe(false); + expect(defaultRelations.includes('hasPart')).toBe(false); + }); + + it('include config', async () => { + const { defaultRelations } = new DefaultCatalogGraphApi({ + defaultRelationTypes: { include: ['ownedBy'] }, + }); + + expect(defaultRelations.includes('')).toBe(false); + expect(defaultRelations.includes('fooRelation')).toBe(false); + expect(defaultRelations.includes('ownedBy')).toBe(true); + expect(defaultRelations.includes('hasPart')).toBe(false); + }); + + it('exclude config', async () => { + const { defaultRelations } = new DefaultCatalogGraphApi({ + defaultRelationTypes: { exclude: ['ownedBy', 'ownerOf'] }, + }); + + expect(defaultRelations.includes('')).toBe(false); + expect(defaultRelations.includes('fooRelation')).toBe(false); + expect(defaultRelations.includes('ownedBy')).toBe(false); + expect(defaultRelations.includes('hasPart')).toBe(true); + }); +}); diff --git a/plugins/catalog-graph/src/api/DefaultCatalogGraphApi.ts b/plugins/catalog-graph/src/api/DefaultCatalogGraphApi.ts new file mode 100644 index 0000000000..edb7b8f509 --- /dev/null +++ b/plugins/catalog-graph/src/api/DefaultCatalogGraphApi.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ALL_RELATION_PAIRS, ALL_RELATIONS, RelationPairs } from '../lib/types'; +import { + CatalogGraphApi, + DefaultRelations, + DefaultRelationsExclude, + DefaultRelationsInclude, +} from './CatalogGraphApi'; + +/** + * Options for the {@link DefaultCatalogGraphApi}. + * + * @public + */ +export interface DefaultCatalogGraphApiOptions { + readonly knownRelations?: string[]; + readonly knownRelationPairs?: RelationPairs; + readonly defaultRelationTypes?: DefaultRelations; +} + +/** + * The default implementation of the {@link CatalogGraphApi}. + * + * @public + */ +export class DefaultCatalogGraphApi implements CatalogGraphApi { + readonly knownRelations: string[]; + readonly knownRelationPairs: [string, string][]; + readonly defaultRelations: string[]; + + constructor( + options: DefaultCatalogGraphApiOptions = { + knownRelations: ALL_RELATIONS, + knownRelationPairs: ALL_RELATION_PAIRS, + defaultRelationTypes: { exclude: [] }, + }, + ) { + this.knownRelations = options.knownRelations ?? ALL_RELATIONS; + this.knownRelationPairs = options.knownRelationPairs ?? ALL_RELATION_PAIRS; + + const defaultRelations = options.defaultRelationTypes; + + if (Array.isArray((defaultRelations as DefaultRelationsInclude).include)) { + const defaultRelationsInclude = + defaultRelations as DefaultRelationsInclude; + + this.defaultRelations = this.knownRelations.filter(rel => + defaultRelationsInclude.include.includes(rel), + ); + } else { + const defaultRelationsExclude = + defaultRelations as DefaultRelationsExclude; + + this.defaultRelations = this.knownRelations.filter( + rel => !defaultRelationsExclude.exclude.includes(rel), + ); + } + } +} diff --git a/plugins/catalog-graph/src/api/index.ts b/plugins/catalog-graph/src/api/index.ts new file mode 100644 index 0000000000..5bca33ce1a --- /dev/null +++ b/plugins/catalog-graph/src/api/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 './CatalogGraphApi'; +export * from './DefaultCatalogGraphApi'; diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx index ed8069a8f4..3ae8310280 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx @@ -35,6 +35,7 @@ import { catalogGraphRouteRef } from '../../routes'; import { CatalogGraphCard } from './CatalogGraphCard'; import Button from '@material-ui/core/Button'; import { translationApiRef } from '@backstage/core-plugin-api/alpha'; +import { catalogGraphApiRef, DefaultCatalogGraphApi } from '../../api'; describe('', () => { let entity: Entity; @@ -56,6 +57,7 @@ describe('', () => { apis = TestApiRegistry.from( [catalogApiRef, catalog], [translationApiRef, mockApis.translation()], + [catalogGraphApiRef, new DefaultCatalogGraphApi()], ); wrapper = ( diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx index 733f209990..2f07b89d3f 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx @@ -32,14 +32,12 @@ import { MouseEvent, ReactNode, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { catalogGraphRouteRef } from '../../routes'; import { - ALL_RELATION_PAIRS, - Direction, - EntityNode, EntityRelationsGraph, EntityRelationsGraphProps, } from '../EntityRelationsGraph'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { catalogGraphTranslationRef } from '../../translation'; +import { Direction, EntityNode } from '../../lib/types'; /** @public */ export type CatalogGraphCardClassKey = 'card' | 'graph'; @@ -71,7 +69,7 @@ export const CatalogGraphCard = ( const { t } = useTranslationRef(catalogGraphTranslationRef); const { variant = 'gridItem', - relationPairs = ALL_RELATION_PAIRS, + relationPairs, maxDepth = 1, unidirectional = true, mergeRelations = true, diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx index 69cb04bf3a..7e6420da1f 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx @@ -37,9 +37,6 @@ import ToggleButton from '@material-ui/lab/ToggleButton'; import { MouseEvent, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { - ALL_RELATION_PAIRS, - Direction, - EntityNode, EntityRelationsGraph, EntityRelationsGraphProps, } from '../EntityRelationsGraph'; @@ -52,6 +49,7 @@ import { SwitchFilter } from './SwitchFilter'; import { useCatalogGraphPage } from './useCatalogGraphPage'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { catalogGraphTranslationRef } from '../../translation'; +import { Direction, EntityNode } from '../../lib/types'; /** @public */ export type CatalogGraphPageClassKey = @@ -133,11 +131,7 @@ export const CatalogGraphPage = ( }; } & Partial, ) => { - const { - relationPairs = ALL_RELATION_PAIRS, - initialState, - entityFilter, - } = props; + const { relationPairs, initialState, entityFilter } = props; const { t } = useTranslationRef(catalogGraphTranslationRef); const navigate = useNavigate(); const classes = useStyles(); @@ -224,7 +218,6 @@ export const CatalogGraphPage = ( diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx index 1262797a43..d5cee68971 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx @@ -16,7 +16,7 @@ import { waitFor, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { Direction } from '../EntityRelationsGraph'; +import { Direction } from '../../lib/types'; import { DirectionFilter } from './DirectionFilter'; import { renderInTestApp } from '@backstage/test-utils'; diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.tsx index 5d21f61c6c..060a1d32c6 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.tsx @@ -16,7 +16,7 @@ import { Select, SelectedItems } from '@backstage/core-components'; import Box from '@material-ui/core/Box'; import { useCallback } from 'react'; -import { Direction } from '../EntityRelationsGraph'; +import { Direction } from '../../lib/types'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { catalogGraphTranslationRef } from '../../translation'; diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx index 057842bcdc..09e09ee822 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx @@ -14,6 +14,8 @@ * limitations under the License. */ +import { PropsWithChildren } from 'react'; +import { ApiProvider } from '@backstage/core-app-api'; import { RELATION_CHILD_OF, RELATION_HAS_MEMBER, @@ -21,18 +23,34 @@ import { } from '@backstage/catalog-model'; import { waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { ALL_RELATION_PAIRS } from '../EntityRelationsGraph'; +import { ALL_RELATIONS } from '../../lib/types'; import { SelectedRelationsFilter } from './SelectedRelationsFilter'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; +import { catalogGraphApiRef, DefaultCatalogGraphApi } from '../../api'; + +function GraphContext(props: PropsWithChildren<{}>) { + return ( + + {props.children} + + ); +} describe('', () => { test('should render current value', async () => { await renderInTestApp( - {}} - />, + + {}} + /> + , ); expect(screen.getByText(RELATION_OWNED_BY)).toBeInTheDocument(); @@ -42,11 +60,13 @@ describe('', () => { test('should select value', async () => { const onChange = jest.fn(); await renderInTestApp( - , + + + , ); await userEvent.click(screen.getByLabelText('Open')); @@ -66,16 +86,16 @@ describe('', () => { }); }); - test('should return undefined if all values are selected', async () => { + test('should return all known relations if all values are selected', async () => { const onChange = jest.fn(); await renderInTestApp( - p).filter( - r => r !== RELATION_HAS_MEMBER, - )} - onChange={onChange} - />, + + r !== RELATION_HAS_MEMBER)} + onChange={onChange} + /> + , ); await userEvent.click(screen.getByLabelText('Open')); @@ -87,18 +107,26 @@ describe('', () => { await userEvent.click(screen.getByText(RELATION_HAS_MEMBER)); await waitFor(() => { - expect(onChange).toHaveBeenCalledWith(undefined); + // Same as ALL_RELATIONS but with RELATION_HAS_MEMBER at the end + const allRelationsOrdered = [ + ...ALL_RELATIONS.filter(rel => rel !== RELATION_HAS_MEMBER), + RELATION_HAS_MEMBER, + ]; + + expect(onChange).toHaveBeenCalledWith(allRelationsOrdered); }); }); test('should return all values when cleared', async () => { const onChange = jest.fn(); await renderInTestApp( - , + + + , ); await userEvent.click(screen.getByRole('combobox')); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.tsx index 9ded3def03..aa5fa026d4 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.tsx @@ -24,9 +24,9 @@ import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import Autocomplete from '@material-ui/lab/Autocomplete'; import { useCallback, useMemo } from 'react'; -import { RelationPairs } from '../EntityRelationsGraph'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { catalogGraphTranslationRef } from '../../translation'; +import { useRelations } from '../../hooks'; /** @public */ export type SelectedRelationsFilterClassKey = 'formControl'; @@ -41,25 +41,31 @@ const useStyles = makeStyles( ); export type Props = { - relationPairs: RelationPairs; + relations?: string[]; value: string[] | undefined; onChange: (value: string[] | undefined) => void; }; -export const SelectedRelationsFilter = ({ - relationPairs, - value, - onChange, -}: Props) => { +export const SelectedRelationsFilter = (props: Props) => { + const { relations: incomingRelations, value, onChange } = props; + const classes = useStyles(); - const relations = useMemo(() => relationPairs.flat(), [relationPairs]); + + const { relations, includeRelation } = useRelations({ + relations: incomingRelations, + }); + + const defaultValue = useMemo( + () => relations.filter(rel => includeRelation(rel)), + [relations, includeRelation], + ); const { t } = useTranslationRef(catalogGraphTranslationRef); const handleChange = useCallback( (_: unknown, v: string[]) => { - onChange(relations.every(r => v.includes(r)) ? undefined : v); + onChange(v); }, - [relations, onChange], + [onChange], ); const handleEmpty = useCallback(() => { @@ -78,7 +84,7 @@ export const SelectedRelationsFilter = ({ disableCloseOnSelect aria-label={t('catalogGraphPage.selectedRelationsFilter.title')} options={relations} - value={value ?? relations} + value={value ?? defaultValue} onChange={handleChange} onBlur={handleEmpty} renderOption={(option, { selected }) => ( diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.test.tsx index c255281ff0..fa3d67520d 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.test.tsx @@ -18,7 +18,7 @@ import { renderHook, waitFor } from '@testing-library/react'; import { ReactNode } from 'react'; import { act } from 'react-dom/test-utils'; import { BrowserRouter } from 'react-router-dom'; -import { Direction } from '../EntityRelationsGraph'; +import { Direction } from '../../lib/types'; import { useCatalogGraphPage } from './useCatalogGraphPage'; const wrapper = ({ children }: { children?: ReactNode }) => { diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts b/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts index 1f6fb74997..efc2170153 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts @@ -28,7 +28,7 @@ import { useState, } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import { Direction } from '../EntityRelationsGraph'; +import { Direction } from '../../lib/types'; export type CatalogGraphPageValue = { rootEntityNames: CompoundEntityRef[]; diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderLabel.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderLabel.tsx index a38f11fca3..c9ddddb4b3 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderLabel.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderLabel.tsx @@ -15,7 +15,7 @@ */ import { DependencyGraphTypes } from '@backstage/core-components'; import makeStyles from '@material-ui/core/styles/makeStyles'; -import { EntityEdgeData } from './types'; +import { EntityEdgeData } from '../../lib/types'; import classNames from 'classnames'; /** @public */ diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderNode.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderNode.tsx index 3541e4511e..97c17af3ec 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderNode.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderNode.tsx @@ -20,7 +20,7 @@ import { makeStyles } from '@material-ui/core/styles'; import classNames from 'classnames'; import { useLayoutEffect, useRef, useState } from 'react'; import { EntityIcon } from './EntityIcon'; -import { EntityNodeData } from './types'; +import { EntityNodeData } from '../../lib/types'; import { DEFAULT_NAMESPACE } from '@backstage/catalog-model'; /** @public */ diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx index 933aa230be..f77ca4b37d 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx @@ -30,8 +30,8 @@ import classNames from 'classnames'; import { MouseEvent, useEffect, useMemo } from 'react'; import { DefaultRenderLabel } from './DefaultRenderLabel'; import { DefaultRenderNode } from './DefaultRenderNode'; -import { ALL_RELATION_PAIRS, RelationPairs } from './relations'; -import { Direction, EntityEdge, EntityNode } from './types'; +import { RelationPairs } from '../../lib/types'; +import { Direction, EntityEdge, EntityNode } from '../../lib/types'; import { useEntityRelationNodesAndEdges } from './useEntityRelationNodesAndEdges'; /** @public */ @@ -109,7 +109,7 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => { entityFilter, direction = Direction.LEFT_RIGHT, onNodeClick, - relationPairs = ALL_RELATION_PAIRS, + relationPairs, className, zoom = 'enabled', renderNode, diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts index 7477d22f87..4a325591ca 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts @@ -18,14 +18,5 @@ export type { EntityRelationsGraphProps, EntityRelationsGraphClassKey, } from './EntityRelationsGraph'; -export { ALL_RELATION_PAIRS } from './relations'; -export type { RelationPairs } from './relations'; -export { Direction } from './types'; -export type { - EntityEdgeData, - EntityEdge, - EntityNodeData, - EntityNode, -} from './types'; export type { CustomLabelClassKey } from './DefaultRenderLabel'; export type { CustomNodeClassKey } from './DefaultRenderNode'; diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.tsx similarity index 78% rename from plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.ts rename to plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.tsx index b2d9083180..26feb6f597 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.tsx @@ -13,16 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { PropsWithChildren } from 'react'; +import { ApiProvider } from '@backstage/core-app-api'; import { RELATION_HAS_PART, RELATION_OWNED_BY, RELATION_OWNER_OF, RELATION_PART_OF, } from '@backstage/catalog-model'; +import { TestApiRegistry } from '@backstage/test-utils'; import { renderHook } from '@testing-library/react'; import { pick } from 'lodash'; import { useEntityRelationGraph } from './useEntityRelationGraph'; import { useEntityStore as useEntityStoreMocked } from './useEntityStore'; +import { catalogGraphApiRef, DefaultCatalogGraphApi } from '../../api'; jest.mock('./useEntityStore'); @@ -30,6 +34,19 @@ const useEntityStore = useEntityStoreMocked as jest.Mock< ReturnType >; +function GraphContext(props: PropsWithChildren<{}>) { + return ( + + {props.children} + + ); +} + describe('useEntityRelationGraph', () => { const requestEntities = jest.fn(); @@ -171,8 +188,9 @@ describe('useEntityRelationGraph', () => { requestEntities, }); - const { result } = renderHook(() => - useEntityRelationGraph({ rootEntityRefs: [] }), + const { result } = renderHook( + () => useEntityRelationGraph({ rootEntityRefs: [] }), + { wrapper: GraphContext }, ); const { entities, loading, error } = result.current; @@ -190,8 +208,9 @@ describe('useEntityRelationGraph', () => { requestEntities, }); - const { result } = renderHook(() => - useEntityRelationGraph({ rootEntityRefs: [] }), + const { result } = renderHook( + () => useEntityRelationGraph({ rootEntityRefs: [] }), + { wrapper: GraphContext }, ); const { entities, loading, error } = result.current; @@ -210,8 +229,9 @@ describe('useEntityRelationGraph', () => { requestEntities, }); - const { result } = renderHook(() => - useEntityRelationGraph({ rootEntityRefs: [] }), + const { result } = renderHook( + () => useEntityRelationGraph({ rootEntityRefs: [] }), + { wrapper: GraphContext }, ); const { entities, loading, error } = result.current; @@ -222,8 +242,9 @@ describe('useEntityRelationGraph', () => { }); test('should walk relation tree', async () => { - const { result, rerender } = renderHook(() => - useEntityRelationGraph({ rootEntityRefs: ['b:d/c'] }), + const { result, rerender } = renderHook( + () => useEntityRelationGraph({ rootEntityRefs: ['b:d/c'] }), + { wrapper: GraphContext }, ); // Simulate rerendering as this is triggered automatically due to the mock @@ -256,11 +277,13 @@ describe('useEntityRelationGraph', () => { }); test('should limit max depth', async () => { - const { result, rerender } = renderHook(() => - useEntityRelationGraph({ - rootEntityRefs: ['b:d/c'], - filter: { maxDepth: 1 }, - }), + const { result, rerender } = renderHook( + () => + useEntityRelationGraph({ + rootEntityRefs: ['b:d/c'], + filter: { maxDepth: 1 }, + }), + { wrapper: GraphContext }, ); // Simulate rerendering as this is triggered automatically due to the mock @@ -277,11 +300,13 @@ describe('useEntityRelationGraph', () => { test('should update on filter change', async () => { let maxDepth: number = Number.POSITIVE_INFINITY; - const { result, rerender } = renderHook(() => - useEntityRelationGraph({ - rootEntityRefs: ['b:d/c'], - filter: { maxDepth }, - }), + const { result, rerender } = renderHook( + () => + useEntityRelationGraph({ + rootEntityRefs: ['b:d/c'], + filter: { maxDepth }, + }), + { wrapper: GraphContext }, ); // Simulate rerendering as this is triggered automatically due to the mock @@ -310,13 +335,15 @@ describe('useEntityRelationGraph', () => { }); test('should filter by relation', async () => { - const { result, rerender } = renderHook(() => - useEntityRelationGraph({ - rootEntityRefs: ['b:d/c'], - filter: { - relations: [RELATION_HAS_PART, RELATION_PART_OF], - }, - }), + const { result, rerender } = renderHook( + () => + useEntityRelationGraph({ + rootEntityRefs: ['b:d/c'], + filter: { + relations: [RELATION_HAS_PART, RELATION_PART_OF], + }, + }), + { wrapper: GraphContext }, ); // Simulate rerendering as this is triggered automatically due to the mock @@ -332,14 +359,16 @@ describe('useEntityRelationGraph', () => { }); test('should filter by kind', async () => { - const { result, rerender } = renderHook(() => - useEntityRelationGraph({ - rootEntityRefs: ['b:d/c'], - filter: { - relations: [RELATION_OWNED_BY, RELATION_OWNER_OF], - kinds: ['k'], - }, - }), + const { result, rerender } = renderHook( + () => + useEntityRelationGraph({ + rootEntityRefs: ['b:d/c'], + filter: { + relations: [RELATION_OWNED_BY, RELATION_OWNER_OF], + kinds: ['k'], + }, + }), + { wrapper: GraphContext }, ); // Simulate rerendering as this is triggered automatically due to the mock @@ -354,13 +383,15 @@ describe('useEntityRelationGraph', () => { }); test('should filter by func', async () => { - const { result, rerender } = renderHook(() => - useEntityRelationGraph({ - rootEntityRefs: ['b:d/c'], - filter: { - entityFilter: e => e.metadata.name !== 'c2', - }, - }), + const { result, rerender } = renderHook( + () => + useEntityRelationGraph({ + rootEntityRefs: ['b:d/c'], + filter: { + entityFilter: e => e.metadata.name !== 'c2', + }, + }), + { wrapper: GraphContext }, ); // Simulate rerendering as this is triggered automatically due to the mock @@ -376,10 +407,12 @@ describe('useEntityRelationGraph', () => { }); test('should support multiple roots by kind', async () => { - const { result, rerender } = renderHook(() => - useEntityRelationGraph({ - rootEntityRefs: ['b:d/c', 'b:d/c2'], - }), + const { result, rerender } = renderHook( + () => + useEntityRelationGraph({ + rootEntityRefs: ['b:d/c', 'b:d/c2'], + }), + { wrapper: GraphContext }, ); // Simulate rerendering as this is triggered automatically due to the mock diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts index 07e3085f1f..1b99cfc074 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts @@ -17,6 +17,7 @@ import { Entity } from '@backstage/catalog-model'; import { useEffect, useMemo } from 'react'; import { useEntityStore } from './useEntityStore'; import { pickBy } from 'lodash'; +import { useRelations } from '../../hooks/useRelations'; /** * Discover the graph of entities connected by relations, starting from a set of @@ -45,6 +46,7 @@ export function useEntityRelationGraph({ error?: Error; } { const { entities, loading, error, requestEntities } = useEntityStore(); + const { includeRelation } = useRelations({ relations }); useEffect(() => { const expectedEntities = new Set([...rootEntityRefs]); @@ -74,7 +76,7 @@ export function useEntityRelationGraph({ } for (const rel of entity.relations) { if ( - (!relations || relations.includes(rel.type)) && + includeRelation(rel.type) && (!kinds || kinds.some(kind => rel.targetRef.startsWith( @@ -98,19 +100,21 @@ export function useEntityRelationGraph({ entities, rootEntityRefs, maxDepth, - relations, + includeRelation, kinds, entityFilter, requestEntities, ]); - const filteredEntities = useMemo( - () => - entityFilter - ? pickBy(entities, (value, _key) => entityFilter(value)) - : entities, - [entities, entityFilter], - ); + const filteredEntities = useMemo(() => { + if (loading) { + return {}; + } + + return entityFilter + ? pickBy(entities, (value, _key) => entityFilter(value)) + : entities; + }, [loading, entities, entityFilter]); return { entities: filteredEntities, diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.tsx similarity index 83% rename from plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.ts rename to plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.tsx index 08b718d789..ad70111fa0 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { PropsWithChildren } from 'react'; import { DEFAULT_NAMESPACE, Entity, @@ -22,11 +23,14 @@ import { RELATION_PART_OF, stringifyEntityRef, } from '@backstage/catalog-model'; +import { ApiProvider } from '@backstage/core-app-api'; +import { TestApiRegistry } from '@backstage/test-utils'; import { renderHook, waitFor } from '@testing-library/react'; import { filter, keyBy } from 'lodash'; import { useEntityRelationGraph as useEntityRelationGraphMocked } from './useEntityRelationGraph'; import { useEntityRelationNodesAndEdges } from './useEntityRelationNodesAndEdges'; -import { EntityNode } from './types'; +import { EntityNode } from '../../lib/types'; +import { catalogGraphApiRef, DefaultCatalogGraphApi } from '../../api'; jest.mock('./useEntityRelationGraph'); @@ -129,6 +133,19 @@ function deprecatedProperties(entity: Entity): Partial { }; } +function GraphContext(props: PropsWithChildren<{}>) { + return ( + + {props.children} + + ); +} + describe('useEntityRelationNodesAndEdges', () => { beforeEach(() => { useEntityRelationGraph.mockImplementation(({ filter: { kinds } }) => ({ @@ -149,10 +166,12 @@ describe('useEntityRelationNodesAndEdges', () => { loading: true, }); - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c'], - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c'], + }), + { wrapper: GraphContext }, ); const { nodes, edges, loading, error } = result.current; @@ -170,10 +189,12 @@ describe('useEntityRelationNodesAndEdges', () => { error: returnError, }); - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c'], - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c'], + }), + { wrapper: GraphContext }, ); const { nodes, edges, loading, error } = result.current; @@ -185,12 +206,14 @@ describe('useEntityRelationNodesAndEdges', () => { }); test('should generate unidirectional graph with merged relations', async () => { - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c'], - unidirectional: true, - mergeRelations: true, - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c'], + unidirectional: true, + mergeRelations: true, + }), + { wrapper: GraphContext }, ); await waitFor(() => { @@ -233,24 +256,21 @@ describe('useEntityRelationNodesAndEdges', () => { ]); expect(edges).toEqual([ { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_OWNER_OF, RELATION_OWNED_BY], to: 'k:d/a1', }, { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_HAS_PART, RELATION_PART_OF], to: 'b:d/c1', }, { - from: 'b:d/c1', - label: 'visible', - relations: [RELATION_OWNER_OF, RELATION_OWNED_BY], - to: 'k:d/a1', - }, - { + distance: 2, from: 'b:d/c1', label: 'visible', relations: [RELATION_HAS_PART, RELATION_PART_OF], @@ -260,12 +280,14 @@ describe('useEntityRelationNodesAndEdges', () => { }); test('should generate unidirectional graph', async () => { - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c'], - unidirectional: true, - mergeRelations: false, - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c'], + unidirectional: true, + mergeRelations: false, + }), + { wrapper: GraphContext }, ); await waitFor(() => { @@ -308,63 +330,38 @@ describe('useEntityRelationNodesAndEdges', () => { ]); expect(edges).toEqual([ { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_OWNER_OF], to: 'k:d/a1', }, { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_HAS_PART], to: 'b:d/c1', }, { - from: 'b:d/c1', - label: 'visible', - relations: [RELATION_PART_OF], - to: 'b:d/c', - }, - { - from: 'b:d/c1', - label: 'visible', - relations: [RELATION_OWNER_OF], - to: 'k:d/a1', - }, - { + distance: 2, from: 'b:d/c1', label: 'visible', relations: [RELATION_HAS_PART], to: 'b:d/c2', }, - { - from: 'b:d/c2', - label: 'visible', - relations: [RELATION_PART_OF], - to: 'b:d/c1', - }, - { - from: 'k:d/a1', - label: 'visible', - relations: [RELATION_OWNED_BY], - to: 'b:d/c', - }, - { - from: 'k:d/a1', - label: 'visible', - relations: [RELATION_OWNED_BY], - to: 'b:d/c1', - }, ]); }); test('should generate bidirectional graph with merged relations', async () => { - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c'], - unidirectional: false, - mergeRelations: true, - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c'], + unidirectional: false, + mergeRelations: true, + }), + { wrapper: GraphContext }, ); await waitFor(() => { @@ -407,24 +404,28 @@ describe('useEntityRelationNodesAndEdges', () => { ]); expect(edges).toEqual([ { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_OWNER_OF, RELATION_OWNED_BY], to: 'k:d/a1', }, { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_HAS_PART, RELATION_PART_OF], to: 'b:d/c1', }, { + distance: 2, from: 'b:d/c1', label: 'visible', relations: [RELATION_OWNER_OF, RELATION_OWNED_BY], to: 'k:d/a1', }, { + distance: 2, from: 'b:d/c1', label: 'visible', relations: [RELATION_HAS_PART, RELATION_PART_OF], @@ -434,12 +435,14 @@ describe('useEntityRelationNodesAndEdges', () => { }); test('should generate bidirectional graph with all relations', async () => { - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c'], - unidirectional: false, - mergeRelations: false, - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c'], + unidirectional: false, + mergeRelations: false, + }), + { wrapper: GraphContext }, ); await waitFor(() => { @@ -482,48 +485,56 @@ describe('useEntityRelationNodesAndEdges', () => { ]); expect(edges).toEqual([ { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_OWNER_OF], to: 'k:d/a1', }, { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_HAS_PART], to: 'b:d/c1', }, { + distance: 1, from: 'b:d/c1', label: 'visible', relations: [RELATION_PART_OF], to: 'b:d/c', }, { + distance: 2, from: 'b:d/c1', label: 'visible', relations: [RELATION_OWNER_OF], to: 'k:d/a1', }, { + distance: 2, from: 'b:d/c1', label: 'visible', relations: [RELATION_HAS_PART], to: 'b:d/c2', }, { + distance: 2, from: 'b:d/c2', label: 'visible', relations: [RELATION_PART_OF], to: 'b:d/c1', }, { + distance: 1, from: 'k:d/a1', label: 'visible', relations: [RELATION_OWNED_BY], to: 'b:d/c', }, { + distance: 2, from: 'k:d/a1', label: 'visible', relations: [RELATION_OWNED_BY], @@ -533,10 +544,12 @@ describe('useEntityRelationNodesAndEdges', () => { }); test('should generate graph with multiple root nodes', async () => { - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c', 'b:d/c2'], - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c', 'b:d/c2'], + }), + { wrapper: GraphContext }, ); await waitFor(() => { @@ -579,24 +592,21 @@ describe('useEntityRelationNodesAndEdges', () => { ]); expect(edges).toEqual([ { + distance: 1, from: 'b:d/c1', label: 'visible', relations: ['hasPart', 'partOf'], to: 'b:d/c2', }, { + distance: 1, from: 'b:d/c', label: 'visible', relations: ['hasPart', 'partOf'], to: 'b:d/c1', }, { - from: 'b:d/c1', - label: 'visible', - relations: ['ownerOf', 'ownedBy'], - to: 'k:d/a1', - }, - { + distance: 1, from: 'b:d/c', label: 'visible', relations: ['ownerOf', 'ownedBy'], @@ -606,11 +616,13 @@ describe('useEntityRelationNodesAndEdges', () => { }); test('should filter by relation', async () => { - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c'], - relations: [RELATION_OWNER_OF], - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c'], + relations: [RELATION_OWNER_OF], + }), + { wrapper: GraphContext }, ); await waitFor(() => { @@ -653,6 +665,7 @@ describe('useEntityRelationNodesAndEdges', () => { ]); expect(edges).toEqual([ { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_OWNER_OF, RELATION_OWNED_BY], @@ -662,11 +675,13 @@ describe('useEntityRelationNodesAndEdges', () => { }); test('should filter by kind', async () => { - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c'], - kinds: ['b'], - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c'], + kinds: ['b'], + }), + { wrapper: GraphContext }, ); await waitFor(() => { @@ -703,12 +718,14 @@ describe('useEntityRelationNodesAndEdges', () => { ]); expect(edges).toEqual([ { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_HAS_PART, RELATION_PART_OF], to: 'b:d/c1', }, { + distance: 2, from: 'b:d/c1', label: 'visible', relations: [RELATION_HAS_PART, RELATION_PART_OF], diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts index 19a6dbe566..7a6f8361e5 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts @@ -15,10 +15,18 @@ */ import { MouseEvent, useState } from 'react'; import useDebounce from 'react-use/esm/useDebounce'; -import { RelationPairs, ALL_RELATION_PAIRS } from './relations'; -import { EntityEdge, EntityNode } from './types'; +import { RelationPairs } from '../../lib/types'; +import { EntityEdge, EntityNode } from '../../lib/types'; import { useEntityRelationGraph } from './useEntityRelationGraph'; import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { useRelations } from '../../hooks/useRelations'; +import { buildGraph } from '../../lib/graph'; +import { + BuiltInTransformations, + builtInTransformations, + GraphTransformer, + TransformationContext, +} from '../../lib/graph-transformations'; /** * Generate nodes and edges to render the entity graph. @@ -32,7 +40,7 @@ export function useEntityRelationNodesAndEdges({ relations, entityFilter, onNodeClick, - relationPairs = ALL_RELATION_PAIRS, + relationPairs: incomingRelationPairs, }: { rootEntityRefs: string[]; maxDepth?: number; @@ -63,6 +71,11 @@ export function useEntityRelationNodesAndEdges({ }, }); + const { relationPairs, includeRelation } = useRelations({ + relations, + relationPairs: incomingRelationPairs, + }); + useDebounce( () => { if (!entities || Object.keys(entities).length === 0) { @@ -92,145 +105,64 @@ export function useEntityRelationNodesAndEdges({ return node; }); - const edges: EntityEdge[] = []; - const visitedNodes = new Set(); - const nodeQueue = [...rootEntityRefs]; + const edges = buildGraph({ + rootEntityRefs, + entities, + includeRelation, + kinds, + mergeRelations, + relationPairs, + unidirectional, + }); - const hasEdge = ( - fromRef: string, - toRef: string, - relation: [string, string?], - ): boolean => { - return edges.some( - edge => - fromRef === edge.from && - toRef === edge.to && - edge.relations.some( - rel => rel[0] === relation[0] && rel[1] === relation[1], - ), - ); + const transformationContext: TransformationContext = { + nodeDistances: new Map(), + edges, + nodes, + + rootEntityRefs, + unidirectional, + maxDepth, }; - while (nodeQueue.length > 0) { - const entityRef = nodeQueue.pop()!; - const entity = entities[entityRef]; - visitedNodes.add(entityRef); - - if (entity) { - entity?.relations?.forEach(rel => { - // Check if the related entity should be displayed, if not, ignore - // the relation too - if (!entities[rel.targetRef]) { - return; - } - - if (relations && !relations.includes(rel.type)) { - return; - } - - if ( - kinds && - !kinds.some(kind => - rel.targetRef.startsWith(`${kind.toLocaleLowerCase('en-US')}:`), - ) - ) { - return; - } - - if (mergeRelations) { - const pair = relationPairs.find( - ([l, r]) => l === rel.type || r === rel.type, - ) ?? [rel.type]; - const [left] = pair; - const from = left === rel.type ? entityRef : rel.targetRef; - const to = left === rel.type ? rel.targetRef : entityRef; - if (!unidirectional || !hasEdge(from, to, pair)) { - edges.push({ - from, - to, - relations: pair, - label: 'visible', - }); - } - } else { - if ( - !unidirectional || - !hasEdge(entityRef, rel.targetRef, [rel.type]) - ) { - edges.push({ - from: entityRef, - to: rel.targetRef, - relations: [rel.type], - label: 'visible', - }); - } - } - - if (!visitedNodes.has(rel.targetRef)) { - nodeQueue.push(rel.targetRef); - visitedNodes.add(rel.targetRef); - } - - // if unidirectional add missing relations as entities are only visited once - if (unidirectional) { - const findIndex = edges.findIndex( - edge => - entityRef === edge.from && - rel.targetRef === edge.to && - !edge.relations.includes(rel.type), - ); - if (findIndex >= 0) { - if (mergeRelations) { - const pair = relationPairs.find( - ([l, r]) => l === rel.type || r === rel.type, - ) ?? [rel.type]; - edges[findIndex].relations = [ - ...edges[findIndex].relations, - ...pair, - ]; - } else { - edges[findIndex].relations = [ - ...edges[findIndex].relations, - rel.type, - ]; - } - } - } - }); + const runTransformation = ( + transformation: BuiltInTransformations | GraphTransformer, + ) => { + if (typeof transformation === 'function') { + transformation(transformationContext); + } else { + builtInTransformations[transformation](transformationContext); } + }; + + runTransformation('reduce-edges'); + runTransformation('set-distances'); + if (unidirectional) { + runTransformation('strip-distant-edges'); + } + if (mergeRelations || unidirectional) { + // Merge relations even if only unidirectional, the next transformer + // 'remove-backward-edges' needs to know about all relations before it + // strips away the backward ones + runTransformation('merge-relations'); + } + if (unidirectional && !mergeRelations) { + runTransformation('order-forward'); + runTransformation('remove-backward-edges'); } - // Reduce edges as the dependency graph anyway ignores duplicated edges regarding from / to - // Additionally, this will improve rendering speed for the dependency graph - const finalEdges = edges.reduce((previousEdges, currentEdge) => { - const indexFound = previousEdges.findIndex( - previousEdge => - previousEdge.from === currentEdge.from && - previousEdge.to === currentEdge.to, - ); - if (indexFound >= 0) { - previousEdges[indexFound] = { - ...previousEdges[indexFound], - relations: Array.from( - new Set([ - ...previousEdges[indexFound].relations, - ...currentEdge.relations, - ]), - ), - }; - return previousEdges; - } - return [...previousEdges, currentEdge]; - }, [] as EntityEdge[]); - - setNodesAndEdges({ nodes, edges: finalEdges }); + setNodesAndEdges({ + nodes: transformationContext.nodes, + edges: transformationContext.edges, + }); }, 100, [ + maxDepth, entities, rootEntityRefs, kinds, - relations, + includeRelation, unidirectional, mergeRelations, onNodeClick, diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts index c55becdd49..5880468317 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts @@ -16,7 +16,7 @@ import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { Dispatch, useCallback, useRef, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import useAsyncFn from 'react-use/esm/useAsyncFn'; // TODO: This is a good use case for a graphql API, once it is available in the @@ -29,7 +29,7 @@ export function useEntityStore(): { entities: { [ref: string]: Entity }; loading: boolean; error?: Error; - requestEntities: Dispatch; + requestEntities: (entityRefs: string[]) => void; } { const catalogClient = useApi(catalogApiRef); const state = useRef({ diff --git a/plugins/catalog-graph/src/hooks/index.ts b/plugins/catalog-graph/src/hooks/index.ts new file mode 100644 index 0000000000..6cc875631d --- /dev/null +++ b/plugins/catalog-graph/src/hooks/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { UseRelationsOptions, UseRelationsResult } from './useRelations'; +export { useRelations } from './useRelations'; diff --git a/plugins/catalog-graph/src/hooks/useRelations.ts b/plugins/catalog-graph/src/hooks/useRelations.ts new file mode 100644 index 0000000000..2c734a2cb1 --- /dev/null +++ b/plugins/catalog-graph/src/hooks/useRelations.ts @@ -0,0 +1,77 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useCallback, useMemo } from 'react'; + +import { useApi } from '@backstage/core-plugin-api'; + +import { catalogGraphApiRef } from '../api'; +import { RelationPairs } from '../lib/types'; + +export interface UseRelationsOptions { + relations?: string[]; + relationPairs?: RelationPairs; +} + +export interface UseRelationsResult { + relations: string[]; + relationPairs: RelationPairs; + defaultRelations: string[]; + relationsToInclude: string[]; + includeRelation: (type: string) => boolean; +} + +/** + * Given an optional list of specific relations and/or relation pairs, + * this hook returns the relations, relation pairs and default relations (to + * show/select), falling back to the configured setting. + * It also returns a function `includeRelation` to filter whether a relation + * should be included or not. + */ +export function useRelations( + opts: UseRelationsOptions = {}, +): UseRelationsResult { + const { knownRelations, knownRelationPairs, defaultRelations } = + useApi(catalogGraphApiRef); + + const relations = opts.relations ?? knownRelations; + const relationPairs = opts.relationPairs ?? knownRelationPairs; + const relationsToInclude = opts.relations ?? defaultRelations; + + const includeRelation = useCallback( + (type: string) => { + return relationsToInclude.includes(type); + }, + [relationsToInclude], + ); + + return useMemo( + () => ({ + relations, + relationPairs, + defaultRelations, + relationsToInclude, + includeRelation, + }), + [ + relations, + relationPairs, + defaultRelations, + relationsToInclude, + includeRelation, + ], + ); +} diff --git a/plugins/catalog-graph/src/index.ts b/plugins/catalog-graph/src/index.ts index af81b076f2..95ac5048cb 100644 --- a/plugins/catalog-graph/src/index.ts +++ b/plugins/catalog-graph/src/index.ts @@ -23,5 +23,16 @@ export * from './components'; export { CatalogGraphPage, EntityCatalogGraphCard } from './extensions'; +export * from './api'; export { catalogGraphPlugin } from './plugin'; export { catalogGraphRouteRef } from './routes'; +export { ALL_RELATIONS, ALL_RELATION_PAIRS } from './lib/types'; +export type { + RelationPairs, + EntityEdgeData, + EntityEdge, + EntityNodeData, + EntityNode, +} from './lib/types'; +export { Direction } from './lib/types'; +export type { TransformationContext } from './lib/graph-transformations'; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/index.ts b/plugins/catalog-graph/src/lib/graph-transformations/index.ts new file mode 100644 index 0000000000..764a20a8d2 --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph-transformations/index.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { GraphTransformer, TransformationContext } from './types'; + +import { reduceEdges } from './reduce-edges'; +import { setDistances } from './set-distance'; +import { orderForward } from './order-forward'; +import { stripDistantEdges } from './strip-distant-edges'; +import { mergeRelations } from './merge-relations'; +import { removeBackwardEdges } from './remove-backward-edges'; + +export const builtInTransformations = { + 'reduce-edges': reduceEdges, + 'set-distances': setDistances, + 'order-forward': orderForward, + 'strip-distant-edges': stripDistantEdges, + 'merge-relations': mergeRelations, + 'remove-backward-edges': removeBackwardEdges, +}; + +export type BuiltInTransformations = keyof typeof builtInTransformations; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/merge-relations.ts b/plugins/catalog-graph/src/lib/graph-transformations/merge-relations.ts new file mode 100644 index 0000000000..88d4bbcd10 --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph-transformations/merge-relations.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityEdge } from '../types'; +import { GraphTransformer } from './types'; + +function concatRelations( + a: readonly string[] | undefined, + b: readonly string[] | undefined, +): string[] { + return Array.from(new Set([...(a ?? []), ...(b ?? [])])); +} + +/** Merge relations in multiple edges into one edge */ +export const mergeRelations: GraphTransformer = options => { + const mergedEdges = new Map(); + + options.edges.forEach(edge => { + const keyForward = `${edge.from}-${edge.to}`; + const keyBackward = `${edge.to}-${edge.from}`; + + const edgeForward = mergedEdges.get(keyForward); + const edgeBackward = mergedEdges.get(keyBackward); + + if (edgeForward) { + edgeForward.relations = concatRelations( + edgeForward.relations, + edge.relations, + ); + } else if (edgeBackward) { + edgeBackward.relations = concatRelations( + edgeBackward.relations, + edge.relations, + ); + } else { + mergedEdges.set(keyForward, edge); + } + }); + + options.edges = Array.from(mergedEdges.values()); +}; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/order-forward.ts b/plugins/catalog-graph/src/lib/graph-transformations/order-forward.ts new file mode 100644 index 0000000000..964c55e352 --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph-transformations/order-forward.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityEdge } from '../types'; +import { GraphTransformer } from './types'; + +/** Orders the edges direction so that the graph goes strictly forward */ +export const orderForward: GraphTransformer = ({ + rootEntityRefs, + nodes, + edges, +}) => { + const entityEdges = new Map(); + edges.forEach(edge => { + let fromEdges = entityEdges.get(edge.from); + if (!fromEdges) { + fromEdges = []; + entityEdges.set(edge.from, fromEdges); + } + fromEdges.push(edge); + + let toEdges = entityEdges.get(edge.to); + if (!toEdges) { + toEdges = []; + entityEdges.set(edge.to, toEdges); + } + toEdges.push(edge); + }); + + const visitedNodes = new Set(); + + for (const rootEntityRef of rootEntityRefs) { + let currentNodes: string[] = [rootEntityRef].filter( + node => !visitedNodes.has(node), + ); + + while (currentNodes.length > 0) { + for (const currentNode of currentNodes) { + visitedNodes.add(currentNode); + } + + const nextNodes: string[] = []; + + currentNodes.forEach(node => { + entityEdges.get(node)?.forEach(edge => { + if (edge.to === node && !visitedNodes.has(edge.from)) { + // Reverse direction + const { from, to } = edge; + edge.from = to; + edge.to = from; + edge.relations.reverse(); + } + + nextNodes.push(edge.from, edge.to); + }); + }); + + currentNodes = Array.from(new Set(nextNodes)).filter( + node => !visitedNodes.has(node), + ); + } + } + + const nodeOrder = Array.from(visitedNodes); + + nodes.sort((a, b) => { + const aOrder = nodeOrder.findIndex(node => node === a.id); + const bOrder = nodeOrder.findIndex(node => node === b.id); + return aOrder - bOrder; + }); +}; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/reduce-edges.ts b/plugins/catalog-graph/src/lib/graph-transformations/reduce-edges.ts new file mode 100644 index 0000000000..115aec438c --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph-transformations/reduce-edges.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityEdge } from '../types'; +import { GraphTransformer } from './types'; + +/** + * Reduce edges as the dependency graph anyway ignores duplicated edges + * regarding from / to + * Additionally, this will improve rendering speed for the dependency graph + */ +export const reduceEdges: GraphTransformer = ctx => { + ctx.edges = ctx.edges.reduce((previousEdges, currentEdge) => { + const indexFound = previousEdges.findIndex( + previousEdge => + previousEdge.from === currentEdge.from && + previousEdge.to === currentEdge.to, + ); + if (indexFound >= 0) { + previousEdges[indexFound] = { + ...previousEdges[indexFound], + relations: Array.from( + new Set([ + ...previousEdges[indexFound].relations, + ...currentEdge.relations, + ]), + ), + }; + return previousEdges; + } + return [...previousEdges, currentEdge]; + }, [] as EntityEdge[]); +}; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/remove-backward-edges.ts b/plugins/catalog-graph/src/lib/graph-transformations/remove-backward-edges.ts new file mode 100644 index 0000000000..2c6f634ba9 --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph-transformations/remove-backward-edges.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityEdge } from '../types'; +import { GraphTransformer } from './types'; + +/** + * Remove edges going backwards, if unidirectional and we haven't already + * merged the relations + */ +export const removeBackwardEdges: GraphTransformer = ctx => { + const { edges } = ctx; + + const edgesMapFrom = new Map(); + + edges.forEach(edge => { + const theseEdges = edgesMapFrom.get(edge.from) ?? []; + theseEdges.push(edge); + edgesMapFrom.set(edge.from, theseEdges); + }); + + edges.forEach(edge => { + if (edge.relations.length > 1) { + edge.relations = edge.relations.slice(0, 1); + } + }); +}; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/set-distance.ts b/plugins/catalog-graph/src/lib/graph-transformations/set-distance.ts new file mode 100644 index 0000000000..5986512305 --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph-transformations/set-distance.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityEdge } from '../types'; +import { GraphTransformer } from './types'; + +/** Set the distance of each edge to the distance from a root entity */ +export const setDistances: GraphTransformer = ({ + nodeDistances, + edges, + rootEntityRefs, +}) => { + const edgeMap = new Map(); + edges.forEach(edge => { + const fromEdges = edgeMap.get(edge.from) ?? []; + fromEdges.push(edge); + edgeMap.set(edge.from, fromEdges); + + const toEdges = edgeMap.get(edge.to) ?? []; + toEdges.push(edge); + edgeMap.set(edge.to, toEdges); + }); + + rootEntityRefs.forEach(rootEntityRef => { + nodeDistances.set(rootEntityRef, 0); + }); + const visitedNodes = new Set(rootEntityRefs); + let currentNodes = rootEntityRefs; + let distance = 0; + + while (currentNodes.length > 0) { + const thisDistance = ++distance; + + const nextNodes: string[] = []; + currentNodes.forEach(node => { + edgeMap.get(node)?.forEach(edge => { + edge.distance ??= thisDistance; + + const fromDistance = nodeDistances.get(edge.from) ?? thisDistance; + nodeDistances.set(edge.from, fromDistance); + nextNodes.push(edge.from); + + const toDistance = nodeDistances.get(edge.to) ?? thisDistance; + nodeDistances.set(edge.to, toDistance); + nextNodes.push(edge.to); + }); + }); + + for (const currentNode of currentNodes) { + visitedNodes.add(currentNode); + } + + currentNodes = Array.from(new Set(nextNodes)).filter( + node => !visitedNodes.has(node), + ); + } +}; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/strip-distant-edges.ts b/plugins/catalog-graph/src/lib/graph-transformations/strip-distant-edges.ts new file mode 100644 index 0000000000..aeb7618e5e --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph-transformations/strip-distant-edges.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityEdge } from '../types'; +import { GraphTransformer } from './types'; + +/** + * Removes edges that are further away then other edges leading up to the root. + */ +export const stripDistantEdges: GraphTransformer = options => { + const entityEdges = new Map(); + options.edges.forEach(edge => { + let fromEdges = entityEdges.get(edge.from); + if (!fromEdges) { + fromEdges = []; + entityEdges.set(edge.from, fromEdges); + } + fromEdges.push(edge); + + let toEdges = entityEdges.get(edge.to); + if (!toEdges) { + toEdges = []; + entityEdges.set(edge.to, toEdges); + } + toEdges.push(edge); + }); + + // Remove edges that are further away than other edges leading up to the root + options.edges = options.edges.filter(edge => { + // Get all edges of the 'from' node except this (and similar) edge + const fromEdges = entityEdges + .get(edge.from)! + .filter( + fromEdge => + !(fromEdge.from === edge.from && fromEdge.to === edge.to) && + !(fromEdge.from === edge.to && fromEdge.to === edge.from), + ); + // Get all edges of the 'from' node except this (and similar) edge + const toEdges = entityEdges + .get(edge.to)! + .filter( + toEdge => + !(toEdge.from === edge.from && toEdge.to === edge.to) && + !(toEdge.from === edge.to && toEdge.to === edge.from), + ); + + if (fromEdges.length === 0 || toEdges.length === 0) { + return true; + } + + const shorterFrom = fromEdges.some( + fromEdge => (fromEdge.distance ?? 0) < (edge.distance ?? 0), + ); + const shorterTo = toEdges.some( + toEdge => (toEdge.distance ?? 0) < (edge.distance ?? 0), + ); + + if (shorterFrom && shorterTo) { + // There are shorter edges from both 'from' and 'to' leading to the root, + // exclude this edge + return false; + } + return true; + }); +}; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/types.ts b/plugins/catalog-graph/src/lib/graph-transformations/types.ts new file mode 100644 index 0000000000..dc13057a04 --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph-transformations/types.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityEdge, EntityNode } from '../types'; + +/** + * Contextual information for a graph transformation. + * + * @public + */ +export interface TransformationContext { + /** + * The distance from an entity node to a root entity + * + * NOTE: This is empty until the 'set-distances' transformation is applied + */ + nodeDistances: Map; + + edges: EntityEdge[]; + nodes: EntityNode[]; + + // Options: + rootEntityRefs: string[]; + unidirectional: boolean; + maxDepth: number; +} + +/** + * A function that transforms a graph. The function modifies `nodes` and `edges` + * in place. + * + * @public + */ +export type GraphTransformer = (context: TransformationContext) => void; diff --git a/plugins/catalog-graph/src/lib/graph/build-graph.ts b/plugins/catalog-graph/src/lib/graph/build-graph.ts new file mode 100644 index 0000000000..3a6bf57120 --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph/build-graph.ts @@ -0,0 +1,151 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; + +import type { EntityEdge, RelationPairs } from '../types'; + +export interface BuildGraphOptions { + rootEntityRefs: readonly string[]; + entities: { + [ref: string]: Entity; + }; + includeRelation: (type: string) => boolean; + kinds: readonly string[] | undefined; + mergeRelations: boolean; + relationPairs: RelationPairs; + unidirectional: boolean; +} + +export function buildGraph({ + rootEntityRefs, + entities, + includeRelation, + kinds, + mergeRelations, + relationPairs, + unidirectional, +}: BuildGraphOptions): EntityEdge[] { + const edges: EntityEdge[] = []; + const visitedNodes = new Set(); + const nodeQueue = [...rootEntityRefs]; + + const hasEdge = ( + fromRef: string, + toRef: string, + relation: [string, string?], + ): boolean => { + return edges.some( + edge => + fromRef === edge.from && + toRef === edge.to && + edge.relations.some( + rel => rel[0] === relation[0] && rel[1] === relation[1], + ), + ); + }; + + while (nodeQueue.length > 0) { + const entityRef = nodeQueue.pop()!; + const entity = entities[entityRef]; + visitedNodes.add(entityRef); + + if (entity) { + entity?.relations?.forEach(rel => { + // Check if the related entity should be displayed, if not, ignore + // the relation too + if (!entities[rel.targetRef]) { + return; + } + + if (!includeRelation(rel.type)) { + return; + } + + if ( + kinds && + !kinds.some(kind => + rel.targetRef.startsWith(`${kind.toLocaleLowerCase('en-US')}:`), + ) + ) { + return; + } + + if (mergeRelations) { + const pair = relationPairs.find( + ([l, r]) => l === rel.type || r === rel.type, + ) ?? [rel.type]; + const [left] = pair; + const from = left === rel.type ? entityRef : rel.targetRef; + const to = left === rel.type ? rel.targetRef : entityRef; + if (!unidirectional || !hasEdge(from, to, pair)) { + edges.push({ + from, + to, + relations: pair, + label: 'visible', + }); + } + } else { + if ( + !unidirectional || + !hasEdge(entityRef, rel.targetRef, [rel.type]) + ) { + edges.push({ + from: entityRef, + to: rel.targetRef, + relations: [rel.type], + label: 'visible', + }); + } + } + + if (!visitedNodes.has(rel.targetRef)) { + nodeQueue.push(rel.targetRef); + visitedNodes.add(rel.targetRef); + } + + // if unidirectional add missing relations as entities are only visited once + if (unidirectional) { + const findIndex = edges.findIndex( + edge => + entityRef === edge.from && + rel.targetRef === edge.to && + !edge.relations.includes(rel.type), + ); + if (findIndex >= 0) { + if (mergeRelations) { + const pair = relationPairs.find( + ([l, r]) => l === rel.type || r === rel.type, + ) ?? [rel.type]; + edges[findIndex].relations = [ + ...edges[findIndex].relations, + ...pair, + ]; + } else { + edges[findIndex].relations = [ + ...edges[findIndex].relations, + rel.type, + ]; + } + } + } + }); + } + } + + return edges; +} diff --git a/plugins/catalog-graph/src/lib/graph/index.ts b/plugins/catalog-graph/src/lib/graph/index.ts new file mode 100644 index 0000000000..77741aa7d8 --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { buildGraph } from './build-graph'; +export type { BuildGraphOptions } from './build-graph'; diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/types.ts b/plugins/catalog-graph/src/lib/types/graph.ts similarity index 100% rename from plugins/catalog-graph/src/components/EntityRelationsGraph/types.ts rename to plugins/catalog-graph/src/lib/types/graph.ts diff --git a/plugins/catalog-graph/src/lib/types/index.ts b/plugins/catalog-graph/src/lib/types/index.ts new file mode 100644 index 0000000000..ba567d679a --- /dev/null +++ b/plugins/catalog-graph/src/lib/types/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { RelationPairs } from './relations'; +export { ALL_RELATIONS, ALL_RELATION_PAIRS } from './relations'; + +export type { + EntityEdgeData, + EntityEdge, + EntityNodeData, + EntityNode, +} from './graph'; +export { Direction } from './graph'; diff --git a/plugins/catalog-graph/src/lib/types/relations.ts b/plugins/catalog-graph/src/lib/types/relations.ts new file mode 100644 index 0000000000..d6a683cf4f --- /dev/null +++ b/plugins/catalog-graph/src/lib/types/relations.ts @@ -0,0 +1,77 @@ +/* + * 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 { + RELATION_API_CONSUMED_BY, + RELATION_API_PROVIDED_BY, + RELATION_CHILD_OF, + RELATION_CONSUMES_API, + RELATION_DEPENDENCY_OF, + RELATION_DEPENDS_ON, + RELATION_HAS_MEMBER, + RELATION_HAS_PART, + RELATION_MEMBER_OF, + RELATION_OWNED_BY, + RELATION_OWNER_OF, + RELATION_PARENT_OF, + RELATION_PART_OF, + RELATION_PROVIDES_API, +} from '@backstage/catalog-model'; + +/** + * A pair of two relations that describe the opposite of each other. The first + * relation is considered as the primary relation. + * + * @public + */ +export type RelationPairs = [string, string][]; + +/** + * A list of built-in relations + * + * @public + */ +export const ALL_RELATIONS: string[] = [ + RELATION_API_CONSUMED_BY, + RELATION_API_PROVIDED_BY, + RELATION_CHILD_OF, + RELATION_CONSUMES_API, + RELATION_DEPENDENCY_OF, + RELATION_DEPENDS_ON, + RELATION_HAS_MEMBER, + RELATION_HAS_PART, + RELATION_MEMBER_OF, + RELATION_OWNED_BY, + RELATION_OWNER_OF, + RELATION_PARENT_OF, + RELATION_PART_OF, + RELATION_PROVIDES_API, +]; + +/** + * A list of pairs of entity relations, used to define which relations are + * merged together and which the primary relation is. + * + * @public + */ +export const ALL_RELATION_PAIRS: RelationPairs = [ + [RELATION_OWNER_OF, RELATION_OWNED_BY], + [RELATION_CONSUMES_API, RELATION_API_CONSUMED_BY], + [RELATION_API_PROVIDED_BY, RELATION_PROVIDES_API], + [RELATION_HAS_PART, RELATION_PART_OF], + [RELATION_PARENT_OF, RELATION_CHILD_OF], + [RELATION_HAS_MEMBER, RELATION_MEMBER_OF], + [RELATION_DEPENDS_ON, RELATION_DEPENDENCY_OF], +]; diff --git a/plugins/catalog-graph/src/plugin.ts b/plugins/catalog-graph/src/plugin.ts index 7629caabee..570283c952 100644 --- a/plugins/catalog-graph/src/plugin.ts +++ b/plugins/catalog-graph/src/plugin.ts @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createPlugin } from '@backstage/core-plugin-api'; +import { createApiFactory, createPlugin } from '@backstage/core-plugin-api'; import { catalogEntityRouteRef, catalogGraphRouteRef } from './routes'; +import { catalogGraphApiRef, DefaultCatalogGraphApi } from './api'; /** * Catalog Graph Plugin instance. @@ -28,4 +29,11 @@ export const catalogGraphPlugin = createPlugin({ externalRoutes: { catalogEntity: catalogEntityRouteRef, }, + apis: [ + createApiFactory({ + api: catalogGraphApiRef, + deps: {}, + factory: () => new DefaultCatalogGraphApi(), + }), + ], }); diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 77ad27ced9..9c8778e168 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-catalog-import +## 0.13.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/integration@1.18.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + - @backstage/integration-react@1.2.10 + +## 0.13.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.13.5-next.1 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 050e8be908..97b7283ab9 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.13.5-next.1", + "version": "0.13.5", "description": "A Backstage plugin the helps you import entities into your catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/catalog-node/CHANGELOG.md b/plugins/catalog-node/CHANGELOG.md index 18f44ba88f..ba27f2a2e2 100644 --- a/plugins/catalog-node/CHANGELOG.md +++ b/plugins/catalog-node/CHANGELOG.md @@ -1,5 +1,74 @@ # @backstage/plugin-catalog-node +## 1.19.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- 2bbd24f: Order catalog processors by priority. + + This change enables the ordering of catalog processors by their priority, + allowing for more control over the catalog processing sequence. + The default priority is set to 20, and processors can be assigned a custom + priority to influence their execution order. Lower number indicates higher priority. + The priority can be set by implementing the `getPriority` method in the processor class + or by adding a `catalog.processors..priority` configuration + in the `app-config.yaml` file. The configuration takes precedence over the method. + +- Updated dependencies + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + +## 1.19.0-next.1 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + ## 1.18.1-next.0 ### Patch Changes diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index a61c4b801b..545656ae80 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-node", - "version": "1.18.1-next.0", + "version": "1.19.0", "description": "The plugin-catalog-node module for @backstage/plugin-catalog-backend", "backstage": { "role": "node-library", diff --git a/plugins/catalog-node/report.api.md b/plugins/catalog-node/report.api.md index 25ab9145ef..9a9f91865c 100644 --- a/plugins/catalog-node/report.api.md +++ b/plugins/catalog-node/report.api.md @@ -60,6 +60,7 @@ export type CatalogProcessor = { emit: CatalogProcessorEmit, cache: CatalogProcessorCache, ): Promise; + getPriority?(): number; }; // @public diff --git a/plugins/catalog-node/src/api/processor.ts b/plugins/catalog-node/src/api/processor.ts index e43fdef071..3aa5bf0fa7 100644 --- a/plugins/catalog-node/src/api/processor.ts +++ b/plugins/catalog-node/src/api/processor.ts @@ -100,6 +100,14 @@ export type CatalogProcessor = { emit: CatalogProcessorEmit, cache: CatalogProcessorCache, ): Promise; + + /** + * Returns a processor priority, which is used to determine the order in which + * processors are run. The default priority is 20, and lower value means + * that the processor runs earlier. + * @returns A number representing the priority of the processor. + */ + getPriority?(): number; }; /** diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index d6eb0dc5fe..f5e4d26638 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,74 @@ # @backstage/plugin-catalog-react +## 1.21.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- 0174799: Fix a potential race condition in EntityListProvider when selecting filters +- 4316c11: Catalog table columns support i18n +- 79ff318: Removed the deprecation warning when not passing an explicit type to `EntityCardBlueprint`. Omitting the type is now intended, allowing the layout to pick the default type instead, typically `content`. +- ad0f58d: Support `default*` for older packages as this package is in range for breaking `/alpha` changes +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/frontend-test-utils@0.3.6 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/integration-react@1.2.10 + +## 1.21.0-next.2 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 1.20.2-next.1 ### Patch Changes diff --git a/plugins/catalog-react/knip-report.md b/plugins/catalog-react/knip-report.md index 4b33c02956..d99b18f20d 100644 --- a/plugins/catalog-react/knip-report.md +++ b/plugins/catalog-react/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------------ | :----------- | :------- | -| react-test-renderer | package.json | error | +| react-test-renderer | plugins/catalog-react/package.json | error | diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 3912d9ce9d..dabfb55d11 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-react", - "version": "1.20.2-next.1", + "version": "1.21.0", "description": "A frontend library that helps other Backstage plugins interact with the catalog", "backstage": { "role": "web-library", diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 4e55240955..cf28d80c11 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -86,6 +86,18 @@ export const catalogReactTranslationRef: TranslationRef< readonly 'userListPicker.personalFilter.title': 'Personal'; readonly 'userListPicker.personalFilter.ownedLabel': 'Owned'; readonly 'userListPicker.personalFilter.starredLabel': 'Starred'; + readonly 'entityTableColumnTitle.name': 'Name'; + readonly 'entityTableColumnTitle.type': 'Type'; + readonly 'entityTableColumnTitle.label': 'Label'; + readonly 'entityTableColumnTitle.title': 'Title'; + readonly 'entityTableColumnTitle.description': 'Description'; + readonly 'entityTableColumnTitle.domain': 'Domain'; + readonly 'entityTableColumnTitle.system': 'System'; + readonly 'entityTableColumnTitle.tags': 'Tags'; + readonly 'entityTableColumnTitle.namespace': 'Namespace'; + readonly 'entityTableColumnTitle.lifecycle': 'Lifecycle'; + readonly 'entityTableColumnTitle.owner': 'Owner'; + readonly 'entityTableColumnTitle.targets': 'Targets'; } >; @@ -516,6 +528,40 @@ export type EntityPredicateValue = $contains: EntityPredicateExpression; }; +// @alpha (undocumented) +export const EntityTableColumnTitle: ({ + translationKey, +}: EntityTableColumnTitleProps) => + | 'Title' + | 'System' + | 'Domain' + | 'Lifecycle' + | 'Namespace' + | 'Owner' + | 'Tags' + | 'Type' + | 'Name' + | 'Description' + | 'Targets' + | 'Label'; + +// @alpha (undocumented) +export type EntityTableColumnTitleProps = { + translationKey: + | 'name' + | 'system' + | 'owner' + | 'type' + | 'lifecycle' + | 'namespace' + | 'description' + | 'tags' + | 'targets' + | 'title' + | 'label' + | 'domain'; +}; + // @alpha export function isOwnerOf(owner: Entity, entity: Entity): boolean; diff --git a/plugins/catalog-react/report.api.md b/plugins/catalog-react/report.api.md index bfebb2b13a..df32c51416 100644 --- a/plugins/catalog-react/report.api.md +++ b/plugins/catalog-react/report.api.md @@ -171,7 +171,7 @@ export const columnFactories: Readonly<{ defaultKind?: string; }): TableColumn; createEntityRelationColumn(options: { - title: string; + title: string | JSX.Element; relation: string; defaultKind?: string; filter?: { @@ -575,7 +575,7 @@ export const EntityTable: { defaultKind?: string; }): TableColumn; createEntityRelationColumn(options: { - title: string; + title: string | JSX.Element; relation: string; defaultKind?: string; filter?: { diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.ts b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.ts index b2f64c19e6..fed6ced70f 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.ts +++ b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.ts @@ -75,11 +75,6 @@ export const EntityCardBlueprint = createExtensionBlueprint({ const finalType = config.type ?? type; if (finalType) { yield entityCardTypeDataRef(finalType); - } else { - // eslint-disable-next-line no-console - console.warn( - `DEPRECATION WARNING: Not providing type for entity cards is deprecated. Missing from '${node.spec.id}'`, - ); } }, }); diff --git a/plugins/catalog-react/src/alpha/index.ts b/plugins/catalog-react/src/alpha/index.ts index 4ff4dbf0dd..f5909c9aa2 100644 --- a/plugins/catalog-react/src/alpha/index.ts +++ b/plugins/catalog-react/src/alpha/index.ts @@ -20,3 +20,4 @@ export * from './predicates'; export { catalogReactTranslationRef } from '../translation'; export { isOwnerOf } from '../utils/isOwnerOf'; export { useEntityPermission } from '../hooks/useEntityPermission'; +export * from '../components/EntityTable/TitleColumn'; diff --git a/plugins/catalog-react/src/components/EntityTable/TitleColumn.test.tsx b/plugins/catalog-react/src/components/EntityTable/TitleColumn.test.tsx new file mode 100644 index 0000000000..e08741d274 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityTable/TitleColumn.test.tsx @@ -0,0 +1,25 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { screen } from '@testing-library/react'; +import { EntityTableColumnTitle } from './TitleColumn'; +import { renderInTestApp } from '@backstage/test-utils'; + +describe('', () => { + it('renders the translated title for the given key', async () => { + await renderInTestApp(); + expect(screen.getByText('Name')).toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog-react/src/components/EntityTable/TitleColumn.tsx b/plugins/catalog-react/src/components/EntityTable/TitleColumn.tsx new file mode 100644 index 0000000000..36bce84882 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityTable/TitleColumn.tsx @@ -0,0 +1,47 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useTranslationRef } from '@backstage/frontend-plugin-api'; +import { catalogReactTranslationRef } from '../../translation'; + +/** + * @alpha + */ +export type EntityTableColumnTitleProps = { + translationKey: + | 'name' + | 'system' + | 'owner' + | 'type' + | 'lifecycle' + | 'namespace' + | 'description' + | 'tags' + | 'targets' + | 'title' + | 'label' + | 'domain'; +}; + +/** + * @alpha + */ +export const EntityTableColumnTitle = ({ + translationKey, +}: EntityTableColumnTitleProps) => { + const { t } = useTranslationRef(catalogReactTranslationRef); + return t(`entityTableColumnTitle.${translationKey}`); +}; diff --git a/plugins/catalog-react/src/components/EntityTable/columns.tsx b/plugins/catalog-react/src/components/EntityTable/columns.tsx index 00e1e5ad43..94b1141057 100644 --- a/plugins/catalog-react/src/components/EntityTable/columns.tsx +++ b/plugins/catalog-react/src/components/EntityTable/columns.tsx @@ -27,8 +27,7 @@ import { EntityRefLinks, humanizeEntityRef, } from '../EntityRefLink'; - -// TODO: column title support i18n +import { EntityTableColumnTitle } from './TitleColumn'; /** @public */ export const columnFactories = Object.freeze({ @@ -46,7 +45,7 @@ export const columnFactories = Object.freeze({ } return { - title: 'Name', + title: , highlight: true, customFilterAndSearch(filter, entity) { // TODO: We could implement this more efficiently, like searching over @@ -72,7 +71,7 @@ export const columnFactories = Object.freeze({ }; }, createEntityRelationColumn(options: { - title: string; + title: string | JSX.Element; relation: string; defaultKind?: string; filter?: { kind: string }; @@ -109,14 +108,14 @@ export const columnFactories = Object.freeze({ }, createOwnerColumn(): TableColumn { return this.createEntityRelationColumn({ - title: 'Owner', + title: , relation: RELATION_OWNED_BY, defaultKind: 'group', }); }, createDomainColumn(): TableColumn { return this.createEntityRelationColumn({ - title: 'Domain', + title: , relation: RELATION_PART_OF, defaultKind: 'domain', filter: { @@ -126,7 +125,7 @@ export const columnFactories = Object.freeze({ }, createSystemColumn(): TableColumn { return this.createEntityRelationColumn({ - title: 'System', + title: , relation: RELATION_PART_OF, defaultKind: 'system', filter: { @@ -136,7 +135,7 @@ export const columnFactories = Object.freeze({ }, createMetadataDescriptionColumn(): TableColumn { return { - title: 'Description', + title: , field: 'metadata.description', render: entity => ( (): TableColumn { return { - title: 'Lifecycle', + title: , field: 'spec.lifecycle', }; }, createSpecTypeColumn(): TableColumn { return { - title: 'Type', + title: , field: 'spec.type', }; }, diff --git a/plugins/catalog-react/src/translation.ts b/plugins/catalog-react/src/translation.ts index 7f9661380f..6e831886cd 100644 --- a/plugins/catalog-react/src/translation.ts +++ b/plugins/catalog-react/src/translation.ts @@ -124,5 +124,19 @@ export const catalogReactTranslationRef = createTranslationRef({ }, orgFilterAllLabel: 'All', }, + entityTableColumnTitle: { + name: 'Name', + system: 'System', + owner: 'Owner', + type: 'Type', + lifecycle: 'Lifecycle', + namespace: 'Namespace', + description: 'Description', + tags: 'Tags', + targets: 'Targets', + title: 'Title', + label: 'Label', + domain: 'Domain', + }, }, }); diff --git a/plugins/catalog-unprocessed-entities/CHANGELOG.md b/plugins/catalog-unprocessed-entities/CHANGELOG.md index 1542eafba3..31667429ca 100644 --- a/plugins/catalog-unprocessed-entities/CHANGELOG.md +++ b/plugins/catalog-unprocessed-entities/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-unprocessed-entities +## 0.2.21 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + ## 0.2.21-next.1 ### Patch Changes diff --git a/plugins/catalog-unprocessed-entities/knip-report.md b/plugins/catalog-unprocessed-entities/knip-report.md index ccda99cbfb..5f40723fab 100644 --- a/plugins/catalog-unprocessed-entities/knip-report.md +++ b/plugins/catalog-unprocessed-entities/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :--------------------- | :----------- | :------- | -| @testing-library/react | package.json | error | +| @testing-library/react | plugins/catalog-unprocessed-entities/package.json | error | diff --git a/plugins/catalog-unprocessed-entities/package.json b/plugins/catalog-unprocessed-entities/package.json index d220dd5b79..a2c1d617e5 100644 --- a/plugins/catalog-unprocessed-entities/package.json +++ b/plugins/catalog-unprocessed-entities/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-unprocessed-entities", - "version": "0.2.21-next.1", + "version": "0.2.21", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-unprocessed-entities", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 19c35e2137..8ae3b4c8da 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-catalog +## 1.31.3 + +### Patch Changes + +- 4316c11: Catalog table columns support i18n +- ce1239e: Auto-focus the first menu item in `EntityContextMenu`, and do not render a divider if an empty array is passed to `UNSTABLE_extraContextMenuItems`. +- 85c5e04: Fix incorrect `defaultTarget` on `createComponentRouteRef`. +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-search-react@1.9.4 + - @backstage/integration-react@1.2.10 + - @backstage/plugin-scaffolder-common@1.7.1 + +## 1.31.3-next.2 + +### Patch Changes + +- 85c5e04: Fix incorrect `defaultTarget` on `createComponentRouteRef`. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 1.31.3-next.1 ### Patch Changes diff --git a/plugins/catalog/knip-report.md b/plugins/catalog/knip-report.md index 7ceae94282..5efd01f3a6 100644 --- a/plugins/catalog/knip-report.md +++ b/plugins/catalog/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------ | :----------- | :------- | -| history | package.json | error | +| history | plugins/catalog/package.json | error | diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 9e1b0fcc05..2a7a956c37 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "1.31.3-next.1", + "version": "1.31.3", "description": "The Backstage plugin for browsing the Backstage catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx index 05674f3e1b..65094bc568 100644 --- a/plugins/catalog/src/components/CatalogTable/columns.tsx +++ b/plugins/catalog/src/components/CatalogTable/columns.tsx @@ -23,6 +23,7 @@ import { CatalogTableRow } from './types'; import { OverflowTooltip, TableColumn } from '@backstage/core-components'; import { Entity } from '@backstage/catalog-model'; import { JsonArray } from '@backstage/types'; +import { EntityTableColumnTitle } from '@backstage/plugin-catalog-react/alpha'; // The columnFactories symbol is not directly exported, but through the // CatalogTable.columns field. @@ -41,7 +42,7 @@ export const columnFactories = Object.freeze({ } return { - title: 'Name', + title: , field: 'resolved.entityRef', highlight: true, customSort({ entity: entity1 }, { entity: entity2 }) { @@ -59,7 +60,7 @@ export const columnFactories = Object.freeze({ }, createSystemColumn(): TableColumn { return { - title: 'System', + title: , field: 'resolved.partOfSystemRelationTitle', customFilterAndSearch: (query, row) => { if (!row.resolved.partOfSystemRelations) { @@ -83,7 +84,7 @@ export const columnFactories = Object.freeze({ }, createOwnerColumn(): TableColumn { return { - title: 'Owner', + title: , field: 'resolved.ownedByRelationsTitle', render: ({ resolved }) => ( { return { - title: 'Targets', + title: , field: 'entity.spec.targets', customFilterAndSearch: (query, row) => { let targets: JsonArray = []; @@ -132,7 +133,7 @@ export const columnFactories = Object.freeze({ } = { hidden: false }, ): TableColumn { return { - title: 'Type', + title: , field: 'entity.spec.type', hidden: options.hidden, width: 'auto', @@ -140,13 +141,13 @@ export const columnFactories = Object.freeze({ }, createSpecLifecycleColumn(): TableColumn { return { - title: 'Lifecycle', + title: , field: 'entity.spec.lifecycle', }; }, createMetadataDescriptionColumn(): TableColumn { return { - title: 'Description', + title: , field: 'entity.metadata.description', render: ({ entity }) => ( { return { - title: 'Tags', + title: , field: 'entity.metadata.tags', cellStyle: { padding: '0px 16px 0px 20px', @@ -185,7 +186,7 @@ export const columnFactories = Object.freeze({ hidden?: boolean; }): TableColumn { return { - title: 'Title', + title: , field: 'entity.metadata.title', hidden: options?.hidden, searchable: true, @@ -202,7 +203,9 @@ export const columnFactories = Object.freeze({ } return { - title: options?.title || 'Label', + title: options?.title || ( + + ), field: 'entity.metadata.labels', cellStyle: { padding: '0px 16px 0px 20px', @@ -235,7 +238,7 @@ export const columnFactories = Object.freeze({ }, createNamespaceColumn(): TableColumn { return { - title: 'Namespace', + title: , field: 'entity.metadata.namespace', width: 'auto', }; diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index 4f7f0427b1..6043ecd1a1 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -22,7 +22,7 @@ import { export const createComponentRouteRef = createExternalRouteRef({ id: 'create-component', optional: true, - defaultTarget: 'scaffolder.createComponent', + defaultTarget: 'scaffolder.root', }); export const viewTechDocRouteRef = createExternalRouteRef({ diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index 4ecd8c4953..74c7a68e58 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-config-schema +## 0.1.72 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + ## 0.1.72-next.0 ### Patch Changes diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index a1c61537bb..78addfd9c2 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-config-schema", - "version": "0.1.72-next.0", + "version": "0.1.72", "description": "A Backstage plugin that lets you browse the configuration schema of your app", "backstage": { "role": "frontend-plugin", diff --git a/plugins/devtools-backend/CHANGELOG.md b/plugins/devtools-backend/CHANGELOG.md index 4da746e10a..eeaeda6605 100644 --- a/plugins/devtools-backend/CHANGELOG.md +++ b/plugins/devtools-backend/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-devtools-backend +## 0.5.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/config-loader@1.10.3 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + +## 0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/backend-defaults@0.12.1-next.1 + ## 0.5.9-next.0 ### Patch Changes diff --git a/plugins/devtools-backend/knip-report.md b/plugins/devtools-backend/knip-report.md index dc1b572006..904730f95f 100644 --- a/plugins/devtools-backend/knip-report.md +++ b/plugins/devtools-backend/knip-report.md @@ -4,7 +4,7 @@ | Name | Location | Severity | | :-------------------------------- | :----------- | :------- | -| @backstage/plugin-permission-node | package.json | error | -| semver | package.json | error | -| yn | package.json | error | +| @backstage/plugin-permission-node | plugins/devtools-backend/package.json | error | +| semver | plugins/devtools-backend/package.json | error | +| yn | plugins/devtools-backend/package.json | error | diff --git a/plugins/devtools-backend/package.json b/plugins/devtools-backend/package.json index 6fafab972a..333a89d158 100644 --- a/plugins/devtools-backend/package.json +++ b/plugins/devtools-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-backend", - "version": "0.5.9-next.0", + "version": "0.5.9", "backstage": { "role": "backend-plugin", "pluginId": "devtools", diff --git a/plugins/devtools-react/src/devToolsContentBlueprint.tsx b/plugins/devtools-react/src/devToolsContentBlueprint.tsx index 835599dff3..e1c4c4ebfb 100644 --- a/plugins/devtools-react/src/devToolsContentBlueprint.tsx +++ b/plugins/devtools-react/src/devToolsContentBlueprint.tsx @@ -68,6 +68,7 @@ export const DevToolsContentBlueprint = createExtensionBlueprint({ }, }, *factory(params: DevToolsRouteBlueprintParams, { node, config }) { + console.log('DevToolsContentBlueprint', params, config); const path = config.path ?? params.path; const title = config.title ?? params.title; diff --git a/plugins/devtools/CHANGELOG.md b/plugins/devtools/CHANGELOG.md index 61c2b41fcd..de537019e3 100644 --- a/plugins/devtools/CHANGELOG.md +++ b/plugins/devtools/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-devtools +## 0.1.31 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + ## 0.1.31-next.1 ### Patch Changes diff --git a/plugins/devtools/package.json b/plugins/devtools/package.json index 5da086b0dc..3da6e8e02c 100644 --- a/plugins/devtools/package.json +++ b/plugins/devtools/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools", - "version": "0.1.31-next.1", + "version": "0.1.31", "backstage": { "role": "frontend-plugin", "pluginId": "devtools", diff --git a/plugins/events-backend-module-aws-sqs/CHANGELOG.md b/plugins/events-backend-module-aws-sqs/CHANGELOG.md index f390c44909..baca97880c 100644 --- a/plugins/events-backend-module-aws-sqs/CHANGELOG.md +++ b/plugins/events-backend-module-aws-sqs/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-backend-module-aws-sqs +## 0.4.15 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.15-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-aws-sqs/knip-report.md b/plugins/events-backend-module-aws-sqs/knip-report.md index c9ce845fd0..3f4259cf67 100644 --- a/plugins/events-backend-module-aws-sqs/knip-report.md +++ b/plugins/events-backend-module-aws-sqs/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------- | :----------- | :------- | -| @aws-sdk/types | package.json | error | +| @aws-sdk/types | plugins/events-backend-module-aws-sqs/package.json | error | diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json index 98fdcec09b..359cd768c8 100644 --- a/plugins/events-backend-module-aws-sqs/package.json +++ b/plugins/events-backend-module-aws-sqs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-aws-sqs", - "version": "0.4.15-next.0", + "version": "0.4.15", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-azure/CHANGELOG.md b/plugins/events-backend-module-azure/CHANGELOG.md index 00d075377e..91791d0966 100644 --- a/plugins/events-backend-module-azure/CHANGELOG.md +++ b/plugins/events-backend-module-azure/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-azure +## 0.2.24 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.24-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-azure/package.json b/plugins/events-backend-module-azure/package.json index 4bc4947de4..3747c2b097 100644 --- a/plugins/events-backend-module-azure/package.json +++ b/plugins/events-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-azure", - "version": "0.2.24-next.0", + "version": "0.2.24", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md index 6b5ea454a3..5014807859 100644 --- a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-bitbucket-cloud +## 0.2.24 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.24-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-bitbucket-cloud/package.json b/plugins/events-backend-module-bitbucket-cloud/package.json index f44a1d5d28..e523b668ca 100644 --- a/plugins/events-backend-module-bitbucket-cloud/package.json +++ b/plugins/events-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-bitbucket-cloud", - "version": "0.2.24-next.0", + "version": "0.2.24", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-bitbucket-server/CHANGELOG.md b/plugins/events-backend-module-bitbucket-server/CHANGELOG.md index ce5a289fa0..8c54c534b3 100644 --- a/plugins/events-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/events-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-bitbucket-server +## 0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + ## 0.1.5-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-bitbucket-server/package.json b/plugins/events-backend-module-bitbucket-server/package.json index 6deaf2e42d..dd4429548c 100644 --- a/plugins/events-backend-module-bitbucket-server/package.json +++ b/plugins/events-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-bitbucket-server", - "version": "0.1.5-next.0", + "version": "0.1.5", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-gerrit/CHANGELOG.md b/plugins/events-backend-module-gerrit/CHANGELOG.md index d0f71a8b2c..511a31488d 100644 --- a/plugins/events-backend-module-gerrit/CHANGELOG.md +++ b/plugins/events-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-gerrit +## 0.2.24 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.24-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-gerrit/package.json b/plugins/events-backend-module-gerrit/package.json index c736069b00..81978168a7 100644 --- a/plugins/events-backend-module-gerrit/package.json +++ b/plugins/events-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gerrit", - "version": "0.2.24-next.0", + "version": "0.2.24", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-github/CHANGELOG.md b/plugins/events-backend-module-github/CHANGELOG.md index 00a0b957fb..28495671fb 100644 --- a/plugins/events-backend-module-github/CHANGELOG.md +++ b/plugins/events-backend-module-github/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-events-backend-module-github +## 0.4.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.4-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-github/package.json b/plugins/events-backend-module-github/package.json index 2d961b83c3..aebbc6eddf 100644 --- a/plugins/events-backend-module-github/package.json +++ b/plugins/events-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-github", - "version": "0.4.4-next.0", + "version": "0.4.4", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-gitlab/CHANGELOG.md b/plugins/events-backend-module-gitlab/CHANGELOG.md index 174f91dc87..0df6ece6ab 100644 --- a/plugins/events-backend-module-gitlab/CHANGELOG.md +++ b/plugins/events-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-gitlab +## 0.3.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.5-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-gitlab/package.json b/plugins/events-backend-module-gitlab/package.json index c86ea6e8b4..560f4fae76 100644 --- a/plugins/events-backend-module-gitlab/package.json +++ b/plugins/events-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gitlab", - "version": "0.3.5-next.0", + "version": "0.3.5", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-google-pubsub/CHANGELOG.md b/plugins/events-backend-module-google-pubsub/CHANGELOG.md index d789713bd1..6002004dc4 100644 --- a/plugins/events-backend-module-google-pubsub/CHANGELOG.md +++ b/plugins/events-backend-module-google-pubsub/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-backend-module-google-pubsub +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.1.4-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-google-pubsub/package.json b/plugins/events-backend-module-google-pubsub/package.json index fda11585ec..88f6fe714e 100644 --- a/plugins/events-backend-module-google-pubsub/package.json +++ b/plugins/events-backend-module-google-pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-google-pubsub", - "version": "0.1.4-next.0", + "version": "0.1.4", "description": "The google-pubsub backend module for the events plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/events-backend-module-kafka/CHANGELOG.md b/plugins/events-backend-module-kafka/CHANGELOG.md index e005093ef8..f12109da75 100644 --- a/plugins/events-backend-module-kafka/CHANGELOG.md +++ b/plugins/events-backend-module-kafka/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-backend-module-kafka +## 0.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.1.3-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-kafka/package.json b/plugins/events-backend-module-kafka/package.json index 0540029b4e..03a1c6c1dc 100644 --- a/plugins/events-backend-module-kafka/package.json +++ b/plugins/events-backend-module-kafka/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-kafka", - "version": "0.1.3-next.0", + "version": "0.1.3", "description": "The kafka backend module for the events plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/events-backend-test-utils/CHANGELOG.md b/plugins/events-backend-test-utils/CHANGELOG.md index 849e5308b9..5a02e4ed51 100644 --- a/plugins/events-backend-test-utils/CHANGELOG.md +++ b/plugins/events-backend-test-utils/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-events-backend-test-utils +## 0.1.48 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + ## 0.1.48-next.0 ### Patch Changes diff --git a/plugins/events-backend-test-utils/package.json b/plugins/events-backend-test-utils/package.json index db95b057a8..a27e4efbe6 100644 --- a/plugins/events-backend-test-utils/package.json +++ b/plugins/events-backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-test-utils", - "version": "0.1.48-next.0", + "version": "0.1.48", "description": "The plugin-events-backend-test-utils for @backstage/plugin-events-node", "backstage": { "role": "node-library", diff --git a/plugins/events-backend/CHANGELOG.md b/plugins/events-backend/CHANGELOG.md index 2220969cd7..15b1b91730 100644 --- a/plugins/events-backend/CHANGELOG.md +++ b/plugins/events-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-events-backend +## 0.5.6 + +### Patch Changes + +- 0efcc97: Updated generated schemas +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + ## 0.5.6-next.0 ### Patch Changes diff --git a/plugins/events-backend/knip-report.md b/plugins/events-backend/knip-report.md index 7df8949921..9da2b3fe7c 100644 --- a/plugins/events-backend/knip-report.md +++ b/plugins/events-backend/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------------------- | :----------- | :------- | -| @backstage/backend-app-api | package.json | error | +| @backstage/backend-app-api | plugins/events-backend/package.json | error | diff --git a/plugins/events-backend/package.json b/plugins/events-backend/package.json index c16c508989..a5f530f33c 100644 --- a/plugins/events-backend/package.json +++ b/plugins/events-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend", - "version": "0.5.6-next.0", + "version": "0.5.6", "backstage": { "role": "backend-plugin", "pluginId": "events", diff --git a/plugins/events-backend/src/schema/openapi/generated/apis/Api.server.ts b/plugins/events-backend/src/schema/openapi/generated/apis/Api.server.ts index 4a1efdfe67..0b116fe012 100644 --- a/plugins/events-backend/src/schema/openapi/generated/apis/Api.server.ts +++ b/plugins/events-backend/src/schema/openapi/generated/apis/Api.server.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/apis/index.ts b/plugins/events-backend/src/schema/openapi/generated/apis/index.ts index a3cdbbebd2..8d81cbaf39 100644 --- a/plugins/events-backend/src/schema/openapi/generated/apis/index.ts +++ b/plugins/events-backend/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/index.ts b/plugins/events-backend/src/schema/openapi/generated/index.ts index 69c39313c6..dec4b8804e 100644 --- a/plugins/events-backend/src/schema/openapi/generated/index.ts +++ b/plugins/events-backend/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/ErrorError.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/ErrorError.model.ts index 0a51c67fef..809cd3fa80 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts index d44dcb66d9..3eb5e15740 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts index 91c120483d..edbcc32df7 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/Event.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/Event.model.ts index edd88b0738..02e52a81d9 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/Event.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/Event.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts index 3ca3282f47..ff15eef3ce 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/ModelError.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/ModelError.model.ts index 3e6af947ec..3914d73531 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/ModelError.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/PostEventRequest.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/PostEventRequest.model.ts index cfaf66ce4f..6bbb2974b5 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/PostEventRequest.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/PostEventRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts index a1dc298789..786f0a0644 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/index.ts b/plugins/events-backend/src/schema/openapi/generated/models/index.ts index 5044f65fff..6baa8ef449 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/index.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/router.ts b/plugins/events-backend/src/schema/openapi/generated/router.ts index d2cedd2166..f1b9de2099 100644 --- a/plugins/events-backend/src/schema/openapi/generated/router.ts +++ b/plugins/events-backend/src/schema/openapi/generated/router.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/index.ts b/plugins/events-backend/src/schema/openapi/index.ts index db98243cbf..196aad553a 100644 --- a/plugins/events-backend/src/schema/openapi/index.ts +++ b/plugins/events-backend/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/CHANGELOG.md b/plugins/events-node/CHANGELOG.md index 2ae679aa8b..1b706fdf22 100644 --- a/plugins/events-node/CHANGELOG.md +++ b/plugins/events-node/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-node +## 0.4.15 + +### Patch Changes + +- 0efcc97: Updated generated schemas +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.15-next.0 ### Patch Changes diff --git a/plugins/events-node/knip-report.md b/plugins/events-node/knip-report.md index 3f80c71260..cb8d19b560 100644 --- a/plugins/events-node/knip-report.md +++ b/plugins/events-node/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :--------------- | :----------- | :------- | -| @backstage/types | package.json | error | +| @backstage/types | plugins/events-node/package.json | error | diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index 536cf246b9..efd879a47a 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-node", - "version": "0.4.15-next.0", + "version": "0.4.15", "description": "The plugin-events-node module for @backstage/plugin-events-backend", "backstage": { "role": "node-library", diff --git a/plugins/events-node/src/schema/openapi/generated/apis/Api.client.ts b/plugins/events-node/src/schema/openapi/generated/apis/Api.client.ts index c77ddc1d67..71e3767af2 100644 --- a/plugins/events-node/src/schema/openapi/generated/apis/Api.client.ts +++ b/plugins/events-node/src/schema/openapi/generated/apis/Api.client.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/apis/index.ts b/plugins/events-node/src/schema/openapi/generated/apis/index.ts index af52f9db46..fc7c83b736 100644 --- a/plugins/events-node/src/schema/openapi/generated/apis/index.ts +++ b/plugins/events-node/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/index.ts b/plugins/events-node/src/schema/openapi/generated/index.ts index bb399e97a0..dc3055033d 100644 --- a/plugins/events-node/src/schema/openapi/generated/index.ts +++ b/plugins/events-node/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/ErrorError.model.ts b/plugins/events-node/src/schema/openapi/generated/models/ErrorError.model.ts index 0a51c67fef..809cd3fa80 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/ErrorRequest.model.ts b/plugins/events-node/src/schema/openapi/generated/models/ErrorRequest.model.ts index d44dcb66d9..3eb5e15740 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/ErrorResponse.model.ts b/plugins/events-node/src/schema/openapi/generated/models/ErrorResponse.model.ts index 91c120483d..edbcc32df7 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/Event.model.ts b/plugins/events-node/src/schema/openapi/generated/models/Event.model.ts index edd88b0738..02e52a81d9 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/Event.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/Event.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts b/plugins/events-node/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts index 3ca3282f47..ff15eef3ce 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/ModelError.model.ts b/plugins/events-node/src/schema/openapi/generated/models/ModelError.model.ts index 3e6af947ec..3914d73531 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/ModelError.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/PostEventRequest.model.ts b/plugins/events-node/src/schema/openapi/generated/models/PostEventRequest.model.ts index cfaf66ce4f..6bbb2974b5 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/PostEventRequest.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/PostEventRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts b/plugins/events-node/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts index a1dc298789..786f0a0644 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/index.ts b/plugins/events-node/src/schema/openapi/generated/models/index.ts index 5044f65fff..6baa8ef449 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/index.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/pluginId.ts b/plugins/events-node/src/schema/openapi/generated/pluginId.ts index 36b5433cc3..b5fc50cfc0 100644 --- a/plugins/events-node/src/schema/openapi/generated/pluginId.ts +++ b/plugins/events-node/src/schema/openapi/generated/pluginId.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/index.ts b/plugins/events-node/src/schema/openapi/index.ts index db98243cbf..196aad553a 100644 --- a/plugins/events-node/src/schema/openapi/index.ts +++ b/plugins/events-node/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/example-todo-list-backend/CHANGELOG.md b/plugins/example-todo-list-backend/CHANGELOG.md index 08227bc14a..bd1db36bb8 100644 --- a/plugins/example-todo-list-backend/CHANGELOG.md +++ b/plugins/example-todo-list-backend/CHANGELOG.md @@ -1,5 +1,12 @@ # @internal/plugin-todo-list-backend +## 1.0.43 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + ## 1.0.43-next.0 ### Patch Changes diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json index 8e31279e2a..6775feb352 100644 --- a/plugins/example-todo-list-backend/package.json +++ b/plugins/example-todo-list-backend/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list-backend", - "version": "1.0.43-next.0", + "version": "1.0.43", "backstage": { "role": "backend-plugin", "pluginId": "todo-list", diff --git a/plugins/example-todo-list/CHANGELOG.md b/plugins/example-todo-list/CHANGELOG.md index 205e703446..621859a9ab 100644 --- a/plugins/example-todo-list/CHANGELOG.md +++ b/plugins/example-todo-list/CHANGELOG.md @@ -1,5 +1,13 @@ # @internal/plugin-todo-list +## 1.0.43 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + ## 1.0.43-next.0 ### Patch Changes diff --git a/plugins/example-todo-list/knip-report.md b/plugins/example-todo-list/knip-report.md index ba9c1dddc5..552227ac1d 100644 --- a/plugins/example-todo-list/knip-report.md +++ b/plugins/example-todo-list/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :--------------------- | :----------- | :------- | -| @testing-library/react | package.json | error | -| @testing-library/dom | package.json | error | +| @testing-library/react | plugins/example-todo-list/package.json | error | +| @testing-library/dom | plugins/example-todo-list/package.json | error | diff --git a/plugins/example-todo-list/package.json b/plugins/example-todo-list/package.json index 3104980821..f0273e482b 100644 --- a/plugins/example-todo-list/package.json +++ b/plugins/example-todo-list/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list", - "version": "1.0.43-next.0", + "version": "1.0.43", "backstage": { "role": "frontend-plugin", "pluginId": "todo-list", diff --git a/plugins/gateway-backend/CHANGELOG.md b/plugins/gateway-backend/CHANGELOG.md index 7e1fe97014..eabcf7fe66 100644 --- a/plugins/gateway-backend/CHANGELOG.md +++ b/plugins/gateway-backend/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-gateway-backend +## 1.0.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + ## 1.0.5-next.0 ### Patch Changes diff --git a/plugins/gateway-backend/package.json b/plugins/gateway-backend/package.json index 1413086b5b..985f5f9380 100644 --- a/plugins/gateway-backend/package.json +++ b/plugins/gateway-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gateway-backend", - "version": "1.0.5-next.0", + "version": "1.0.5", "backstage": { "role": "backend-plugin", "pluginId": "gateway", diff --git a/plugins/home-react/CHANGELOG.md b/plugins/home-react/CHANGELOG.md index bcd79b94d6..b4fff66b63 100644 --- a/plugins/home-react/CHANGELOG.md +++ b/plugins/home-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-home-react +## 0.1.30 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + ## 0.1.30-next.0 ### Patch Changes diff --git a/plugins/home-react/knip-report.md b/plugins/home-react/knip-report.md index c9f34521a3..3fea06e47b 100644 --- a/plugins/home-react/knip-report.md +++ b/plugins/home-react/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :----------------------- | :----------- | :------- | -| @types/react-grid-layout | package.json | error | +| @types/react-grid-layout | plugins/home-react/package.json | error | diff --git a/plugins/home-react/package.json b/plugins/home-react/package.json index f1605514e0..bce8419b08 100644 --- a/plugins/home-react/package.json +++ b/plugins/home-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home-react", - "version": "0.1.30-next.0", + "version": "0.1.30", "description": "A Backstage plugin that contains react components helps you build a home page", "backstage": { "role": "web-library", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index ea7326d9b3..5731706ece 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-home +## 0.8.12 + +### Patch Changes + +- 929c55a: Fixed race condition in CustomHomepageGrid by waiting for storage to load before rendering custom layout to prevent + rendering of the default content. +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + - @backstage/core-app-api@1.19.0 + - @backstage/plugin-home-react@0.1.30 + +## 0.8.12-next.2 + +### Patch Changes + +- 929c55a: Fixed race condition in CustomHomepageGrid by waiting for storage to load before rendering custom layout to prevent + rendering of the default content. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.8.12-next.1 ### Patch Changes diff --git a/plugins/home/package.json b/plugins/home/package.json index 21469df7cd..a348b8eff9 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home", - "version": "0.8.12-next.1", + "version": "0.8.12", "description": "A Backstage plugin that helps you build a home page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx index 5d85e834e8..fec7895170 100644 --- a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx +++ b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx @@ -34,7 +34,11 @@ import { } from '@material-ui/core/styles'; import { compact } from 'lodash'; import useObservable from 'react-use/esm/useObservable'; -import { ContentHeader, ErrorBoundary } from '@backstage/core-components'; +import { + ContentHeader, + ErrorBoundary, + Progress, +} from '@backstage/core-components'; import Typography from '@material-ui/core/Typography'; import { WidgetSettingsOverlay } from './WidgetSettingsOverlay'; import { AddWidgetDialog } from './AddWidgetDialog'; @@ -90,7 +94,7 @@ const useStyles = makeStyles((theme: Theme) => function useHomeStorage( defaultWidgets: GridWidget[], -): [GridWidget[], (value: GridWidget[]) => void] { +): [GridWidget[], (value: GridWidget[]) => void, boolean] { const key = 'home'; const storageApi = useApi(storageApiRef).forBucket('home.customHomepage'); // TODO: Support multiple home pages @@ -110,6 +114,9 @@ function useHomeStorage( storageApi.observe$(key), storageApi.snapshot(key), ); + + const isStorageLoading = homeSnapshot.presence === 'unknown' || !homeSnapshot; + const widgets: GridWidget[] = useMemo(() => { if (homeSnapshot.presence === 'absent') { return defaultWidgets; @@ -122,7 +129,7 @@ function useHomeStorage( } }, [homeSnapshot, defaultWidgets]); - return [widgets, setWidgets]; + return [widgets, setWidgets, isStorageLoading]; } const convertConfigToDefaultWidgets = ( @@ -213,7 +220,7 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => { ? convertConfigToDefaultWidgets(props.config, availableWidgets) : []; }, [props.config, availableWidgets]); - const [widgets, setWidgets] = useHomeStorage(defaultLayout); + const [widgets, setWidgets, isStorageLoading] = useHomeStorage(defaultLayout); const [addWidgetDialogOpen, setAddWidgetDialogOpen] = useState(false); const editModeOn = widgets.find(w => w.layout.isResizable) !== undefined; const [editMode, setEditMode] = useState(editModeOn); @@ -322,6 +329,10 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => { ); }; + if (isStorageLoading) { + return ; + } + return ( <> diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 761e87e5e0..1ede69fa30 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-kubernetes-backend +## 0.20.2 + +### Patch Changes + +- dd7b6d2: Fix a bug where `getDefault` in the `kubernetesFetcherExtensionPoint` had the wrong `this` value +- 80cf8c9: Fix issue with default objects not being loaded properly +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-kubernetes-node@0.3.4 + - @backstage/plugin-permission-node@0.10.4 + +## 0.20.2-next.2 + +### Patch Changes + +- dd7b6d2: Fix a bug where `getDefault` in the `kubernetesFetcherExtensionPoint` had the wrong `this` value +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration-aws-node@0.1.17 + ## 0.20.2-next.1 ### Patch Changes diff --git a/plugins/kubernetes-backend/knip-report.md b/plugins/kubernetes-backend/knip-report.md index 7804e0ecb8..4d8545fd20 100644 --- a/plugins/kubernetes-backend/knip-report.md +++ b/plugins/kubernetes-backend/knip-report.md @@ -1,21 +1,22 @@ # Knip report -## Unused dependencies (7) +## Unused dependencies (8) | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/plugin-auth-node | package.json | error | -| stream-buffers | package.json | error | -| compression | package.json | error | -| helmet | package.json | error | -| morgan | package.json | error | -| cors | package.json | error | -| yn | package.json | error | +| @backstage/plugin-auth-node | plugins/kubernetes-backend/package.json | error | +| stream-buffers | plugins/kubernetes-backend/package.json | error | +| compression | plugins/kubernetes-backend/package.json | error | +| winston | plugins/kubernetes-backend/package.json | error | +| helmet | plugins/kubernetes-backend/package.json | error | +| morgan | plugins/kubernetes-backend/package.json | error | +| cors | plugins/kubernetes-backend/package.json | error | +| yn | plugins/kubernetes-backend/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :------------------------- | :----------- | :------- | -| @backstage/backend-app-api | package.json | error | -| @types/aws4 | package.json | error | +| @backstage/backend-app-api | plugins/kubernetes-backend/package.json | error | +| @types/aws4 | plugins/kubernetes-backend/package.json | error | diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index bae94407b5..6d029eae95 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.20.2-next.1", + "version": "0.20.2", "description": "A Backstage backend plugin that integrates towards Kubernetes", "backstage": { "role": "backend-plugin", diff --git a/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts b/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts index c32023235f..1b0b9a31e1 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts @@ -183,8 +183,9 @@ export class KubernetesInitializer { async init() { const fetcher = - (await this.opts.fetcher?.({ getDefault: this.defaultFetcher })) ?? - (await this.defaultFetcher()); + (await this.opts.fetcher?.({ + getDefault: () => this.defaultFetcher(), + })) ?? (await this.defaultFetcher()); const authStrategyMap = this.opts.authStrategyMap ?? (await this.defaultAuthStrategy()); diff --git a/plugins/kubernetes-cluster/CHANGELOG.md b/plugins/kubernetes-cluster/CHANGELOG.md index 63ee864a46..7f7cf88809 100644 --- a/plugins/kubernetes-cluster/CHANGELOG.md +++ b/plugins/kubernetes-cluster/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-kubernetes-cluster +## 0.0.29 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/plugin-kubernetes-react@0.5.11 + +## 0.0.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + ## 0.0.29-next.1 ### Patch Changes diff --git a/plugins/kubernetes-cluster/knip-report.md b/plugins/kubernetes-cluster/knip-report.md index a5a618460d..55eba7a829 100644 --- a/plugins/kubernetes-cluster/knip-report.md +++ b/plugins/kubernetes-cluster/knip-report.md @@ -4,15 +4,15 @@ | Name | Location | Severity | | :---------------------- | :----------- | :------- | -| @kubernetes-models/base | package.json | error | -| cronstrue | package.json | error | -| js-yaml | package.json | error | -| lodash | package.json | error | -| luxon | package.json | error | +| @kubernetes-models/base | plugins/kubernetes-cluster/package.json | error | +| cronstrue | plugins/kubernetes-cluster/package.json | error | +| js-yaml | plugins/kubernetes-cluster/package.json | error | +| lodash | plugins/kubernetes-cluster/package.json | error | +| luxon | plugins/kubernetes-cluster/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :------------------- | :----------- | :------- | -| @testing-library/dom | package.json | error | +| @testing-library/dom | plugins/kubernetes-cluster/package.json | error | diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json index 6488b959f6..e789c3edaf 100644 --- a/plugins/kubernetes-cluster/package.json +++ b/plugins/kubernetes-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-cluster", - "version": "0.0.29-next.1", + "version": "0.0.29", "description": "A Backstage plugin that shows details of Kubernetes clusters", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes-common/report.api.md b/plugins/kubernetes-common/report.api.md index 2d94aaefff..748ae94656 100644 --- a/plugins/kubernetes-common/report.api.md +++ b/plugins/kubernetes-common/report.api.md @@ -455,6 +455,10 @@ export interface SecretsFetchResponse { type: 'secrets'; } +// @public +export const SERVICEACCOUNT_CA_PATH = + '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'; + // @public (undocumented) export interface ServiceFetchResponse { // (undocumented) diff --git a/plugins/kubernetes-common/src/certificate-authority-constants.ts b/plugins/kubernetes-common/src/certificate-authority-constants.ts index 46401bde23..71bcad8a53 100644 --- a/plugins/kubernetes-common/src/certificate-authority-constants.ts +++ b/plugins/kubernetes-common/src/certificate-authority-constants.ts @@ -17,7 +17,7 @@ /** * A constant that specifies the location of the certificate for the service account. * - * @internal + * @public */ export const SERVICEACCOUNT_CA_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'; diff --git a/plugins/kubernetes-node/CHANGELOG.md b/plugins/kubernetes-node/CHANGELOG.md index b0eac9c476..d7366fa90c 100644 --- a/plugins/kubernetes-node/CHANGELOG.md +++ b/plugins/kubernetes-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-kubernetes-node +## 0.3.4 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.4-next.0 ### Patch Changes diff --git a/plugins/kubernetes-node/knip-report.md b/plugins/kubernetes-node/knip-report.md index 1c25fa062b..31fc64b3c7 100644 --- a/plugins/kubernetes-node/knip-report.md +++ b/plugins/kubernetes-node/knip-report.md @@ -4,11 +4,11 @@ | Name | Location | Severity | | :------ | :----------- | :------- | -| winston | package.json | error | +| winston | plugins/kubernetes-node/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :------------------------- | :----------- | :------- | -| @backstage/backend-app-api | package.json | error | +| @backstage/backend-app-api | plugins/kubernetes-node/package.json | error | diff --git a/plugins/kubernetes-node/package.json b/plugins/kubernetes-node/package.json index 23aae77049..2c4c043ec5 100644 --- a/plugins/kubernetes-node/package.json +++ b/plugins/kubernetes-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-node", - "version": "0.3.4-next.0", + "version": "0.3.4", "description": "Node.js library for the kubernetes plugin", "backstage": { "role": "node-library", diff --git a/plugins/kubernetes-react/CHANGELOG.md b/plugins/kubernetes-react/CHANGELOG.md index 973509d5f2..c542907fe6 100644 --- a/plugins/kubernetes-react/CHANGELOG.md +++ b/plugins/kubernetes-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-kubernetes-react +## 0.5.11 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + ## 0.5.11-next.0 ### Patch Changes diff --git a/plugins/kubernetes-react/package.json b/plugins/kubernetes-react/package.json index baa541cc87..82e3d59d8b 100644 --- a/plugins/kubernetes-react/package.json +++ b/plugins/kubernetes-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-react", - "version": "0.5.11-next.0", + "version": "0.5.11", "description": "Web library for the kubernetes-react plugin", "backstage": { "role": "web-library", diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 6f76f6bc2a..627226fee0 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-kubernetes +## 0.12.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-kubernetes-react@0.5.11 + +## 0.12.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.12.11-next.1 ### Patch Changes diff --git a/plugins/kubernetes/knip-report.md b/plugins/kubernetes/knip-report.md index bb11f63d0d..11b66acd1b 100644 --- a/plugins/kubernetes/knip-report.md +++ b/plugins/kubernetes/knip-report.md @@ -4,15 +4,15 @@ | Name | Location | Severity | | :------------------------------ | :----------- | :------- | -| @kubernetes-models/apimachinery | package.json | error | -| @kubernetes-models/base | package.json | error | -| @kubernetes/client-node | package.json | error | -| @xterm/addon-attach | package.json | error | -| kubernetes-models | package.json | error | -| @xterm/addon-fit | package.json | error | -| @xterm/xterm | package.json | error | -| cronstrue | package.json | error | -| js-yaml | package.json | error | -| lodash | package.json | error | -| luxon | package.json | error | +| @kubernetes-models/apimachinery | plugins/kubernetes/package.json | error | +| @kubernetes-models/base | plugins/kubernetes/package.json | error | +| @kubernetes/client-node | plugins/kubernetes/package.json | error | +| @xterm/addon-attach | plugins/kubernetes/package.json | error | +| kubernetes-models | plugins/kubernetes/package.json | error | +| @xterm/addon-fit | plugins/kubernetes/package.json | error | +| @xterm/xterm | plugins/kubernetes/package.json | error | +| cronstrue | plugins/kubernetes/package.json | error | +| js-yaml | plugins/kubernetes/package.json | error | +| lodash | plugins/kubernetes/package.json | error | +| luxon | plugins/kubernetes/package.json | error | diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 3fd141319c..e9da7b47ec 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.12.11-next.1", + "version": "0.12.11", "description": "A Backstage plugin that integrates towards Kubernetes", "backstage": { "role": "frontend-plugin", diff --git a/plugins/mcp-actions-backend/CHANGELOG.md b/plugins/mcp-actions-backend/CHANGELOG.md index 0ca964fbd1..8f079b5dd6 100644 --- a/plugins/mcp-actions-backend/CHANGELOG.md +++ b/plugins/mcp-actions-backend/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-mcp-actions-backend +## 0.1.3 + +### Patch Changes + +- 1d47bf3: Proxy `/.well-known/oauth-authorization-server` to `/.well-known/openid-configuration` on `auth-backend` when `auth.experimentalDynamicClientRegistration.enabled` is enabled. +- 7f2a4a0: Updating docs +- d08b0c9: The MCP backend will now convert known Backstage errors into textual responses with `isError: true`. + The error message can be useful for an LLM to understand and maybe give back to the user. + Previously all errors where thrown out to `@modelcontextprotocol/sdk` which causes a generic 500. +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## 0.1.3-next.1 + +### Patch Changes + +- 1d47bf3: Proxy `/.well-known/oauth-authorization-server` to `/.well-known/openid-configuration` on `auth-backend` when `auth.experimentalDynamicClientRegistration.enabled` is enabled. +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.1.3-next.0 ### Patch Changes diff --git a/plugins/mcp-actions-backend/README.md b/plugins/mcp-actions-backend/README.md index c44bd7f0a6..d81d0207c5 100644 --- a/plugins/mcp-actions-backend/README.md +++ b/plugins/mcp-actions-backend/README.md @@ -103,6 +103,31 @@ node -p 'require("crypto").randomBytes(24).toString("base64")' Set the `MCP_TOKEN` environment variable with this token, and configure your MCP client to use it in the [Authorization header](#configuring-mcp-clients) +#### Experimental: Dynamic Client Registration + +> [!CAUTION] +> This is highly experimental, proceed with caution. + +You can configure the `auth-backend` and install the `auth` frontend plugin in order to enable [Dynamic Client Registration](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#dynamic-client-registration) with MCP Clients. + +This means that there is no token required in your MCP settings, and a token will be given to a client that requests a token on your behalf. When adding the MCP server to an MCP client like Cursor or Claude, a popup that requires your approval will be opened in your Backstage instance, which is powered by the `auth` plugin. + +You will need to add the `@backstage/plugin-auth` package to your `app` `package.json`, and enable the following config in `app-config.yaml`: + +```yaml +auth: + experimentalDynamicClientRegistration: + # enable the feature + enabled: true + + # this is optional and will default to *, but you can limit the callback URLs which are valid for added security + allowedRedirectUriPatterns: + - cursor://* +``` + +> [!NOTE] +> The `@backstage/plugin-auth` package is currently only available in the new frontend system. + ## Configuring MCP Clients The MCP server supports both Server-Sent Events (SSE) and Streamable HTTP protocols. diff --git a/plugins/mcp-actions-backend/knip-report.md b/plugins/mcp-actions-backend/knip-report.md index 830018776f..79729ec010 100644 --- a/plugins/mcp-actions-backend/knip-report.md +++ b/plugins/mcp-actions-backend/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @backstage/catalog-client | package.json | error | -| zod | package.json | error | +| @backstage/catalog-client | plugins/mcp-actions-backend/package.json | error | +| zod | plugins/mcp-actions-backend/package.json | error | diff --git a/plugins/mcp-actions-backend/package.json b/plugins/mcp-actions-backend/package.json index 2a7fbdf8a3..4b8bbd3b72 100644 --- a/plugins/mcp-actions-backend/package.json +++ b/plugins/mcp-actions-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-mcp-actions-backend", - "version": "0.1.3-next.0", + "version": "0.1.3", "backstage": { "role": "backend-plugin", "pluginId": "mcp-actions", diff --git a/plugins/mcp-actions-backend/src/plugin.ts b/plugins/mcp-actions-backend/src/plugin.ts index f1e89417e9..d04df6cdf5 100644 --- a/plugins/mcp-actions-backend/src/plugin.ts +++ b/plugins/mcp-actions-backend/src/plugin.ts @@ -17,7 +17,8 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { json, Router } from 'express'; +import { json } from 'express'; +import Router from 'express-promise-router'; import { McpService } from './services/McpService'; import { createStreamableRouter } from './routers/createStreamableRouter'; import { createSseRouter } from './routers/createSseRouter'; @@ -42,8 +43,19 @@ export const mcpPlugin = createBackendPlugin({ httpRouter: coreServices.httpRouter, actions: actionsServiceRef, registry: actionsRegistryServiceRef, + rootRouter: coreServices.rootHttpRouter, + discovery: coreServices.discovery, + config: coreServices.rootConfig, }, - async init({ actions, logger, httpRouter, httpAuth }) { + async init({ + actions, + logger, + httpRouter, + httpAuth, + rootRouter, + discovery, + config, + }) { const mcpService = await McpService.create({ actions, }); @@ -66,6 +78,26 @@ export const mcpPlugin = createBackendPlugin({ router.use('/v1', streamableRouter); httpRouter.use(router); + + if ( + config.getOptionalBoolean( + 'auth.experimentalDynamicClientRegistration.enabled', + ) + ) { + // This should be replaced with throwing a WWW-Authenticate header, but that doesn't seem to be supported by + // many of the MCP client as of yet. So this seems to be the oldest version of the spec thats implemented. + rootRouter.use( + '/.well-known/oauth-authorization-server', + async (_, res) => { + const authBaseUrl = await discovery.getBaseUrl('auth'); + const oidcResponse = await fetch( + `${authBaseUrl}/.well-known/openid-configuration`, + ); + + res.json(await oidcResponse.json()); + }, + ); + } }, }); }, diff --git a/plugins/notifications-backend-module-email/CHANGELOG.md b/plugins/notifications-backend-module-email/CHANGELOG.md index 9a46dad0bc..1d58fa9727 100644 --- a/plugins/notifications-backend-module-email/CHANGELOG.md +++ b/plugins/notifications-backend-module-email/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-notifications-backend-module-email +## 0.3.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-node@0.2.19 + - @backstage/backend-plugin-api@1.4.3 + +## 0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration-aws-node@0.1.17 + - @backstage/plugin-notifications-node@0.2.19-next.1 + ## 0.3.13-next.0 ### Patch Changes diff --git a/plugins/notifications-backend-module-email/knip-report.md b/plugins/notifications-backend-module-email/knip-report.md index 38a3e4ea8a..a36d75cae4 100644 --- a/plugins/notifications-backend-module-email/knip-report.md +++ b/plugins/notifications-backend-module-email/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------- | :----------- | :------- | -| @aws-sdk/types | package.json | error | +| @aws-sdk/types | plugins/notifications-backend-module-email/package.json | error | diff --git a/plugins/notifications-backend-module-email/package.json b/plugins/notifications-backend-module-email/package.json index 602edf0e66..ac8682fe2d 100644 --- a/plugins/notifications-backend-module-email/package.json +++ b/plugins/notifications-backend-module-email/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-email", - "version": "0.3.13-next.0", + "version": "0.3.13", "description": "The email backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/notifications-backend-module-slack/CHANGELOG.md b/plugins/notifications-backend-module-slack/CHANGELOG.md index e38e8f9d07..cc69729a3a 100644 --- a/plugins/notifications-backend-module-slack/CHANGELOG.md +++ b/plugins/notifications-backend-module-slack/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-notifications-backend-module-slack +## 0.1.5 + +### Patch Changes + +- a95cebd: Internal refactoring for better type support +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-node@0.2.19 + - @backstage/backend-plugin-api@1.4.3 + +## 0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-notifications-node@0.2.19-next.1 + ## 0.1.5-next.0 ### Patch Changes diff --git a/plugins/notifications-backend-module-slack/knip-report.md b/plugins/notifications-backend-module-slack/knip-report.md index eb139623ce..d3eb017b29 100644 --- a/plugins/notifications-backend-module-slack/knip-report.md +++ b/plugins/notifications-backend-module-slack/knip-report.md @@ -4,14 +4,14 @@ | Name | Location | Severity | | :----------- | :----------- | :------- | -| @slack/types | package.json | error | -| @slack/bolt | package.json | error | +| @slack/types | plugins/notifications-backend-module-slack/package.json | error | +| @slack/bolt | plugins/notifications-backend-module-slack/package.json | error | ## Unused devDependencies (3) | Name | Location | Severity | | :-------------------- | :----------- | :------- | -| @backstage/test-utils | package.json | error | -| @faker-js/faker | package.json | error | -| msw | package.json | error | +| @backstage/test-utils | plugins/notifications-backend-module-slack/package.json | error | +| @faker-js/faker | plugins/notifications-backend-module-slack/package.json | error | +| msw | plugins/notifications-backend-module-slack/package.json | error | diff --git a/plugins/notifications-backend-module-slack/package.json b/plugins/notifications-backend-module-slack/package.json index 227201984b..4f7e59770e 100644 --- a/plugins/notifications-backend-module-slack/package.json +++ b/plugins/notifications-backend-module-slack/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-slack", - "version": "0.1.5-next.0", + "version": "0.1.5", "description": "The slack backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts index c587580657..f7dbc6a93e 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts @@ -42,7 +42,9 @@ export class SlackNotificationProcessor implements NotificationProcessor { private readonly catalog: CatalogService; private readonly auth: AuthService; private readonly slack: WebClient; - private readonly sendNotifications; + private readonly sendNotifications: ( + opts: ChatPostMessageArguments[], + ) => Promise; private readonly messagesSent: Counter; private readonly messagesFailed: Counter; private readonly broadcastChannels?: string[]; diff --git a/plugins/notifications-backend/CHANGELOG.md b/plugins/notifications-backend/CHANGELOG.md index 41905ec918..9291e3e550 100644 --- a/plugins/notifications-backend/CHANGELOG.md +++ b/plugins/notifications-backend/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-notifications-backend +## 0.5.10 + +### Patch Changes + +- a95cebd: Internal refactoring for better type support +- 7e7ed57: A new extension point was added that can be used to modify how the users receiving notifications + are resolved. The interface passed to the extension point should only return complete user entity references + based on the notification target references and the excluded entity references. Note that the inputs are lists + of entity references that can be any entity kind, not just user entities. + + Using this extension point will override the default behavior of resolving users with the + `DefaultNotificationRecipientResolver`. + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-node@0.2.19 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-signals-node@0.1.24 + +## 0.5.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-notifications-node@0.2.19-next.1 + ## 0.5.10-next.0 ### Patch Changes diff --git a/plugins/notifications-backend/knip-report.md b/plugins/notifications-backend/knip-report.md index 858e4d279e..3514fea30a 100644 --- a/plugins/notifications-backend/knip-report.md +++ b/plugins/notifications-backend/knip-report.md @@ -4,14 +4,14 @@ | Name | Location | Severity | | :---------------------------- | :----------- | :------- | -| @backstage/plugin-events-node | package.json | error | -| @backstage/plugin-auth-node | package.json | error | -| winston | package.json | error | -| yn | package.json | error | +| @backstage/plugin-events-node | plugins/notifications-backend/package.json | error | +| @backstage/plugin-auth-node | plugins/notifications-backend/package.json | error | +| winston | plugins/notifications-backend/package.json | error | +| yn | plugins/notifications-backend/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :-- | :----------- | :------- | -| msw | package.json | error | +| msw | plugins/notifications-backend/package.json | error | diff --git a/plugins/notifications-backend/migrations/20231215_init.js b/plugins/notifications-backend/migrations/20231215_init.js index a1e57237b6..fbe597534c 100644 --- a/plugins/notifications-backend/migrations/20231215_init.js +++ b/plugins/notifications-backend/migrations/20231215_init.js @@ -14,6 +14,11 @@ * limitations under the License. */ +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ exports.up = async function up(knex) { await knex.schema.createTable('notification', table => { table.uuid('id').primary(); diff --git a/plugins/notifications-backend/migrations/20240221_removeDone.js b/plugins/notifications-backend/migrations/20240221_removeDone.js index d67ac81825..2397ab69b3 100644 --- a/plugins/notifications-backend/migrations/20240221_removeDone.js +++ b/plugins/notifications-backend/migrations/20240221_removeDone.js @@ -14,6 +14,11 @@ * limitations under the License. */ +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ exports.up = async function up(knex) { await knex.schema.alterTable('notification', table => { table.text('link').nullable().alter(); @@ -21,6 +26,9 @@ exports.up = async function up(knex) { }); }; +/** + * @param {import('knex').Knex} knex + */ exports.down = async function down(knex) { await knex.schema.alterTable('notification', table => { table.text('link').notNullable().alter(); diff --git a/plugins/notifications-backend/migrations/20240313_broadcast.js b/plugins/notifications-backend/migrations/20240313_broadcast.js index 8b9ddeb24d..dd3ecd58cc 100644 --- a/plugins/notifications-backend/migrations/20240313_broadcast.js +++ b/plugins/notifications-backend/migrations/20240313_broadcast.js @@ -14,6 +14,11 @@ * limitations under the License. */ +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ exports.up = async function up(knex) { await knex.schema.createTable('broadcast', table => { table.uuid('id').primary(); @@ -37,7 +42,7 @@ exports.up = async function up(knex) { table .foreign('broadcast_id') - .references(['id']) + .references('id') .inTable('broadcast') .onDelete('CASCADE'); table.unique(['broadcast_id', 'user'], { diff --git a/plugins/notifications-backend/migrations/20240808_settings.js b/plugins/notifications-backend/migrations/20240808_settings.js index 4d4fbc24cd..8d2351d0c5 100644 --- a/plugins/notifications-backend/migrations/20240808_settings.js +++ b/plugins/notifications-backend/migrations/20240808_settings.js @@ -13,6 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ exports.up = async function up(knex) { await knex.schema.createTable('user_settings', table => { table.string('user').notNullable(); diff --git a/plugins/notifications-backend/migrations/20240902_addIcon.js b/plugins/notifications-backend/migrations/20240902_addIcon.js index a5327f8d73..f57948e08a 100644 --- a/plugins/notifications-backend/migrations/20240902_addIcon.js +++ b/plugins/notifications-backend/migrations/20240902_addIcon.js @@ -14,6 +14,11 @@ * limitations under the License. */ +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ exports.up = async function up(knex) { await knex.schema.alterTable('notification', table => { table.string('icon', 255).nullable(); @@ -23,6 +28,9 @@ exports.up = async function up(knex) { }); }; +/** + * @param {import('knex').Knex} knex + */ exports.down = async function down(knex) { await knex.schema.alterTable('notification', table => { table.dropColumn('icon'); diff --git a/plugins/notifications-backend/migrations/20250317_addTopic.js b/plugins/notifications-backend/migrations/20250317_addTopic.js index e17b8d0789..c2d4d7b10e 100644 --- a/plugins/notifications-backend/migrations/20250317_addTopic.js +++ b/plugins/notifications-backend/migrations/20250317_addTopic.js @@ -13,8 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +// @ts-check + const crypto = require('crypto'); +/** + * @param {import('knex').Knex} knex + */ exports.up = async function up(knex) { await knex.schema.alterTable('user_settings', table => { table.string('topic').nullable().after('origin'); @@ -41,6 +47,9 @@ exports.up = async function up(knex) { }); }; +/** + * @param {import('knex').Knex} knex + */ exports.down = async function down(knex) { await knex.schema.table('user_settings', table => { table.dropUnique([], 'user_settings_unique_idx'); diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index 94c42005c0..1a2c746409 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend", - "version": "0.5.10-next.0", + "version": "0.5.10", "backstage": { "role": "backend-plugin", "pluginId": "notifications", diff --git a/plugins/notifications-backend/src/plugin.ts b/plugins/notifications-backend/src/plugin.ts index 5f430318b5..49b3924a68 100644 --- a/plugins/notifications-backend/src/plugin.ts +++ b/plugins/notifications-backend/src/plugin.ts @@ -22,6 +22,7 @@ import { createRouter } from './service/router'; import { signalsServiceRef } from '@backstage/plugin-signals-node'; import { NotificationProcessor, + NotificationRecipientResolver, notificationsProcessingExtensionPoint, NotificationsProcessingExtensionPoint, } from '@backstage/plugin-notifications-node'; @@ -33,6 +34,7 @@ class NotificationsProcessingExtensionPointImpl implements NotificationsProcessingExtensionPoint { #processors = new Array(); + #recipientResolver: NotificationRecipientResolver | undefined = undefined; addProcessor( ...processors: Array> @@ -43,6 +45,21 @@ class NotificationsProcessingExtensionPointImpl get processors() { return this.#processors; } + + setNotificationRecipientResolver( + resolver: NotificationRecipientResolver, + ): void { + if (this.#recipientResolver) { + throw new Error( + 'Notification recipient resolver is already set. You can only set it once.', + ); + } + this.#recipientResolver = resolver; + } + + get recipientResolver() { + return this.#recipientResolver; + } } /** @@ -98,6 +115,7 @@ export const notificationsPlugin = createBackendPlugin({ catalog, signals, processors: processingExtensions.processors, + recipientResolver: processingExtensions.recipientResolver, }), ); httpRouter.addAuthPolicy({ diff --git a/plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts b/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.test.ts similarity index 58% rename from plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts rename to plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.test.ts index 838ded5c18..cd17db3680 100644 --- a/plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts +++ b/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.test.ts @@ -15,33 +15,28 @@ */ import { mockServices } from '@backstage/backend-test-utils'; -import { getUsersForEntityRef } from './getUsersForEntityRef'; import { RELATION_HAS_MEMBER, RELATION_OWNED_BY, RELATION_PARENT_OF, } from '@backstage/catalog-model'; import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; +import { DefaultNotificationRecipientResolver } from './DefaultNotificationRecipientResolver.ts'; describe('getUsersForEntityRef', () => { - it('should return empty array if entityRef is null', async () => { - await expect( - getUsersForEntityRef(null, [], { - auth: mockServices.auth(), - catalog: catalogServiceMock(), - }), - ).resolves.toEqual([]); - }); - it('should resolve users without calling catalog', async () => { const catalog = catalogServiceMock(); jest.spyOn(catalog, 'getEntitiesByRefs'); + const resolver = new DefaultNotificationRecipientResolver( + mockServices.auth(), + catalog, + ); await expect( - getUsersForEntityRef(['user:foo', 'user:ignored'], ['user:ignored'], { - auth: mockServices.auth(), - catalog, + resolver.resolveNotificationRecipients({ + entityRefs: ['user:foo', 'user:ignored'], + excludeEntityRefs: ['user:ignored'], }), - ).resolves.toEqual(['user:foo']); + ).resolves.toEqual({ userEntityRefs: ['user:foo'] }); expect(catalog.getEntitiesByRefs).not.toHaveBeenCalled(); }); @@ -85,16 +80,18 @@ describe('getUsersForEntityRef', () => { ], }); + const resolver = new DefaultNotificationRecipientResolver( + mockServices.auth(), + catalog, + ); await expect( - getUsersForEntityRef( - 'group:default/parent_group', - ['user:default/ignored'], - { - auth: mockServices.auth(), - catalog, - }, - ), - ).resolves.toEqual(['user:default/foo', 'user:default/bar']); + resolver.resolveNotificationRecipients({ + entityRefs: ['group:default/parent_group'], + excludeEntityRefs: ['user:default/ignored'], + }), + ).resolves.toEqual({ + userEntityRefs: ['user:default/foo', 'user:default/bar'], + }); }); it('should resolve user owner of entity from entity ref', async () => { @@ -116,12 +113,16 @@ describe('getUsersForEntityRef', () => { ], }); + const resolver = new DefaultNotificationRecipientResolver( + mockServices.auth(), + catalog, + ); await expect( - getUsersForEntityRef('component:default/test_component', [], { - auth: mockServices.auth(), - catalog, + resolver.resolveNotificationRecipients({ + entityRefs: ['component:default/test_component'], + excludeEntityRefs: [], }), - ).resolves.toEqual(['user:default/foo']); + ).resolves.toEqual({ userEntityRefs: ['user:default/foo'] }); }); it('should resolve group owner of entity from entity ref', async () => { @@ -156,11 +157,59 @@ describe('getUsersForEntityRef', () => { ], }); + const resolver = new DefaultNotificationRecipientResolver( + mockServices.auth(), + catalog, + ); await expect( - getUsersForEntityRef('component:default/test_component', [], { - auth: mockServices.auth(), - catalog, + resolver.resolveNotificationRecipients({ + entityRefs: ['component:default/test_component'], + excludeEntityRefs: [], }), - ).resolves.toEqual(['user:default/foo']); + ).resolves.toEqual({ userEntityRefs: ['user:default/foo'] }); + }); + + it('should filter excluded refs after resolving', async () => { + const catalog = catalogServiceMock({ + entities: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test_component', + }, + relations: [ + { + type: RELATION_OWNED_BY, + targetRef: 'group:default/owner_group', + }, + ], + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'owner_group', + }, + relations: [ + { + type: RELATION_HAS_MEMBER, + targetRef: 'user:default/foo', + }, + ], + }, + ], + }); + + const resolver = new DefaultNotificationRecipientResolver( + mockServices.auth(), + catalog, + ); + await expect( + resolver.resolveNotificationRecipients({ + entityRefs: ['component:default/test_component'], + excludeEntityRefs: ['user:default/foo'], + }), + ).resolves.toEqual({ userEntityRefs: [] }); }); }); diff --git a/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.ts b/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.ts new file mode 100644 index 0000000000..2401ec7c1f --- /dev/null +++ b/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.ts @@ -0,0 +1,177 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Entity, + isGroupEntity, + isUserEntity, + parseEntityRef, + RELATION_HAS_MEMBER, + RELATION_OWNED_BY, + RELATION_PARENT_OF, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { AuthService } from '@backstage/backend-plugin-api'; +import { CatalogService } from '@backstage/plugin-catalog-node'; +import { NotificationRecipientResolver } from '@backstage/plugin-notifications-node'; + +const isUserEntityRef = (ref: string) => + parseEntityRef(ref).kind.toLocaleLowerCase() === 'user'; + +// Partitions array of entity references to two arrays; user entity refs and other entity refs +const partitionEntityRefs = (refs: string[]): string[][] => { + const ret = [[], []] as string[][]; + for (const ref of refs) { + if (isUserEntityRef(ref)) { + ret[0].push(ref); + } else { + ret[1].push(ref); + } + } + return ret; +}; + +export class DefaultNotificationRecipientResolver + implements NotificationRecipientResolver +{ + constructor( + private readonly auth: AuthService, + private readonly catalog: CatalogService, + ) {} + + async resolveNotificationRecipients(options: { + entityRefs: string[]; + excludeEntityRefs?: string[]; + }): Promise<{ userEntityRefs: string[] }> { + const { entityRefs, excludeEntityRefs = [] } = options; + + const [userEntityRefs, otherEntityRefs] = partitionEntityRefs(entityRefs); + const users: string[] = userEntityRefs.filter( + ref => !excludeEntityRefs.includes(ref), + ); + const filtered = otherEntityRefs.filter( + ref => !excludeEntityRefs.includes(ref), + ); + + const fields = ['kind', 'metadata.name', 'metadata.namespace', 'relations']; + let entities: Array = []; + if (filtered.length > 0) { + const fetchedEntities = await this.catalog.getEntitiesByRefs( + { + entityRefs: filtered, + fields, + }, + { credentials: await this.auth.getOwnServiceCredentials() }, + ); + entities = fetchedEntities.items; + } + + const cachedEntityRefs = new Map(); + + const mapEntity = async (entity: Entity | undefined): Promise => { + if (!entity) { + return []; + } + + const currentEntityRef = stringifyEntityRef(entity); + if (excludeEntityRefs.includes(currentEntityRef)) { + return []; + } + + if (cachedEntityRefs.has(currentEntityRef)) { + return cachedEntityRefs.get(currentEntityRef)!; + } + + if (isUserEntity(entity)) { + return [currentEntityRef]; + } + + if (isGroupEntity(entity)) { + if (!entity.relations?.length) { + return []; + } + + const groupUsers = entity.relations + .filter( + relation => + relation.type === RELATION_HAS_MEMBER && + isUserEntityRef(relation.targetRef), + ) + .map(r => r.targetRef); + + const childGroupRefs = entity.relations + .filter(relation => relation.type === RELATION_PARENT_OF) + .map(r => r.targetRef); + + let childGroupUsers: string[][] = []; + if (childGroupRefs.length > 0) { + const childGroups = await this.catalog.getEntitiesByRefs( + { + entityRefs: childGroupRefs, + fields, + }, + { credentials: await this.auth.getOwnServiceCredentials() }, + ); + childGroupUsers = await Promise.all(childGroups.items.map(mapEntity)); + } + + const ret = [ + ...new Set([...groupUsers, ...childGroupUsers.flat(2)]), + ].filter(ref => !excludeEntityRefs.includes(ref)); + cachedEntityRefs.set(currentEntityRef, ret); + return ret; + } + + if (entity.relations?.length) { + const ownerRef = entity.relations.find( + relation => relation.type === RELATION_OWNED_BY, + )?.targetRef; + + if (!ownerRef) { + return []; + } + + if (isUserEntityRef(ownerRef)) { + if (excludeEntityRefs.includes(ownerRef)) { + return []; + } + return [ownerRef]; + } + + const owner = await this.catalog.getEntityByRef(ownerRef, { + credentials: await this.auth.getOwnServiceCredentials(), + }); + const ret = await mapEntity(owner); + cachedEntityRefs.set(currentEntityRef, ret); + return ret; + } + + return []; + }; + + for (const entity of entities) { + const u = await mapEntity(entity); + users.push(...u); + } + + return { + userEntityRefs: [...new Set(users)] + .filter(Boolean) + // Need to filter again after resolving users + .filter(ref => !excludeEntityRefs.includes(ref)), + }; + } +} diff --git a/plugins/notifications-backend/src/service/getUsersForEntityRef.ts b/plugins/notifications-backend/src/service/getUsersForEntityRef.ts deleted file mode 100644 index 2fa1412992..0000000000 --- a/plugins/notifications-backend/src/service/getUsersForEntityRef.ts +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - Entity, - isGroupEntity, - isUserEntity, - parseEntityRef, - RELATION_HAS_MEMBER, - RELATION_OWNED_BY, - RELATION_PARENT_OF, - stringifyEntityRef, -} from '@backstage/catalog-model'; -import { AuthService } from '@backstage/backend-plugin-api'; -import { CatalogService } from '@backstage/plugin-catalog-node'; - -const isUserEntityRef = (ref: string) => - parseEntityRef(ref).kind.toLocaleLowerCase() === 'user'; - -// Partitions array of entity references to two arrays; user entity refs and other entity refs -const partitionEntityRefs = (refs: string[]): string[][] => - refs.reduce( - ([userEntityRefs, otherEntityRefs]: string[][], ref: string) => { - return isUserEntityRef(ref) - ? [[...userEntityRefs, ref], otherEntityRefs] - : [userEntityRefs, [...otherEntityRefs, ref]]; - }, - [[], []], - ); - -export const getUsersForEntityRef = async ( - entityRef: string | string[] | null, - excludeEntityRefs: string | string[], - options: { - auth: AuthService; - catalog: CatalogService; - }, -): Promise => { - const { auth, catalog } = options; - - if (entityRef === null) { - return []; - } - - const excluded = Array.isArray(excludeEntityRefs) - ? excludeEntityRefs - : [excludeEntityRefs]; - - const refsArr = Array.isArray(entityRef) ? entityRef : [entityRef]; - const [userEntityRefs, otherEntityRefs] = partitionEntityRefs(refsArr); - const users: string[] = userEntityRefs.filter(ref => !excluded.includes(ref)); - const entityRefs = otherEntityRefs.filter(ref => !excluded.includes(ref)); - - const fields = ['kind', 'metadata.name', 'metadata.namespace', 'relations']; - let entities: Array = []; - if (entityRefs.length > 0) { - const fetchedEntities = await catalog.getEntitiesByRefs( - { - entityRefs, - fields, - }, - { credentials: await auth.getOwnServiceCredentials() }, - ); - entities = fetchedEntities.items; - } - - const mapEntity = async (entity: Entity | undefined): Promise => { - if (!entity) { - return []; - } - - const currentEntityRef = stringifyEntityRef(entity); - if (excluded.includes(currentEntityRef)) { - return []; - } - - if (isUserEntity(entity)) { - return [currentEntityRef]; - } - - if (isGroupEntity(entity)) { - if (!entity.relations?.length) { - return []; - } - - const groupUsers = entity.relations - .filter( - relation => - relation.type === RELATION_HAS_MEMBER && - isUserEntityRef(relation.targetRef), - ) - .map(r => r.targetRef); - - const childGroupRefs = entity.relations - .filter(relation => relation.type === RELATION_PARENT_OF) - .map(r => r.targetRef); - - let childGroupUsers: string[][] = []; - if (childGroupRefs.length > 0) { - const childGroups = await catalog.getEntitiesByRefs( - { - entityRefs: childGroupRefs, - fields, - }, - { credentials: await auth.getOwnServiceCredentials() }, - ); - childGroupUsers = await Promise.all(childGroups.items.map(mapEntity)); - } - - return [...groupUsers, ...childGroupUsers.flat(2)].filter( - ref => !excluded.includes(ref), - ); - } - - if (entity.relations?.length) { - const ownerRef = entity.relations.find( - relation => relation.type === RELATION_OWNED_BY, - )?.targetRef; - - if (!ownerRef) { - return []; - } - - if (isUserEntityRef(ownerRef)) { - if (excluded.includes(ownerRef)) { - return []; - } - return [ownerRef]; - } - - const owner = await catalog.getEntityByRef(ownerRef, { - credentials: await auth.getOwnServiceCredentials(), - }); - return mapEntity(owner); - } - - return []; - }; - - for (const entity of entities) { - const u = await mapEntity(entity); - users.push(...u); - } - - return [...new Set(users)].filter(Boolean); -}; diff --git a/plugins/notifications-backend/src/service/router.test.ts b/plugins/notifications-backend/src/service/router.test.ts index 980762edbc..08aa021014 100644 --- a/plugins/notifications-backend/src/service/router.test.ts +++ b/plugins/notifications-backend/src/service/router.test.ts @@ -25,7 +25,10 @@ import { TestDatabaseId, TestDatabases, } from '@backstage/backend-test-utils'; -import { NotificationSendOptions } from '@backstage/plugin-notifications-node'; +import { + NotificationRecipientResolver, + NotificationSendOptions, +} from '@backstage/plugin-notifications-node'; import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; import { DatabaseService } from '@backstage/backend-plugin-api'; import { v4 as uuid } from 'uuid'; @@ -501,6 +504,99 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { }); }); + describe('POST /notifications with custom receiver resolver', () => { + const httpAuth = mockServices.httpAuth({ + defaultCredentials: mockCredentials.service(), + }); + + const resolveFn = jest.fn(); + const recipientResolver: NotificationRecipientResolver = { + resolveNotificationRecipients: resolveFn, + }; + + beforeAll(async () => { + const router = await createRouter({ + logger: mockServices.logger.mock(), + store, + signals: signalService, + userInfo, + config, + httpAuth, + auth, + catalog, + recipientResolver, + }); + app = express().use(router).use(mockErrorHandler()); + }); + + beforeEach(async () => { + jest.resetAllMocks(); + const client = await database.getClient(); + await client('notification').del(); + await client('broadcast').del(); + await client('user_settings').del(); + }); + + const sendNotification = async (data: NotificationSendOptions) => + request(app) + .post('/notifications') + .send(data) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + it('should use custom recipient resolver', async () => { + resolveFn.mockResolvedValue({ + userEntityRefs: ['user:default/mock'], + }); + const response = await sendNotification({ + recipients: { + type: 'entity', + entityRef: ['system:default/mock'], + }, + payload: { + title: 'test notification', + }, + }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual([ + { + created: expect.any(String), + id: expect.any(String), + origin: 'external:test-service', + payload: { + severity: 'normal', + title: 'test notification', + }, + user: 'user:default/mock', + }, + ]); + + const client = await database.getClient(); + const notifications = await client('notification') + .where('user', 'user:default/mock') + .select(); + expect(notifications).toHaveLength(1); + }); + + it('should ignore if recipient resolver returns something other than an array of user entity refs', async () => { + resolveFn.mockResolvedValue({ + userEntityRefs: ['system:default/mock'], + }); + const response = await sendNotification({ + recipients: { + type: 'entity', + entityRef: ['system:default/mock'], + }, + payload: { + title: 'test notification', + }, + }); + expect(response.status).toEqual(200); + expect(response.body).toEqual([]); + }); + }); + describe('GET /', () => { const httpAuth = mockServices.httpAuth({ defaultCredentials: mockCredentials.user(), diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index 5805092096..027d6a1732 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -26,6 +26,7 @@ import { v4 as uuid } from 'uuid'; import { CatalogService } from '@backstage/plugin-catalog-node'; import { NotificationProcessor, + NotificationRecipientResolver, NotificationSendOptions, } from '@backstage/plugin-notifications-node'; import { InputError, NotFoundError } from '@backstage/errors'; @@ -48,10 +49,11 @@ import { OriginSetting, } from '@backstage/plugin-notifications-common'; import { parseEntityOrderFieldParams } from './parseEntityOrderFieldParams'; -import { getUsersForEntityRef } from './getUsersForEntityRef'; import { Config, readDurationFromConfig } from '@backstage/config'; import { durationToMilliseconds } from '@backstage/types'; import pThrottle from 'p-throttle'; +import { parseEntityRef } from '@backstage/catalog-model'; +import { DefaultNotificationRecipientResolver } from './DefaultNotificationRecipientResolver.ts'; /** @internal */ export interface RouterOptions { @@ -64,6 +66,7 @@ export interface RouterOptions { signals?: SignalsService; catalog: CatalogService; processors?: NotificationProcessor[]; + recipientResolver?: NotificationRecipientResolver; } /** @internal */ @@ -80,6 +83,7 @@ export async function createRouter( catalog, processors = [], signals, + recipientResolver, } = options; const WEB_NOTIFICATION_CHANNEL = 'Web'; @@ -100,6 +104,10 @@ export async function createRouter( const defaultNotificationSettings: NotificationSettings | undefined = config.getOptional('notifications.defaultSettings'); + const usedRecipientResolver = + recipientResolver ?? + new DefaultNotificationRecipientResolver(auth, catalog); + const getUser = async (req: Request) => { const credentials = await httpAuth.credentials(req, { allow: ['user'] }); const info = await userInfo.getUserInfo(credentials); @@ -633,6 +641,17 @@ export async function createRouter( return ret; }; + const filterNonUserEntityRefs = (refs: string[]): string[] => { + return refs.filter(ref => { + try { + const parsed = parseEntityRef(ref); + return parsed.kind.toLowerCase() === 'user'; + } catch { + return false; + } + }); + }; + const sendUserNotifications = async ( baseNotification: Omit, users: string[], @@ -640,7 +659,7 @@ export async function createRouter( origin: string, ): Promise => { const { scope } = opts.payload; - const uniqueUsers = [...new Set(users)]; + const uniqueUsers = [...new Set(filterNonUserEntityRefs(users))]; const throttled = throttle((user: string) => sendUserNotification(baseNotification, user, opts, origin, scope), ); @@ -661,7 +680,6 @@ export async function createRouter( const { recipients, payload } = opts; const { title, link } = payload; const notifications: Notification[] = []; - let users = []; if (!recipients || !title) { const missing = [ @@ -699,25 +717,26 @@ export async function createRouter( ); notifications.push(broadcast); } else if (recipients.type === 'entity') { - const entityRef = recipients.entityRef; - + const entityRefs = [recipients.entityRef].flat(); + const excludedEntityRefs = recipients.excludeEntityRef + ? [recipients.excludeEntityRef].flat() + : undefined; try { - users = await getUsersForEntityRef( - entityRef, - recipients.excludeEntityRef ?? [], - { auth, catalog }, + const { userEntityRefs } = + await usedRecipientResolver.resolveNotificationRecipients({ + entityRefs, + excludedEntityRefs, + }); + const userNotifications = await sendUserNotifications( + baseNotification, + userEntityRefs, + opts, + origin, ); + notifications.push(...userNotifications); } catch (e) { - throw new InputError('Failed to resolve notification receivers', e); + throw new InputError('Failed to send user notifications', e); } - - const userNotifications = await sendUserNotifications( - baseNotification, - users, - opts, - origin, - ); - notifications.push(...userNotifications); } else { throw new InputError( `Invalid recipients type, please use either 'broadcast' or 'entity'`, diff --git a/plugins/notifications-common/knip-report.md b/plugins/notifications-common/knip-report.md index 6bc617b812..90eba3c41e 100644 --- a/plugins/notifications-common/knip-report.md +++ b/plugins/notifications-common/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :----------------- | :----------- | :------- | -| @material-ui/icons | package.json | error | +| @material-ui/icons | plugins/notifications-common/package.json | error | diff --git a/plugins/notifications-node/CHANGELOG.md b/plugins/notifications-node/CHANGELOG.md index af3c020fd3..1db30a23ee 100644 --- a/plugins/notifications-node/CHANGELOG.md +++ b/plugins/notifications-node/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-notifications-node +## 0.2.19 + +### Patch Changes + +- 7e7ed57: A new extension point was added that can be used to modify how the users receiving notifications + are resolved. The interface passed to the extension point should only return complete user entity references + based on the notification target references and the excluded entity references. Note that the inputs are lists + of entity references that can be any entity kind, not just user entities. + + Using this extension point will override the default behavior of resolving users with the + `DefaultNotificationRecipientResolver`. + +- Updated dependencies + - @backstage/catalog-client@1.12.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-signals-node@0.1.24 + +## 0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + ## 0.2.19-next.0 ### Patch Changes diff --git a/plugins/notifications-node/knip-report.md b/plugins/notifications-node/knip-report.md index 1206fc9c3b..624533dcfc 100644 --- a/plugins/notifications-node/knip-report.md +++ b/plugins/notifications-node/knip-report.md @@ -4,9 +4,9 @@ | Name | Location | Severity | | :----------------------------- | :----------- | :------- | -| @backstage/plugin-signals-node | package.json | error | -| @backstage/catalog-client | package.json | error | -| @backstage/catalog-model | package.json | error | -| knex | package.json | error | -| uuid | package.json | error | +| @backstage/plugin-signals-node | plugins/notifications-node/package.json | error | +| @backstage/catalog-client | plugins/notifications-node/package.json | error | +| @backstage/catalog-model | plugins/notifications-node/package.json | error | +| knex | plugins/notifications-node/package.json | error | +| uuid | plugins/notifications-node/package.json | error | diff --git a/plugins/notifications-node/package.json b/plugins/notifications-node/package.json index c20cf4c820..2046692085 100644 --- a/plugins/notifications-node/package.json +++ b/plugins/notifications-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-node", - "version": "0.2.19-next.0", + "version": "0.2.19", "description": "Node.js library for the notifications plugin", "backstage": { "role": "node-library", diff --git a/plugins/notifications-node/report.api.md b/plugins/notifications-node/report.api.md index 995b98bc21..ced4fac94e 100644 --- a/plugins/notifications-node/report.api.md +++ b/plugins/notifications-node/report.api.md @@ -41,6 +41,17 @@ export interface NotificationProcessor { // @public @deprecated (undocumented) export type NotificationProcessorFilters = NotificationProcessorFilters_2; +// @public +export interface NotificationRecipientResolver { + // (undocumented) + resolveNotificationRecipients(options: { + entityRefs: string[]; + excludedEntityRefs?: string[]; + }): Promise<{ + userEntityRefs: string[]; + }>; +} + // @public (undocumented) export type NotificationRecipients = | { @@ -83,6 +94,10 @@ export interface NotificationsProcessingExtensionPoint { addProcessor( ...processors: Array> ): void; + // (undocumented) + setNotificationRecipientResolver( + resolver: NotificationRecipientResolver, + ): void; } // @public (undocumented) diff --git a/plugins/notifications-node/src/extensions.ts b/plugins/notifications-node/src/extensions.ts index 03ce47fea6..98338de221 100644 --- a/plugins/notifications-node/src/extensions.ts +++ b/plugins/notifications-node/src/extensions.ts @@ -100,6 +100,24 @@ export interface NotificationProcessor { getNotificationFilters?(): NotificationProcessorFilters; } +/** + * NotificationRecipientResolver interface is used to resolve the individual + * users to receive the notification. + * + * The `resolveNotificationRecipients` is used to resolve notifications sent for + * entity references, and it should return object with a list of user + * entity references that should receive the notification. In case the function + * returns other than user entity references, those are ignored. + * + * @public + */ +export interface NotificationRecipientResolver { + resolveNotificationRecipients(options: { + entityRefs: string[]; + excludedEntityRefs?: string[]; + }): Promise<{ userEntityRefs: string[] }>; +} + /** * @public */ @@ -107,6 +125,9 @@ export interface NotificationsProcessingExtensionPoint { addProcessor( ...processors: Array> ): void; + setNotificationRecipientResolver( + resolver: NotificationRecipientResolver, + ): void; } /** diff --git a/plugins/notifications/CHANGELOG.md b/plugins/notifications/CHANGELOG.md index 97b280ed47..9ce2086421 100644 --- a/plugins/notifications/CHANGELOG.md +++ b/plugins/notifications/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-notifications +## 0.5.9 + +### Patch Changes + +- 4815b12: Fixed missing app context when rendering the notifications view +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + ## 0.5.9-next.1 ### Patch Changes diff --git a/plugins/notifications/knip-report.md b/plugins/notifications/knip-report.md index da21340672..6cd21cf3b8 100644 --- a/plugins/notifications/knip-report.md +++ b/plugins/notifications/knip-report.md @@ -4,13 +4,13 @@ | Name | Location | Severity | | :--------------- | :----------- | :------- | -| @backstage/types | package.json | error | -| @material-ui/lab | package.json | error | +| @backstage/types | plugins/notifications/package.json | error | +| @material-ui/lab | plugins/notifications/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @testing-library/user-event | package.json | error | -| @backstage/core-app-api | package.json | error | +| @testing-library/user-event | plugins/notifications/package.json | error | +| @backstage/core-app-api | plugins/notifications/package.json | error | diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json index 0aa3cde29b..e6d66a5ec7 100644 --- a/plugins/notifications/package.json +++ b/plugins/notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications", - "version": "0.5.9-next.1", + "version": "0.5.9", "backstage": { "role": "frontend-plugin", "pluginId": "notifications", diff --git a/plugins/notifications/src/alpha.tsx b/plugins/notifications/src/alpha.tsx index 6d32150c35..ca4be58209 100644 --- a/plugins/notifications/src/alpha.tsx +++ b/plugins/notifications/src/alpha.tsx @@ -23,6 +23,7 @@ import { } from '@backstage/frontend-plugin-api'; import { rootRouteRef } from './routes'; import { + compatWrapper, convertLegacyRouteRef, convertLegacyRouteRefs, } from '@backstage/core-compat-api'; @@ -33,9 +34,9 @@ const page = PageBlueprint.make({ path: '/notifications', routeRef: convertLegacyRouteRef(rootRouteRef), loader: () => - import('./components/NotificationsPage').then(m => ( - - )), + import('./components/NotificationsPage').then(m => + compatWrapper(), + ), }, }); diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md index 410b9da982..aee4c9702e 100644 --- a/plugins/org-react/CHANGELOG.md +++ b/plugins/org-react/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-org-react +## 0.1.42 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + +## 0.1.42-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + ## 0.1.42-next.1 ### Patch Changes diff --git a/plugins/org-react/knip-report.md b/plugins/org-react/knip-report.md index b5cb1d8b22..a2a90b227e 100644 --- a/plugins/org-react/knip-report.md +++ b/plugins/org-react/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @backstage/catalog-client | package.json | error | +| @backstage/catalog-client | plugins/org-react/package.json | error | diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json index f8410a3f77..c177584968 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.42-next.1", + "version": "0.1.42", "backstage": { "role": "web-library", "pluginId": "org", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index ee5def4fcf..6a62f12664 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-org +## 0.6.44 + +### Patch Changes + +- 22b69f2: Fixing issue with extra slash in the routing +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + +## 0.6.44-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.6.44-next.1 ### Patch Changes diff --git a/plugins/org/knip-report.md b/plugins/org/knip-report.md index d1f8bacd53..45c5be2d03 100644 --- a/plugins/org/knip-report.md +++ b/plugins/org/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---------------------------------- | :----------- | :------- | -| @backstage/plugin-permission-common | package.json | error | +| @backstage/plugin-permission-common | plugins/org/package.json | error | diff --git a/plugins/org/package.json b/plugins/org/package.json index df5b58446c..9d1c60072c 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org", - "version": "0.6.44-next.1", + "version": "0.6.44", "description": "A Backstage plugin that helps you create entity pages for your organization", "backstage": { "role": "frontend-plugin", diff --git a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md index 210db3363a..601c110e9d 100644 --- a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md +++ b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-permission-backend-module-allow-all-policy +## 0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + ## 0.2.12-next.0 ### Patch Changes diff --git a/plugins/permission-backend-module-policy-allow-all/knip-report.md b/plugins/permission-backend-module-policy-allow-all/knip-report.md index 02ee898bec..3790422ce9 100644 --- a/plugins/permission-backend-module-policy-allow-all/knip-report.md +++ b/plugins/permission-backend-module-policy-allow-all/knip-report.md @@ -4,11 +4,11 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/plugin-auth-node | package.json | error | +| @backstage/plugin-auth-node | plugins/permission-backend-module-policy-allow-all/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :---------------------------- | :----------- | :------- | -| @backstage/backend-test-utils | package.json | error | +| @backstage/backend-test-utils | plugins/permission-backend-module-policy-allow-all/package.json | error | diff --git a/plugins/permission-backend-module-policy-allow-all/package.json b/plugins/permission-backend-module-policy-allow-all/package.json index e741d11560..b20d4f85a6 100644 --- a/plugins/permission-backend-module-policy-allow-all/package.json +++ b/plugins/permission-backend-module-policy-allow-all/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend-module-allow-all-policy", - "version": "0.2.12-next.0", + "version": "0.2.12", "description": "Allow all policy backend module for the permission plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index cc20d3020e..5bcc2ccff7 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-permission-backend +## 0.7.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + ## 0.7.4-next.0 ### Patch Changes diff --git a/plugins/permission-backend/knip-report.md b/plugins/permission-backend/knip-report.md index 679e0ffb63..61ea01af65 100644 --- a/plugins/permission-backend/knip-report.md +++ b/plugins/permission-backend/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :- | :----------- | :------- | -| yn | package.json | error | +| yn | plugins/permission-backend/package.json | error | diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index 702ed29bd6..434694dc06 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend", - "version": "0.7.4-next.0", + "version": "0.7.4", "backstage": { "role": "backend-plugin", "pluginId": "permission", diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index e0e7e6a026..bcd8dc8be3 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-permission-node +## 0.10.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.10.4-next.0 ### Patch Changes diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 2b325f9cf4..38f9d07cd8 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-node", - "version": "0.10.4-next.0", + "version": "0.10.4", "description": "Common permission and authorization utilities for backend plugins", "backstage": { "role": "node-library", diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index 15f5f02520..fd2683ed4d 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-proxy-backend +## 0.6.6 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-proxy-node@0.1.8 + ## 0.6.6-next.0 ### Patch Changes diff --git a/plugins/proxy-backend/knip-report.md b/plugins/proxy-backend/knip-report.md index 7df8949921..3f66c63579 100644 --- a/plugins/proxy-backend/knip-report.md +++ b/plugins/proxy-backend/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------------------- | :----------- | :------- | -| @backstage/backend-app-api | package.json | error | +| @backstage/backend-app-api | plugins/proxy-backend/package.json | error | diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 5413888fca..76501017c5 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-proxy-backend", - "version": "0.6.6-next.0", + "version": "0.6.6", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", "backstage": { "role": "backend-plugin", diff --git a/plugins/proxy-node/CHANGELOG.md b/plugins/proxy-node/CHANGELOG.md index b9d5e5987b..01cc972c66 100644 --- a/plugins/proxy-node/CHANGELOG.md +++ b/plugins/proxy-node/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-proxy-node +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + ## 0.1.8-next.0 ### Patch Changes diff --git a/plugins/proxy-node/knip-report.md b/plugins/proxy-node/knip-report.md index e7921e52b4..7d1ae72c58 100644 --- a/plugins/proxy-node/knip-report.md +++ b/plugins/proxy-node/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :---------------------------- | :----------- | :------- | -| @backstage/backend-test-utils | package.json | error | -| @backstage/config | package.json | error | +| @backstage/backend-test-utils | plugins/proxy-node/package.json | error | +| @backstage/config | plugins/proxy-node/package.json | error | diff --git a/plugins/proxy-node/package.json b/plugins/proxy-node/package.json index 4ad0c63bff..79453d4c6c 100644 --- a/plugins/proxy-node/package.json +++ b/plugins/proxy-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-proxy-node", - "version": "0.1.8-next.0", + "version": "0.1.8", "description": "The plugin-proxy-node module for @backstage/plugin-proxy-backend", "backstage": { "role": "node-library", diff --git a/plugins/scaffolder-backend-module-azure/CHANGELOG.md b/plugins/scaffolder-backend-module-azure/CHANGELOG.md index 5f00e22a34..fc6d08bf48 100644 --- a/plugins/scaffolder-backend-module-azure/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-azure/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-azure +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-azure/package.json b/plugins/scaffolder-backend-module-azure/package.json index 8dff9c680e..c1db900f79 100644 --- a/plugins/scaffolder-backend-module-azure/package.json +++ b/plugins/scaffolder-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-azure", - "version": "0.2.13-next.0", + "version": "0.2.13", "description": "The azure module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md index 10a2cde00b..cc61bd54fd 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-cloud +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-bitbucket-cloud-common@0.3.2 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json index ae94ed0caa..a68a30da84 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud", - "version": "0.2.13-next.0", + "version": "0.2.13", "description": "The Bitbucket Cloud module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md index 1fe58506b1..ff9e8a84d1 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-server +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket-server/knip-report.md b/plugins/scaffolder-backend-module-bitbucket-server/knip-report.md index b9ed10af2c..0dd672d03d 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/knip-report.md +++ b/plugins/scaffolder-backend-module-bitbucket-server/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-- | :----------- | :------- | -| zod | package.json | error | +| zod | plugins/scaffolder-backend-module-bitbucket-server/package.json | error | diff --git a/plugins/scaffolder-backend-module-bitbucket-server/package.json b/plugins/scaffolder-backend-module-bitbucket-server/package.json index 818033a8fd..452653afb7 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket-server", - "version": "0.2.13-next.0", + "version": "0.2.13", "description": "The Bitbucket Server module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md index b9d50d9591..25ff743f3c 100644 --- a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket +## 0.3.14 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.13 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.13 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.3.14-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket/knip-report.md b/plugins/scaffolder-backend-module-bitbucket/knip-report.md index 8c142397d7..c8b2aa1788 100644 --- a/plugins/scaffolder-backend-module-bitbucket/knip-report.md +++ b/plugins/scaffolder-backend-module-bitbucket/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| fs-extra | package.json | error | +| fs-extra | plugins/scaffolder-backend-module-bitbucket/package.json | error | diff --git a/plugins/scaffolder-backend-module-bitbucket/package.json b/plugins/scaffolder-backend-module-bitbucket/package.json index 4b54d49087..b88bfcc259 100644 --- a/plugins/scaffolder-backend-module-bitbucket/package.json +++ b/plugins/scaffolder-backend-module-bitbucket/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket", - "version": "0.3.14-next.0", + "version": "0.3.14", "description": "The bitbucket module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md index 09560b5526..354a075745 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-confluence-to-markdown +## 0.3.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.3.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json index 06a9627649..a708176e47 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown", - "version": "0.3.13-next.0", + "version": "0.3.13", "description": "The confluence-to-markdown module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 8766a54513..24b6ca0dd4 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.3.15-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/knip-report.md b/plugins/scaffolder-backend-module-cookiecutter/knip-report.md index 0e8b43df56..5d34678cdf 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/knip-report.md +++ b/plugins/scaffolder-backend-module-cookiecutter/knip-report.md @@ -4,7 +4,7 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/backend-defaults | package.json | error | -| winston | package.json | error | -| yn | package.json | error | +| @backstage/backend-defaults | plugins/scaffolder-backend-module-cookiecutter/package.json | error | +| winston | plugins/scaffolder-backend-module-cookiecutter/package.json | error | +| yn | plugins/scaffolder-backend-module-cookiecutter/package.json | error | diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 32d3ec7be7..6300973618 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", - "version": "0.3.15-next.0", + "version": "0.3.15", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gcp/CHANGELOG.md b/plugins/scaffolder-backend-module-gcp/CHANGELOG.md index 0da8a53d22..e7c99d1075 100644 --- a/plugins/scaffolder-backend-module-gcp/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-gcp +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gcp/knip-report.md b/plugins/scaffolder-backend-module-gcp/knip-report.md index 5eace4cfc6..3e771319e4 100644 --- a/plugins/scaffolder-backend-module-gcp/knip-report.md +++ b/plugins/scaffolder-backend-module-gcp/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :--------------------- | :----------- | :------- | -| @backstage/integration | package.json | error | -| @backstage/errors | package.json | error | +| @backstage/integration | plugins/scaffolder-backend-module-gcp/package.json | error | +| @backstage/errors | plugins/scaffolder-backend-module-gcp/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :------------------------------------------- | :----------- | :------- | -| @backstage/plugin-scaffolder-node-test-utils | package.json | error | +| @backstage/plugin-scaffolder-node-test-utils | plugins/scaffolder-backend-module-gcp/package.json | error | diff --git a/plugins/scaffolder-backend-module-gcp/package.json b/plugins/scaffolder-backend-module-gcp/package.json index 20ba1f6148..bc3a2cbabb 100644 --- a/plugins/scaffolder-backend-module-gcp/package.json +++ b/plugins/scaffolder-backend-module-gcp/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gcp", - "version": "0.2.13-next.0", + "version": "0.2.13", "description": "The GCP Bucket module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md b/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md index 462bdce34f..c6b2802b91 100644 --- a/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-gerrit +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gerrit/package.json b/plugins/scaffolder-backend-module-gerrit/package.json index 6939ff2cc0..3a4013a54e 100644 --- a/plugins/scaffolder-backend-module-gerrit/package.json +++ b/plugins/scaffolder-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gerrit", - "version": "0.2.13-next.0", + "version": "0.2.13", "description": "The gerrit module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md index 3a166448f7..4c76e53bf5 100644 --- a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-gitea +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index 5ee0625450..acc972343f 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitea", - "version": "0.2.13-next.0", + "version": "0.2.13", "description": "The gitea module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-github/CHANGELOG.md b/plugins/scaffolder-backend-module-github/CHANGELOG.md index 9e99278912..7f7ba5a597 100644 --- a/plugins/scaffolder-backend-module-github/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-github/CHANGELOG.md @@ -1,5 +1,74 @@ # @backstage/plugin-scaffolder-backend-module-github +## 0.9.0 + +### Minor Changes + +- f0f06b4: Adding a new scaffolder action `github:issues:create` following the reference of `github:issues:label` with `dryRun` testing possibility + + It can be used like this + + ``` + steps: + - id: create-simple-issue + name: Create Simple Issue + action: github:issues:create + input: + repoUrl: ${{ parameters.repoUrl }} + title: "[${{ parameters.projectName }}] Simple Bug Report" + body: | + ## Bug Description + This is a simple bug report created by the scaffolder template. + + ### Steps to Reproduce + 1. Run the application + 2. Navigate to the main page + 3. Click on the problematic button + + ### Expected Behavior + The button should work correctly. + + ### Actual Behavior + The button does not respond to clicks. + output: + links: + - title: Simple Issue + url: ${{ steps['create-simple-issue'].output.issueUrl }} + ``` + +### Patch Changes + +- aee107b: Add `auto_init` option to `github:repo:create` action to create repository with an initial commit containing a README.md file + + This initial commit is created by GitHub itself and the commit is signed, so the repository will not be empty after creation. + + ```diff + - action: github:repo:create + id: init-new-repo + input: + repoUrl: 'github.com?repo=repo&owner=owner' + description: This is the description + visibility: private + + autoInit: true + + ``` + +- 6393b78: Add block creations field in github branch protection scaffolder actions +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## 0.8.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.8.3-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-github/knip-report.md b/plugins/scaffolder-backend-module-github/knip-report.md index f6577387b7..75e49c21a1 100644 --- a/plugins/scaffolder-backend-module-github/knip-report.md +++ b/plugins/scaffolder-backend-module-github/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------ | :---------- | :------- | -| @octokit/core | src/util.ts | error | +| @octokit/core | plugins/scaffolder-backend-module-github/src/util.ts | error | diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index 15dedf5ac8..50fc1646e0 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-github", - "version": "0.8.3-next.0", + "version": "0.9.0", "description": "The github module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index 85ca128c34..ed109eb9e3 100644 --- a/plugins/scaffolder-backend-module-github/report.api.md +++ b/plugins/scaffolder-backend-module-github/report.api.md @@ -81,6 +81,7 @@ export function createGithubBranchProtectionAction(options: { requireLastPushApproval?: boolean | undefined; requiredCommitSigning?: boolean | undefined; requiredLinearHistory?: boolean | undefined; + blockCreations?: boolean | undefined; token?: string | undefined; }, { @@ -136,6 +137,27 @@ export function createGithubEnvironmentAction(options: { 'v2' >; +// @public +export function createGithubIssuesCreateAction(options: { + integrations: ScmIntegrationRegistry; + githubCredentialsProvider?: GithubCredentialsProvider; +}): TemplateAction< + { + repoUrl: string; + title: string; + body?: string | undefined; + assignees?: string[] | undefined; + milestone?: string | number | undefined; + labels?: string[] | undefined; + token?: string | undefined; + }, + { + issueUrl: string; + issueNumber: number; + }, + 'v2' +>; + // @public export function createGithubIssuesLabelAction(options: { integrations: ScmIntegrationRegistry; @@ -269,6 +291,7 @@ export function createGithubRepoCreateAction(options: { requiredLinearHistory?: boolean | undefined; customProperties?: Record | undefined; subscribe?: boolean | undefined; + autoInit?: boolean | undefined; }, { remoteUrl: string; diff --git a/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts b/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts index c720690ee6..d7ef3496dd 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts @@ -44,6 +44,7 @@ type BranchProtectionOptions = { dismissStaleReviews?: boolean; requiredCommitSigning?: boolean; requiredLinearHistory?: boolean; + blockCreations?: boolean; }; export const enableBranchProtectionOnDefaultRepoBranch = async ({ @@ -64,6 +65,7 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ dismissStaleReviews = false, requiredCommitSigning = false, requiredLinearHistory = false, + blockCreations = false, }: BranchProtectionOptions): Promise => { const tryOnce = async () => { try { @@ -96,6 +98,7 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ }, required_conversation_resolution: requiredConversationResolution, required_linear_history: requiredLinearHistory, + block_creations: blockCreations, }); if (requiredCommitSigning) { diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.examples.test.ts index 31a15f6019..b02d451992 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.examples.test.ts @@ -99,6 +99,7 @@ describe('github:branch-protection:create', () => { }, required_conversation_resolution: false, required_linear_history: false, + block_creations: false, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, @@ -132,6 +133,7 @@ describe('github:branch-protection:create', () => { }, required_conversation_resolution: false, required_linear_history: false, + block_creations: false, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, @@ -165,6 +167,7 @@ describe('github:branch-protection:create', () => { }, required_conversation_resolution: true, required_linear_history: false, + block_creations: false, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, @@ -202,6 +205,7 @@ describe('github:branch-protection:create', () => { }, required_conversation_resolution: true, required_linear_history: true, + block_creations: false, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.test.ts index d20a02d460..98d296e176 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.test.ts @@ -105,6 +105,7 @@ describe('github:branch-protection:create', () => { }, required_conversation_resolution: false, required_linear_history: false, + block_creations: false, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, @@ -142,6 +143,7 @@ describe('github:branch-protection:create', () => { }, required_conversation_resolution: false, required_linear_history: false, + block_creations: false, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, @@ -178,6 +180,7 @@ describe('github:branch-protection:create', () => { requireLastPushApproval: true, requiredCommitSigning: true, requiredLinearHistory: true, + blockCreations: true, }, }); @@ -211,6 +214,7 @@ describe('github:branch-protection:create', () => { }, required_conversation_resolution: true, required_linear_history: true, + block_creations: true, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts index f82bfc6950..ee8555661b 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts @@ -57,6 +57,7 @@ export function createGithubBranchProtectionAction(options: { requireLastPushApproval: inputProps.requireLastPushApproval, requiredCommitSigning: inputProps.requiredCommitSigning, requiredLinearHistory: inputProps.requiredLinearHistory, + blockCreations: inputProps.blockCreations, token: inputProps.token, }, }, @@ -76,6 +77,7 @@ export function createGithubBranchProtectionAction(options: { requireLastPushApproval = false, requiredCommitSigning = false, requiredLinearHistory = false, + blockCreations, token: providedToken, } = ctx.input; @@ -129,6 +131,7 @@ export function createGithubBranchProtectionAction(options: { dismissStaleReviews, requiredCommitSigning, requiredLinearHistory, + blockCreations, }); }, }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.examples.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.examples.ts new file mode 100644 index 0000000000..7d767f0337 --- /dev/null +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.examples.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 { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import * as yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Create a simple issue', + example: yaml.stringify({ + steps: [ + { + action: 'github:issues:create', + name: 'Create issue', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + title: 'Bug report', + body: 'Found a bug that needs to be fixed', + }, + }, + ], + }), + }, + { + description: 'Create an issue with labels and assignees', + example: yaml.stringify({ + steps: [ + { + action: 'github:issues:create', + name: 'Create issue with metadata', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + title: 'Feature request', + body: 'This is a new feature request', + labels: ['enhancement', 'needs-review'], + assignees: ['octocat'], + milestone: 1, + }, + }, + ], + }), + }, + { + description: 'Create an issue with specific token', + example: yaml.stringify({ + steps: [ + { + action: 'github:issues:create', + name: 'Create issue with token', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + title: 'Documentation update', + body: 'Update the documentation for the new API', + labels: ['documentation'], + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.test.ts new file mode 100644 index 0000000000..474acf14d7 --- /dev/null +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.test.ts @@ -0,0 +1,160 @@ +/* + * 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 { createGithubIssuesCreateAction } from './githubIssuesCreate'; +import { + ScmIntegrations, + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, +} from '@backstage/integration'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { getOctokitOptions } from '../util'; + +jest.mock('../util', () => { + return { + getOctokitOptions: jest.fn(), + }; +}); + +import { Octokit } from 'octokit'; + +const octokitMock = Octokit as unknown as jest.Mock; +const mockOctokit = { + rest: { + issues: { + create: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: jest.fn(), +})); + +describe('github:issues:create', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const getOctokitOptionsMock = getOctokitOptions as jest.Mock; + const integrations = ScmIntegrations.fromConfig(config); + let githubCredentialsProvider: GithubCredentialsProvider; + let action: TemplateAction; + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + title: 'Test Issue', + body: 'This is a test issue', + labels: ['bug', 'test'], + assignees: ['octocat'], + milestone: 1, + }, + }); + + beforeEach(() => { + jest.resetAllMocks(); + octokitMock.mockImplementation(() => mockOctokit); + mockOctokit.rest.issues.create.mockResolvedValue({ + data: { + html_url: 'https://github.com/owner/repo/issues/1', + number: 1, + }, + }); + githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + action = createGithubIssuesCreateAction({ + integrations, + githubCredentialsProvider, + }); + }); + + it('should pass context logger to Octokit client', async () => { + await action.handler(mockContext); + + expect(octokitMock).toHaveBeenCalledWith( + expect.objectContaining({ log: mockContext.logger }), + ); + }); + + it('should call the githubApi for creating issue', async () => { + await action.handler(mockContext); + expect(mockOctokit.rest.issues.create).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + title: 'Test Issue', + body: 'This is a test issue', + labels: ['bug', 'test'], + assignees: ['octocat'], + milestone: 1, + }); + expect(getOctokitOptionsMock.mock.calls[0][0].token).toBeUndefined(); + }); + + it('should call the githubApi for creating issue with token', async () => { + await action.handler({ + ...mockContext, + input: { ...mockContext.input, token: 'gph_YourGitHubToken' }, + }); + expect(mockOctokit.rest.issues.create).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + title: 'Test Issue', + body: 'This is a test issue', + labels: ['bug', 'test'], + assignees: ['octocat'], + milestone: 1, + }); + expect(getOctokitOptionsMock.mock.calls[0][0].token).toEqual( + 'gph_YourGitHubToken', + ); + }); + + it('should output issue URL and number', async () => { + await action.handler(mockContext); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://github.com/owner/repo/issues/1', + ); + expect(mockContext.output).toHaveBeenCalledWith('issueNumber', 1); + }); + + it('should create issue with minimal input', async () => { + const minimalContext = createMockActionContext({ + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + title: 'Simple Issue', + }, + }); + + await action.handler(minimalContext); + expect(mockOctokit.rest.issues.create).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + title: 'Simple Issue', + body: undefined, + labels: undefined, + assignees: undefined, + milestone: undefined, + }); + }); +}); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.ts new file mode 100644 index 0000000000..5c5703e20d --- /dev/null +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.ts @@ -0,0 +1,182 @@ +/* + * 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 { + GithubCredentialsProvider, + ScmIntegrationRegistry, +} from '@backstage/integration'; +import { + createTemplateAction, + parseRepoUrl, +} from '@backstage/plugin-scaffolder-node'; +import { assertError, InputError } from '@backstage/errors'; +import { Octokit } from 'octokit'; +import { getOctokitOptions } from '../util'; +import { examples } from './githubIssuesCreate.examples'; + +/** + * Creates an issue on GitHub + * @public + */ +export function createGithubIssuesCreateAction(options: { + integrations: ScmIntegrationRegistry; + githubCredentialsProvider?: GithubCredentialsProvider; +}) { + const { integrations, githubCredentialsProvider } = options; + + return createTemplateAction({ + id: 'github:issues:create', + description: 'Creates an issue on GitHub.', + examples, + supportsDryRun: true, + schema: { + input: { + repoUrl: z => + z.string({ + description: + 'Accepts the format `github.com?repo=reponame&owner=owner` where `reponame` is the repository name and `owner` is an organization or username', + }), + title: z => + z.string({ + description: 'The title of the issue', + }), + body: z => + z + .string({ + description: 'The contents of the issue', + }) + .optional(), + assignees: z => + z + .array(z.string(), { + description: + 'Logins for Users to assign to this issue. NOTE: Only users with push access can set assignees for new issues. Assignees are silently dropped otherwise.', + }) + .optional(), + milestone: z => + z + .union([z.string(), z.number()], { + description: + 'The number of the milestone to associate this issue with. NOTE: Only users with push access can set the milestone for new issues. The milestone is silently dropped otherwise.', + }) + .optional(), + labels: z => + z + .array(z.string(), { + description: + 'Labels to associate with this issue. NOTE: Only users with push access can set labels for new issues. Labels are silently dropped otherwise.', + }) + .optional(), + token: z => + z + .string({ + description: + 'The `GITHUB_TOKEN` to use for authorization to GitHub', + }) + .optional(), + }, + output: { + issueUrl: z => + z.string({ + description: 'The URL of the created issue', + }), + issueNumber: z => + z.number({ + description: 'The number of the created issue', + }), + }, + }, + async handler(ctx) { + const { + repoUrl, + title, + body, + assignees, + milestone, + labels, + token: providedToken, + } = ctx.input; + + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + ctx.logger.info(`Creating issue "${title}" on repo ${repo}`); + + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + host, + owner, + repo, + token: providedToken, + }); + + const client = new Octokit({ + ...octokitOptions, + log: ctx.logger, + }); + + if (ctx.isDryRun) { + ctx.logger.info(`Performing dry run of creating issue "${title}"`); + ctx.output('issueUrl', `https://github.com/${owner}/${repo}/issues/42`); + ctx.output('issueNumber', 42); + ctx.logger.info(`Dry run complete`); + return; + } + + try { + const issue = await ctx.checkpoint({ + key: `github.issues.create.${owner}.${repo}.${title}`, + fn: async () => { + const response = await client.rest.issues.create({ + owner, + repo, + title, + body, + assignees, + milestone, + labels, + }); + + return { + html_url: response.data.html_url, + number: response.data.number, + }; + }, + }); + + if (!issue) { + throw new Error('Failed to create issue'); + } + + ctx.output('issueUrl', issue.html_url); + ctx.output('issueNumber', issue.number); + + ctx.logger.info( + `Successfully created issue #${issue.number}: ${issue.html_url}`, + ); + } catch (e) { + assertError(e); + ctx.logger.warn( + `Failed: creating issue '${title}' on repo: '${repo}', ${e.message}`, + ); + throw e; + } + }, + }); +} diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts index da4837adb7..c37e9c222b 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts @@ -136,6 +136,7 @@ describe('github:repo:create examples', () => { has_wiki: undefined, homepage: undefined, visibility: 'private', + auto_init: undefined, }); }); @@ -169,6 +170,7 @@ describe('github:repo:create examples', () => { has_wiki: undefined, homepage: undefined, visibility: 'private', + auto_init: undefined, }); }); @@ -203,6 +205,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: false, // disable wiki homepage: undefined, + auto_init: undefined, }); }); @@ -246,6 +249,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: 'https://example.com', + auto_init: undefined, }); }); @@ -289,6 +293,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -332,6 +337,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -375,6 +381,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -418,6 +425,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -461,6 +469,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -504,6 +513,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -547,6 +557,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -590,6 +601,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -633,6 +645,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -676,6 +689,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -719,6 +733,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -762,6 +777,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -805,6 +821,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -848,6 +865,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -891,6 +909,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -934,6 +953,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -977,6 +997,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: 'https://project-xyz.com', + auto_init: undefined, }); }); @@ -1020,6 +1041,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1063,6 +1085,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1106,6 +1129,7 @@ describe('github:repo:create examples', () => { has_projects: true, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1149,6 +1173,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1192,6 +1217,7 @@ describe('github:repo:create examples', () => { has_projects: false, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1235,6 +1261,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1278,6 +1305,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1321,6 +1349,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1364,6 +1393,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1407,6 +1437,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1450,6 +1481,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1493,6 +1525,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1536,6 +1569,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1579,6 +1613,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: 'https://internal.example.com', + auto_init: undefined, }); }); @@ -1622,6 +1657,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1665,6 +1701,7 @@ describe('github:repo:create examples', () => { has_projects: true, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1708,6 +1745,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1751,6 +1789,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1794,6 +1833,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1837,6 +1877,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: 'https://webapp.example.com', + auto_init: undefined, }); }); @@ -1880,6 +1921,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1923,6 +1965,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1966,6 +2009,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2009,6 +2053,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2052,6 +2097,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2095,6 +2141,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: false, homepage: undefined, + auto_init: undefined, }); }); @@ -2138,6 +2185,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2181,6 +2229,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2224,6 +2273,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2267,6 +2317,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2310,6 +2361,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: 'https://example.com', + auto_init: undefined, }); }); @@ -2353,6 +2405,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2396,6 +2449,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2439,6 +2493,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2482,6 +2537,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2525,6 +2581,51 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, + }); + }); + + it(`Should ${examples[58].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[58].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_update_branch: false, + custom_properties: undefined, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + auto_init: true, }); }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts index ce39e7510d..41c6a15fba 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts @@ -1006,4 +1006,19 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Create a repository with an initial commit.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with an initial (signed) commit containing a README', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + autoInit: true, + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts index b90f0c4e5a..adad5237da 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts @@ -157,6 +157,7 @@ describe('github:repo:create', () => { has_wiki: undefined, homepage: undefined, visibility: 'private', + auto_init: undefined, }); await action.handler({ @@ -185,6 +186,7 @@ describe('github:repo:create', () => { has_wiki: undefined, homepage: undefined, visibility: 'public', + auto_init: undefined, }); await action.handler({ @@ -212,6 +214,7 @@ describe('github:repo:create', () => { has_projects: undefined, has_wiki: undefined, visibility: 'private', + auto_init: undefined, }); await action.handler({ @@ -242,6 +245,7 @@ describe('github:repo:create', () => { has_projects: undefined, has_issues: undefined, homepage: 'https://example.com', + auto_init: undefined, }); await action.handler({ @@ -271,6 +275,7 @@ describe('github:repo:create', () => { has_wiki: undefined, has_projects: undefined, has_issues: undefined, + auto_init: undefined, }); await action.handler({ @@ -303,6 +308,67 @@ describe('github:repo:create', () => { has_wiki: undefined, homepage: 'https://example.com', visibility: 'private', + auto_init: undefined, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + autoInit: true, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + description: 'description', + 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, + allow_update_branch: false, + custom_properties: undefined, + has_issues: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + visibility: 'private', + auto_init: true, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + autoInit: false, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + description: 'description', + 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, + allow_update_branch: false, + custom_properties: undefined, + has_issues: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + visibility: 'private', + auto_init: false, }); }); @@ -334,6 +400,7 @@ describe('github:repo:create', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); await action.handler({ @@ -361,6 +428,7 @@ describe('github:repo:create', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); await action.handler({ @@ -388,6 +456,7 @@ describe('github:repo:create', () => { has_issues: undefined, has_projects: undefined, has_wiki: undefined, + auto_init: undefined, }); await action.handler({ @@ -417,6 +486,7 @@ describe('github:repo:create', () => { has_projects: undefined, has_issues: undefined, homepage: 'https://example.com', + auto_init: undefined, }); await action.handler({ @@ -446,6 +516,7 @@ describe('github:repo:create', () => { has_projects: undefined, has_issues: undefined, homepage: 'https://example.com', + auto_init: undefined, }); // Custom properties on user repos should be ignored @@ -478,6 +549,63 @@ describe('github:repo:create', () => { has_projects: undefined, has_wiki: undefined, homepage: 'https://example.com', + auto_init: undefined, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + autoInit: true, + }, + }); + expect( + mockOctokit.rest.repos.createForAuthenticatedUser, + ).toHaveBeenCalledWith({ + description: 'description', + name: 'repo', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_message: 'COMMIT_MESSAGES', + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + allow_update_branch: false, + has_wiki: undefined, + has_projects: undefined, + has_issues: undefined, + homepage: undefined, + auto_init: true, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + autoInit: false, + }, + }); + expect( + mockOctokit.rest.repos.createForAuthenticatedUser, + ).toHaveBeenCalledWith({ + description: 'description', + name: 'repo', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_message: 'COMMIT_MESSAGES', + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + allow_update_branch: false, + has_wiki: undefined, + has_projects: undefined, + has_issues: undefined, + homepage: undefined, + auto_init: false, }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts index a85010797d..952bf514d4 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts @@ -81,6 +81,7 @@ export function createGithubRepoCreateAction(options: { requiredLinearHistory: inputProps.requiredLinearHistory, customProperties: inputProps.customProperties, subscribe: inputProps.subscribe, + autoInit: inputProps.autoInit, }, output: { remoteUrl: outputProps.remoteUrl, @@ -113,6 +114,7 @@ export function createGithubRepoCreateAction(options: { customProperties, subscribe, token: providedToken, + autoInit = undefined, } = ctx.input; const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); @@ -164,6 +166,7 @@ export function createGithubRepoCreateAction(options: { customProperties, subscribe, ctx.logger, + autoInit, ); return newRepo.clone_url; }, diff --git a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts index 6509d7fd29..a3d0caed33 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts @@ -78,6 +78,7 @@ export async function createGithubRepoWithCollaboratorsAndTopics( customProperties: { [key: string]: string | string[] } | undefined, subscribe: boolean | undefined, logger: LoggerService, + autoInit?: boolean | undefined, ) { // eslint-disable-next-line testing-library/no-await-sync-queries const user = await client.rest.users.getByUsername({ @@ -109,6 +110,7 @@ export async function createGithubRepoWithCollaboratorsAndTopics( has_projects: hasProjects, has_wiki: hasWiki, has_issues: hasIssues, + auto_init: autoInit, // Custom properties only available on org repos custom_properties: customProperties, }) @@ -128,6 +130,7 @@ export async function createGithubRepoWithCollaboratorsAndTopics( has_projects: hasProjects, has_wiki: hasWiki, has_issues: hasIssues, + auto_init: autoInit, }); let newRepo; diff --git a/plugins/scaffolder-backend-module-github/src/actions/index.ts b/plugins/scaffolder-backend-module-github/src/actions/index.ts index 7cf4109e6b..21732d7908 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/index.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/index.ts @@ -16,6 +16,7 @@ export { createGithubActionsDispatchAction } from './githubActionsDispatch'; export { createGithubIssuesLabelAction } from './githubIssuesLabel'; +export { createGithubIssuesCreateAction } from './githubIssuesCreate'; export { createGithubRepoCreateAction } from './githubRepoCreate'; export { createGithubRepoPushAction } from './githubRepoPush'; export { createGithubWebhookAction } from './githubWebhook'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts b/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts index e1df40f85d..8a901b0e99 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts @@ -348,6 +348,14 @@ const requiredLinearHistory = (z: typeof zod) => }) .optional(); +const blockCreations = (z: typeof zod) => + z + .boolean({ + description: `Prevents creation of new branches during push, unless the push is initiated by a user, team, or app (defined in restrictions) which has the ability to push.`, + }) + .default(false) + .optional(); + const repoVariables = (z: typeof zod) => z .record(z.string(), { @@ -405,6 +413,14 @@ const branch = (z: typeof zod) => }) .optional(); +const autoInit = (z: typeof zod) => + z + .boolean({ + description: `Create an initial commit with empty README. Default is 'false'`, + }) + .default(false) + .optional(); + export { repoUrl, description, @@ -449,4 +465,6 @@ export { protectEnforceAdmins, bypassPullRequestAllowances, branch, + blockCreations, + autoInit, }; diff --git a/plugins/scaffolder-backend-module-github/src/module.ts b/plugins/scaffolder-backend-module-github/src/module.ts index 5812a22560..67bda58fc4 100644 --- a/plugins/scaffolder-backend-module-github/src/module.ts +++ b/plugins/scaffolder-backend-module-github/src/module.ts @@ -27,6 +27,7 @@ import { createGithubDeployKeyAction, createGithubEnvironmentAction, createGithubIssuesLabelAction, + createGithubIssuesCreateAction, createGithubRepoCreateAction, createGithubRepoPushAction, createGithubWebhookAction, @@ -82,6 +83,10 @@ export const githubModule = createBackendModule({ integrations, githubCredentialsProvider, }), + createGithubIssuesCreateAction({ + integrations, + githubCredentialsProvider, + }), createGithubRepoCreateAction({ integrations, githubCredentialsProvider, diff --git a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md index c6a42b8e6a..df082eb7eb 100644 --- a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-backend-module-gitlab +## 0.9.5 + +### Patch Changes + +- a84ddea: The log message now indicates that the pipeline trigger token was deleted and not pipeline itself. +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.9.5-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitlab/knip-report.md b/plugins/scaffolder-backend-module-gitlab/knip-report.md index c955ca23f0..3e1fc873d4 100644 --- a/plugins/scaffolder-backend-module-gitlab/knip-report.md +++ b/plugins/scaffolder-backend-module-gitlab/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------ | :----------- | :------- | -| winston | package.json | error | +| winston | plugins/scaffolder-backend-module-gitlab/package.json | error | diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index 495563a7c1..f8194a434c 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.9.5-next.0", + "version": "0.9.5", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend-module-notifications/CHANGELOG.md b/plugins/scaffolder-backend-module-notifications/CHANGELOG.md index 29fe4c33d4..bf28a5d88f 100644 --- a/plugins/scaffolder-backend-module-notifications/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-notifications/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-notifications +## 0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-node@0.2.19 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.1.14-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-notifications/package.json b/plugins/scaffolder-backend-module-notifications/package.json index d3f0b19e7e..22aaa22396 100644 --- a/plugins/scaffolder-backend-module-notifications/package.json +++ b/plugins/scaffolder-backend-module-notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-notifications", - "version": "0.1.14-next.0", + "version": "0.1.14", "description": "The notifications backend module for the scaffolder plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index afd81c17cd..5970f3f8a2 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.5.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.5.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/knip-report.md b/plugins/scaffolder-backend-module-rails/knip-report.md index 38a14d270f..c7301f2b85 100644 --- a/plugins/scaffolder-backend-module-rails/knip-report.md +++ b/plugins/scaffolder-backend-module-rails/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-------- | :----------- | :------- | -| jest-when | package.json | error | +| jest-when | plugins/scaffolder-backend-module-rails/package.json | error | diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index fb74e346bd..c6f57ae2a4 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", - "version": "0.5.13-next.0", + "version": "0.5.13", "description": "A module for the scaffolder backend that lets you template projects using Rails", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-rails/report.api.md b/plugins/scaffolder-backend-module-rails/report.api.md index a8abcfee1e..c7bbd19a72 100644 --- a/plugins/scaffolder-backend-module-rails/report.api.md +++ b/plugins/scaffolder-backend-module-rails/report.api.md @@ -48,6 +48,7 @@ export function createFetchRailsAction(options: { railsArguments?: | { template?: string | undefined; + api?: boolean | undefined; database?: | 'sqlite3' | 'mysql' @@ -59,7 +60,6 @@ export function createFetchRailsAction(options: { | 'jdbcpostgresql' | 'jdbc' | undefined; - api?: boolean | undefined; force?: boolean | undefined; minimal?: boolean | undefined; railsVersion?: 'edge' | 'master' | 'dev' | 'fromImage' | undefined; diff --git a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md index ecc41bb537..097b7f38f3 100644 --- a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-scaffolder-backend-module-sentry +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index 492e4cc9e2..55930738b6 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.2.13-next.0", + "version": "0.2.13", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index 06e1e82c98..9ac2c16a7e 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.4.14 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + - @backstage/plugin-scaffolder-node-test-utils@0.3.3 + ## 0.4.14-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/knip-report.md b/plugins/scaffolder-backend-module-yeoman/knip-report.md index c955ca23f0..56fbe222c7 100644 --- a/plugins/scaffolder-backend-module-yeoman/knip-report.md +++ b/plugins/scaffolder-backend-module-yeoman/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------ | :----------- | :------- | -| winston | package.json | error | +| winston | plugins/scaffolder-backend-module-yeoman/package.json | error | diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index 0af061c967..e5b60c405d 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.4.14-next.1", + "version": "0.4.14", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 5fbb8edcc2..94b1ed5e93 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-scaffolder-backend +## 2.2.1 + +### Patch Changes + +- a57185f: Added support for executing actions from the `ActionsRegistry` in the `scaffolder-backend` +- c3405db: Fixed a regression that prevented uploads greater than 100KB. Uploads up to 10MB are supported again. +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-scaffolder-backend-module-github@0.9.0 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.9.5 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-bitbucket-cloud-common@0.3.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12 + - @backstage/plugin-permission-node@0.10.4 + - @backstage/plugin-scaffolder-backend-module-azure@0.2.13 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.14 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.13 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.13 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.2.13 + - @backstage/plugin-scaffolder-backend-module-gitea@0.2.13 + - @backstage/plugin-scaffolder-common@1.7.1 + - @backstage/plugin-scaffolder-node@0.11.1 + +## 2.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12-next.1 + - @backstage/plugin-scaffolder-backend-module-github@0.8.3-next.1 + ## 2.2.1-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend/knip-report.md b/plugins/scaffolder-backend/knip-report.md index b6fcf59bb3..eb8ddd05aa 100644 --- a/plugins/scaffolder-backend/knip-report.md +++ b/plugins/scaffolder-backend/knip-report.md @@ -4,24 +4,24 @@ | Name | Location | Severity | | :--------------------------------------------------------------- | :----------- | :------- | -| @backstage/plugin-catalog-backend-module-scaffolder-entity-model | package.json | error | -| @backstage/plugin-scaffolder-backend-module-bitbucket-server | package.json | error | -| @backstage/plugin-scaffolder-backend-module-bitbucket-cloud | package.json | error | -| @backstage/plugin-scaffolder-backend-module-bitbucket | package.json | error | -| @backstage/plugin-scaffolder-backend-module-gerrit | package.json | error | -| @backstage/plugin-scaffolder-backend-module-github | package.json | error | -| @backstage/plugin-scaffolder-backend-module-gitlab | package.json | error | -| @backstage/plugin-scaffolder-backend-module-azure | package.json | error | -| @backstage/plugin-scaffolder-backend-module-gitea | package.json | error | -| @backstage/plugin-bitbucket-cloud-common | package.json | error | -| @backstage/plugin-auth-node | package.json | error | -| concat-stream | package.json | error | -| p-limit | package.json | error | -| tar | package.json | error | +| @backstage/plugin-catalog-backend-module-scaffolder-entity-model | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-scaffolder-backend-module-bitbucket-server | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-scaffolder-backend-module-bitbucket-cloud | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-scaffolder-backend-module-bitbucket | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-scaffolder-backend-module-gerrit | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-scaffolder-backend-module-github | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-scaffolder-backend-module-gitlab | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-scaffolder-backend-module-azure | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-scaffolder-backend-module-gitea | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-bitbucket-cloud-common | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-auth-node | plugins/scaffolder-backend/package.json | error | +| concat-stream | plugins/scaffolder-backend/package.json | error | +| p-limit | plugins/scaffolder-backend/package.json | error | +| tar | plugins/scaffolder-backend/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :------------------------- | :----------- | :------- | -| @backstage/backend-app-api | package.json | error | +| @backstage/backend-app-api | plugins/scaffolder-backend/package.json | error | diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 6b2f9ef0cb..53aab2b891 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "2.2.1-next.0", + "version": "2.2.1", "description": "The Backstage backend plugin that helps you create new things", "backstage": { "role": "backend-plugin", diff --git a/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml b/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml index abd18b1d42..56a7e7f5c6 100644 --- a/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml @@ -32,7 +32,7 @@ spec: title: Title type: string description: Notification title - description: + info: title: Description type: string description: Notification longer description @@ -67,7 +67,7 @@ spec: recipients: ${{ parameters.recipients }} entityRefs: ${{ parameters.entityRefs }} title: ${{ parameters.title }} - description: ${{ parameters.description }} + info: ${{ parameters.info }} link: ${{ parameters.link }} severity: ${{ parameters.severity }} topic: ${{ parameters.topic }} diff --git a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts index 7ef3abcbe3..a00da9e524 100644 --- a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts +++ b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts @@ -56,6 +56,7 @@ import { convertFiltersToRecord, convertGlobalsToRecord, } from './util/templating'; +import { actionsServiceRef } from '@backstage/backend-plugin-api/alpha'; /** * Scaffolder plugin @@ -139,6 +140,7 @@ export const scaffolderPlugin = createBackendPlugin({ auditor: coreServices.auditor, catalog: catalogServiceRef, events: eventsServiceRef, + actionsRegistry: actionsServiceRef, }, async init({ logger, @@ -153,6 +155,7 @@ export const scaffolderPlugin = createBackendPlugin({ permissions, events, auditor, + actionsRegistry, }) { const log = loggerToWinstonLogger(logger); const integrations = ScmIntegrations.fromConfig(config); @@ -222,6 +225,7 @@ export const scaffolderPlugin = createBackendPlugin({ additionalWorkspaceProviders, events, auditor, + actionsRegistry, }); httpRouter.use(router); }, diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 467b590bed..16a6000224 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -68,6 +68,8 @@ import { import { createDefaultFilters } from '../lib/templating/filters/createDefaultFilters'; import { createRouter } from './router'; import { DatabaseTaskStore } from '../scaffolder/tasks/DatabaseTaskStore'; +import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha'; +import { ActionsService } from '@backstage/backend-plugin-api/alpha'; function createDatabase(): DatabaseService { return DatabaseManager.fromConfig( @@ -178,6 +180,7 @@ const createTestRouter = async ( | Record | CreatedTemplateGlobal[]; autocompleteHandlers?: Record; + actionsRegistry?: ActionsService; } = {}, ) => { const logger = mockServices.logger.mock({ @@ -246,6 +249,7 @@ const createTestRouter = async ( }), createDebugLogAction(), ], + actionsRegistry: overrides.actionsRegistry ?? actionsRegistryServiceMock(), }); router.use(mockErrorHandler()); @@ -275,6 +279,49 @@ describe('scaffolder router', () => { expect(response.body[0].id).toBeDefined(); expect(response.body.length).toBe(2); }); + + it('should include actions from the remote actions registry', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + mockActionsRegistry.register({ + name: 'my-demo-action', + title: 'Test', + description: 'Test', + schema: { + input: z => z.object({ name: z.string() }), + output: z => z.object({ name: z.string() }), + }, + action: async () => ({ output: { name: 'test' } }), + }); + const { router } = await createTestRouter({ + actionsRegistry: mockActionsRegistry, + }); + const response = await request(router).get('/v2/actions').send(); + + expect(response.status).toEqual(200); + expect(response.body.length).toBe(3); + + expect(response.body).toContainEqual({ + description: 'Test', + examples: [], + id: 'test:my-demo-action', + schema: { + input: { + $schema: 'http://json-schema.org/draft-07/schema#', + additionalProperties: false, + properties: { name: { type: 'string' } }, + required: ['name'], + type: 'object', + }, + output: { + $schema: 'http://json-schema.org/draft-07/schema#', + additionalProperties: false, + properties: { name: { type: 'string' } }, + required: ['name'], + type: 'object', + }, + }, + }); + }); }); describe('GET /v2/templating-extensions', () => { @@ -1385,34 +1432,24 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ expect.anything(), ); }); - it('disallows users from seeing tasks they do not own', async () => { - const { permissions, router, taskBroker } = await createTestRouter(); - jest - .spyOn(permissions, 'authorizeConditional') - .mockImplementationOnce(async () => [ - { - conditions: { - resourceType: 'scaffolder-task', - rule: 'IS_TASK_OWNER', - params: { createdBy: ['user'] }, - }, - pluginId: 'scaffolder', - resourceType: 'scaffolder-task', - result: AuthorizeResult.CONDITIONAL, + it('allows payloads up to 10MB', async () => { + const { unwrappedRouter } = await createTestRouter(); + const mockToken = mockCredentials.user.token(); + const mockTemplate = generateMockTemplate(); + + const response = await request(unwrappedRouter) + .post('/v2/dry-run') + .set('Authorization', `Bearer ${mockToken}`) + .send({ + template: mockTemplate, + values: { + requiredParameter1: 'A'.repeat(9 * 1024 * 1024), // ~9MB + requiredParameter2: 'required-value-2', }, - ]); - const response = await request(router).get( - `/v2/tasks?createdBy=not-user`, - ); - expect(taskBroker.list).toHaveBeenCalledWith({ - filters: { createdBy: ['not-user'], status: undefined }, - order: undefined, - pagination: { limit: undefined, offset: undefined }, - permissionFilters: { key: 'created_by', values: ['user'] }, - }); + directoryContents: [], + }); + expect(response.status).toBe(200); - expect(response.body.totalTasks).toBe(0); - expect(response.body.tasks).toEqual([]); }); }); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 9315e8ccb2..2a0792346d 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -26,6 +26,7 @@ import { resolveSafeChildPath, SchedulerService, } from '@backstage/backend-plugin-api'; +import { Schema } from 'jsonschema'; import { CompoundEntityRef, Entity, @@ -131,6 +132,8 @@ import { } from './rules'; import { TaskFilters } from '@backstage/plugin-scaffolder-node'; +import { ActionsService } from '@backstage/backend-plugin-api/alpha'; +import { isPlainObject } from 'lodash'; /** * RouterOptions @@ -163,6 +166,7 @@ export interface RouterOptions { events?: EventsService; auditor?: AuditorService; autocompleteHandlers?: Record; + actionsRegistry: ActionsService; } function isSupportedTemplate(entity: TemplateEntityV1beta3) { @@ -186,16 +190,19 @@ const readDuration = ( export async function createRouter( options: RouterOptions, ): Promise { - const router = await createOpenApiRouter(); - // Be generous in upload size to support a wide range of templates in dry-run mode. - router.use(express.json({ limit: '10MB' })); + const router = await createOpenApiRouter({ + middleware: [ + // Be generous in upload size to support a wide range of templates in dry-run mode. + express.json({ limit: '10MB' }), + ], + }); const { logger: parentLogger, config, database, catalog, - actions, + actions = [], scheduler, additionalTemplateFilters, additionalTemplateGlobals, @@ -207,6 +214,7 @@ export async function createRouter( auth, httpAuth, auditor, + actionsRegistry, } = options; const concurrentTasksLimit = @@ -298,7 +306,47 @@ export async function createRouter( workers.push(worker); } - actions?.forEach(action => actionRegistry.register(action)); + // TODO(blam): it's a little unfortunate that you have to restart the scaffolder + // backend in order to pick these up. We should really just make `ActionsRegistry.get()` async + // and then we can move this logic into the there instead. + // But we can't make those changes until next major. + // Alternatively, we could look at setting up a periodic task that refreshes the actions registry, but + // not feeling that it's worth the complexity. + const { actions: distributedActions } = await actionsRegistry.list({ + credentials: await auth.getOwnServiceCredentials(), + }); + + for (const action of actions) { + actionRegistry.register(action); + } + + for (const action of distributedActions) { + actionRegistry.register({ + id: action.id, + description: action.description, + examples: [], + supportsDryRun: + action.attributes?.readOnly === true && + action.attributes?.destructive === false, + handler: async ctx => { + const { output } = await actionsRegistry.invoke({ + id: action.id, + input: ctx.input, + credentials: await ctx.getInitiatorCredentials(), + }); + + if (isPlainObject(output)) { + for (const [key, value] of Object.entries(output as JsonObject)) { + ctx.output(key as keyof typeof output, value); + } + } + }, + schema: { + input: action.schema.input as Schema, + output: action.schema.output as Schema, + }, + }); + } const launchWorkers = () => workers.forEach(worker => worker.start()); diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md index bb407c70b8..2c8133b33c 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-scaffolder-common +## 1.7.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + ## 1.7.1-next.0 ### Patch Changes diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index 0f4af8faf9..63ddd99e9e 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-common", - "version": "1.7.1-next.0", + "version": "1.7.1", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", "backstage": { "role": "common-library", diff --git a/plugins/scaffolder-node-test-utils/CHANGELOG.md b/plugins/scaffolder-node-test-utils/CHANGELOG.md index 6497a73281..d14226a463 100644 --- a/plugins/scaffolder-node-test-utils/CHANGELOG.md +++ b/plugins/scaffolder-node-test-utils/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-node-test-utils +## 0.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@1.9.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.3.3-next.1 ### Patch Changes diff --git a/plugins/scaffolder-node-test-utils/knip-report.md b/plugins/scaffolder-node-test-utils/knip-report.md index beefceb792..9a0eed890a 100644 --- a/plugins/scaffolder-node-test-utils/knip-report.md +++ b/plugins/scaffolder-node-test-utils/knip-report.md @@ -4,7 +4,7 @@ | Name | Location | Severity | | :--------------- | :----------- | :------- | -| react-router-dom | package.json | error | -| react-dom | package.json | error | -| react | package.json | error | +| react-router-dom | plugins/scaffolder-node-test-utils/package.json | error | +| react-dom | plugins/scaffolder-node-test-utils/package.json | error | +| react | plugins/scaffolder-node-test-utils/package.json | error | diff --git a/plugins/scaffolder-node-test-utils/package.json b/plugins/scaffolder-node-test-utils/package.json index eb223f4580..8baac6e67f 100644 --- a/plugins/scaffolder-node-test-utils/package.json +++ b/plugins/scaffolder-node-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-node-test-utils", - "version": "0.3.3-next.1", + "version": "0.3.3", "backstage": { "role": "node-library", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-node/CHANGELOG.md b/plugins/scaffolder-node/CHANGELOG.md index 6af39da4a2..c322aa5700 100644 --- a/plugins/scaffolder-node/CHANGELOG.md +++ b/plugins/scaffolder-node/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-node +## 0.11.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-common@1.7.1 + ## 0.11.1-next.0 ### Patch Changes diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index a8a3016e28..a3ae56d3d5 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-node", - "version": "0.11.1-next.0", + "version": "0.11.1", "description": "The plugin-scaffolder-node module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "node-library", diff --git a/plugins/scaffolder-react/CHANGELOG.md b/plugins/scaffolder-react/CHANGELOG.md index 78d40108f4..12061a9561 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-scaffolder-react +## 1.19.1 + +### Patch Changes + +- 58fc108: Fix scaffolder task log stream not having a minimum height +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-common@1.7.1 + +## 1.19.1-next.2 + +### Patch Changes + +- 58fc108: Fix scaffolder task log stream not having a minimum height +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + ## 1.19.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder-react/knip-report.md b/plugins/scaffolder-react/knip-report.md index 38b9eed013..c1c8bd6b32 100644 --- a/plugins/scaffolder-react/knip-report.md +++ b/plugins/scaffolder-react/knip-report.md @@ -4,11 +4,11 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @backstage/catalog-client | package.json | error | +| @backstage/catalog-client | plugins/scaffolder-react/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :------------------------------- | :----------- | :------- | -| @backstage/plugin-catalog-common | package.json | error | +| @backstage/plugin-catalog-common | plugins/scaffolder-react/package.json | error | diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index 1d8737db72..f33c6e0a70 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-react", - "version": "1.19.1-next.1", + "version": "1.19.1", "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", "backstage": { "role": "web-library", diff --git a/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.tsx b/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.tsx index a0c4ce94cd..b88d96510e 100644 --- a/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.tsx +++ b/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.tsx @@ -21,6 +21,7 @@ const useStyles = makeStyles({ width: '100%', height: '100%', position: 'relative', + minHeight: 240, }, }); diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index bbb826965c..5b331f64d3 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-scaffolder +## 1.34.1 + +### Patch Changes + +- 0d415ae: Render a TechDocs link on the Scaffolder Template List page when templates include either `backstage.io/techdocs-ref` or `backstage.io/techdocs-entity` annotations, using the shared `buildTechDocsURL` helper. Also adds tests to verify both annotations and optional `backstage.io/techdocs-entity-path` are respected. +- 7151260: Prevent the MultiEntityPicker from removing options present in form state when new options are selected +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/integration@1.18.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-scaffolder-react@1.19.1 + - @backstage/integration-react@1.2.10 + - @backstage/plugin-scaffolder-common@1.7.1 + +## 1.34.1-next.2 + +### Patch Changes + +- 0d415ae: Render a TechDocs link on the Scaffolder Template List page when templates include either `backstage.io/techdocs-ref` or `backstage.io/techdocs-entity` annotations, using the shared `buildTechDocsURL` helper. Also adds tests to verify both annotations and optional `backstage.io/techdocs-entity-path` are respected. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + ## 1.34.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder/knip-report.md b/plugins/scaffolder/knip-report.md index 7d1731eb0b..38ae11af79 100644 --- a/plugins/scaffolder/knip-report.md +++ b/plugins/scaffolder/knip-report.md @@ -4,14 +4,14 @@ | Name | Location | Severity | | :------------------ | :----------- | :------- | -| json-schema-library | package.json | error | -| @rjsf/material-ui | package.json | error | -| react-resizable | package.json | error | -| git-url-parse | package.json | error | +| json-schema-library | plugins/scaffolder/package.json | error | +| @rjsf/material-ui | plugins/scaffolder/package.json | error | +| react-resizable | plugins/scaffolder/package.json | error | +| git-url-parse | plugins/scaffolder/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :--------------------- | :----------- | :------- | -| @types/react-resizable | package.json | error | +| @types/react-resizable | plugins/scaffolder/package.json | error | diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index bad13ef8d2..6190cefbac 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "1.34.1-next.1", + "version": "1.34.1", "description": "The Backstage plugin that helps you create new things", "backstage": { "role": "frontend-plugin", @@ -72,6 +72,8 @@ "@backstage/plugin-permission-react": "workspace:^", "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/plugin-scaffolder-react": "workspace:^", + "@backstage/plugin-techdocs-common": "workspace:^", + "@backstage/plugin-techdocs-react": "workspace:^", "@backstage/types": "workspace:^", "@codemirror/language": "^6.0.0", "@codemirror/legacy-modes": "^6.1.0", @@ -109,6 +111,7 @@ "@backstage/dev-utils": "workspace:^", "@backstage/plugin-catalog": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", + "@backstage/plugin-techdocs": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/dom": "^10.0.0", "@testing-library/jest-dom": "^6.0.0", diff --git a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx index a16474a4cf..da544cb2c0 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx @@ -27,13 +27,19 @@ import { TestApiProvider, mockApis, } from '@backstage/test-utils'; -import { rootRouteRef } from '../../../routes'; +import { rootRouteRef, viewTechDocRouteRef } from '../../../routes'; import { TemplateListPage } from './TemplateListPage'; +import { + TECHDOCS_ANNOTATION, + TECHDOCS_EXTERNAL_ANNOTATION, + TECHDOCS_EXTERNAL_PATH_ANNOTATION, +} from '@backstage/plugin-techdocs-common'; const mountedRoutes = { mountedRoutes: { '/': rootRouteRef, '/catalog/:namespace/:kind/:name': entityRouteRef, + '/docs/:namespace/:kind/:name': viewTechDocRouteRef, }, }; @@ -51,6 +57,127 @@ describe('TemplateListPage', () => { ], }); + describe('TechDocs link rendering', () => { + it('shows TechDocs link when template has backstage.io/techdocs-ref', async () => { + const mockCatalogApiWithDocs = catalogApiMock({ + entities: [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 'tmpl-a', + annotations: { [TECHDOCS_ANNOTATION]: 'dir:.' }, + }, + spec: { type: 'service' }, + }, + ], + }); + + const { findByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + expect(await findByText('View TechDocs')).toBeInTheDocument(); + }); + + it('shows TechDocs link when template has backstage.io/techdocs-entity', async () => { + const mockCatalogApiWithExternal = catalogApiMock({ + entities: [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 'tmpl-b', + annotations: { + [TECHDOCS_EXTERNAL_ANNOTATION]: 'component:default/other', + }, + }, + spec: { type: 'service' }, + }, + ], + }); + + const { findByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + expect(await findByText('View TechDocs')).toBeInTheDocument(); + }); + + it('appends path when backstage.io/techdocs-entity-path is set', async () => { + const mockCatalogApiWithPath = catalogApiMock({ + entities: [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 'tmpl-c', + annotations: { + [TECHDOCS_EXTERNAL_ANNOTATION]: 'component:default/other', + [TECHDOCS_EXTERNAL_PATH_ANNOTATION]: '/guides/start', + }, + }, + spec: { type: 'service' }, + }, + ], + }); + + const { findByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + const link = (await findByText('View TechDocs')).closest('a')!; + expect(link).toHaveAttribute( + 'href', + expect.stringMatching( + /\/docs\/default\/component\/other\/?(index\.html)?#?\/guides\/start|\/docs\/default\/component\/other\/guides\/start/, + ), + ); + }); + }); + it('should render the search bar for templates', async () => { const { getByPlaceholderText } = await renderInTestApp( { const additionalLinksForEntity = useCallback( (template: TemplateEntityV1beta3) => { - const { kind, namespace, name } = parseEntityRef( - stringifyEntityRef(template), - ); - return template.metadata.annotations?.['backstage.io/techdocs-ref'] && - viewTechDocsLink + if ( + !( + template.metadata.annotations?.[TECHDOCS_ANNOTATION] || + template.metadata.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION] + ) || + !viewTechDocsLink + ) { + return []; + } + + const url = buildTechDocsURL(template, viewTechDocsLink); + return url ? [ { icon: app.getSystemIcon('docs') ?? DocsIcon, text: t( 'templateListPage.additionalLinksForEntity.viewTechDocsTitle', ), - url: viewTechDocsLink({ kind, namespace, name }), + url, }, ] : []; diff --git a/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.test.tsx b/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.test.tsx index a02d793a34..fc824f20f9 100644 --- a/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.test.tsx @@ -262,6 +262,71 @@ describe('', () => { }); }); + describe('with existing form data', () => { + beforeEach(() => { + uiSchema = { 'ui:options': {} }; + props = { + onChange, + schema, + required, + uiSchema, + rawErrors, + formData: ['group:default/team-a'], + } as unknown as FieldProps; + }); + + it('preserves existing data on blur', async () => { + const { getByRole } = await renderInTestApp( + + + , + ); + + const input = getByRole('textbox'); + + fireEvent.change(input, { target: { value: 'squ' } }); + fireEvent.blur(input); + + expect(onChange).toHaveBeenCalledWith(['group:default/team-a', 'squ']); + }); + + it('preserves existing data on value create', async () => { + const { getByRole } = await renderInTestApp( + + + , + ); + + const input = getByRole('textbox'); + + fireEvent.change(input, { target: { value: 'squ' } }); + fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); + + expect(onChange).toHaveBeenCalledWith(['group:default/team-a', 'squ']); + }); + + it('preserves existing data on selecting an existing option', async () => { + catalogApi.getEntities.mockResolvedValue({ items: entities }); + + const { getByRole } = await renderInTestApp( + + + , + ); + + const input = getByRole('textbox'); + + fireEvent.mouseDown(input); + const optionA = screen.getByText('squad-b'); + await userEvent.click(optionA as HTMLElement); + + expect(onChange).toHaveBeenCalledWith([ + 'group:default/team-a', + 'group:default/squad-b', + ]); + }); + }); + describe('uses full entity ref', () => { beforeEach(() => { uiSchema = { diff --git a/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.tsx b/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.tsx index 57c4b87d6d..39130c4b78 100644 --- a/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.tsx @@ -50,6 +50,12 @@ import { scaffolderTranslationRef } from '../../../translation'; export { MultiEntityPickerSchema } from './schema'; +// AutocompleteChangeReason events that can be triggered when a user inputs a freeSolo option +const FREE_SOLO_EVENTS: readonly AutocompleteChangeReason[] = [ + 'blur', + 'create-option', +]; + /** * The underlying component that is rendered in the form for the `MultiEntityPicker` * field extension. @@ -110,29 +116,34 @@ export const MultiEntityPicker = (props: MultiEntityPickerProps) => { (_: any, refs: (string | Entity)[], reason: AutocompleteChangeReason) => { const values = refs .map(ref => { + // If the ref is not a string, then it was a selected option in the picker if (typeof ref !== 'string') { // if ref does not exist: pass 'undefined' to trigger validation for required value return ref ? stringifyEntityRef(ref as Entity) : undefined; } - if (reason === 'blur' || reason === 'create-option') { - // Add in default namespace, etc. - let entityRef = ref; - try { - // Attempt to parse the entity ref into it's full form. - entityRef = stringifyEntityRef( - parseEntityRef(ref as string, { - defaultKind, - defaultNamespace, - }), - ); - } catch (err) { - // If the passed in value isn't an entity ref, do nothing. - } - // We need to check against formData here as that's the previous value for this field. - if (formData?.includes(ref) || allowArbitraryValues) { - return entityRef; - } + // Add in default namespace, etc. + let entityRef = ref; + try { + // Attempt to parse the entity ref into it's full form. + entityRef = stringifyEntityRef( + parseEntityRef(ref as string, { + defaultKind, + defaultNamespace, + }), + ); + } catch (err) { + // If the passed in value isn't an entity ref, do nothing. + } + + // We need to check against formData here as that's the previous value for this field. + if ( + // If value already matches what exists in form data, allow it + formData?.includes(ref) || + // If arbitrary values are allowed and the reason is a free solo event, allow it + (allowArbitraryValues && FREE_SOLO_EVENTS.includes(reason)) + ) { + return entityRef; } return undefined; diff --git a/plugins/search-backend-module-catalog/CHANGELOG.md b/plugins/search-backend-module-catalog/CHANGELOG.md index e9a56fa5f9..5267a5f25a 100644 --- a/plugins/search-backend-module-catalog/CHANGELOG.md +++ b/plugins/search-backend-module-catalog/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-search-backend-module-catalog +## 0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + +## 0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.3.8-next.0 ### Patch Changes diff --git a/plugins/search-backend-module-catalog/package.json b/plugins/search-backend-module-catalog/package.json index 9bcc2287ff..8db4d6fb96 100644 --- a/plugins/search-backend-module-catalog/package.json +++ b/plugins/search-backend-module-catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-catalog", - "version": "0.3.8-next.0", + "version": "0.3.8", "description": "A module for the search backend that exports catalog modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index 69735daa2b..f9aaad3e06 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 1.7.6 + +### Patch Changes + +- cde70ca: Added support for batchKeyField in the Elasticsearch indexer to allow consistent document IDs during bulk uploads. +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + ## 1.7.6-next.0 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/config.d.ts b/plugins/search-backend-module-elasticsearch/config.d.ts index 731ad1c77e..00af29e372 100644 --- a/plugins/search-backend-module-elasticsearch/config.d.ts +++ b/plugins/search-backend-module-elasticsearch/config.d.ts @@ -29,6 +29,11 @@ export interface Config { * Batch size for elastic search indexing tasks. Defaults to 1000. */ batchSize?: number; + /** + * Defines the name of the field in each document that will be used to identify documents during a batch upload. + * If not provided, a custom ID will be generated for each document. + */ + batchKeyField?: string; /** * Options for configuring highlight settings * See https://www.elastic.co/guide/en/elasticsearch/reference/7.17/highlighting.html diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 7e56865e3c..7e87d35fc6 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", - "version": "1.7.6-next.0", + "version": "1.7.6", "description": "A module for the search backend that implements search using ElasticSearch", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-elasticsearch/report.api.md b/plugins/search-backend-module-elasticsearch/report.api.md index 19c44bd566..94cce4b319 100644 --- a/plugins/search-backend-module-elasticsearch/report.api.md +++ b/plugins/search-backend-module-elasticsearch/report.api.md @@ -137,7 +137,7 @@ export class ElasticSearchClientWrapper { // (undocumented) bulk(bulkOptions: { datasource: Readable; - onDocument: () => ElasticSearchIndexAction; + onDocument: (doc: any) => ElasticSearchIndexAction; refreshOnCompletion?: string | boolean; }): BulkHelper; // (undocumented) @@ -348,6 +348,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { indexPrefix: string, logger: LoggerService, batchSize: number, + batchKeyField?: string | undefined, highlightOptions?: ElasticSearchHighlightOptions, queryOptions?: ElasticSearchQueryConfig, ); @@ -393,6 +394,7 @@ export type ElasticSearchSearchEngineIndexerOptions = { logger: LoggerService; elasticSearchClientWrapper: ElasticSearchClientWrapper; batchSize: number; + batchKeyField?: string; skipRefresh?: boolean; }; diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientWrapper.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientWrapper.ts index 4bf8a3d998..14539b716f 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientWrapper.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientWrapper.ts @@ -115,7 +115,7 @@ export class ElasticSearchClientWrapper { bulk(bulkOptions: { datasource: Readable; - onDocument: () => ElasticSearchIndexAction; + onDocument: (doc: any) => ElasticSearchIndexAction; refreshOnCompletion?: string | boolean; }) { if (this.openSearchClient) { diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 2bd5c88f89..2e95385eb4 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -142,6 +142,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { private readonly indexPrefix: string, private readonly logger: LoggerService, private readonly batchSize: number, + private readonly batchKeyField?: string, highlightOptions?: ElasticSearchHighlightOptions, queryOptions?: ElasticSearchQueryConfig, ) { @@ -189,6 +190,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { logger, config.getOptionalNumber('search.elasticsearch.batchSize') ?? DEFAULT_INDEXER_BATCH_SIZE, + config.getOptionalString('search.elasticsearch.batchKeyField'), config.getOptional( 'search.elasticsearch.highlightOptions', ), @@ -345,6 +347,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { elasticSearchClientWrapper: this.elasticSearchClientWrapper, logger: indexerLogger, batchSize: this.batchSize, + batchKeyField: this.batchKeyField, skipRefresh: ( this diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts index 1590d5fb2a..0ba2882c61 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts @@ -351,4 +351,80 @@ describe('ElasticSearchSearchEngineIndexer', () => { expect(bulkSpy).toHaveBeenCalledTimes(1); expect(refreshSpy).toHaveBeenCalledTimes(0); }); + + it('indexes documents with custom batch key field', async () => { + indexer = new ElasticSearchSearchEngineIndexer({ + type: 'some-type', + indexPrefix: '', + indexSeparator: '-index__', + alias: 'some-type-index__search', + logger: mockServices.logger.mock(), + elasticSearchClientWrapper: clientWrapper, + batchSize: 1000, + skipRefresh: false, + batchKeyField: 'customId', + }); + + const documents = [ + { + title: 'testTerm', + text: 'testText', + location: 'test/location', + customId: '123', + }, + { + title: 'Another test', + text: 'Some more text', + location: 'test/location/2', + customId: '456', + }, + ]; + + await TestPipeline.fromIndexer(indexer).withDocuments(documents).execute(); + + expect(bulkSpy).toHaveBeenCalled(); + + const bulkBody = bulkSpy.mock.calls[0][0].body; + + expect(bulkBody[0]).toEqual( + expect.objectContaining({ + _id: '123', + index: expect.objectContaining({ + _index: expect.stringContaining('some-type-index__'), + }), + }), + ); + expect(bulkBody[2]).toEqual( + expect.objectContaining({ + _id: '456', + index: expect.objectContaining({ + _index: expect.stringContaining('some-type-index__'), + }), + }), + ); + }, 40000); + + it('indexes documents without custom batch key field when not specified', async () => { + const documents = [ + { + title: 'testTerm', + text: 'testText', + location: 'test/location', + customId: '123', + }, + ]; + + await TestPipeline.fromIndexer(indexer).withDocuments(documents).execute(); + + const bulkBody = bulkSpy.mock.calls[0][0].body; + + expect(bulkBody[0]).not.toHaveProperty('_id'); + expect(bulkBody[0]).toEqual( + expect.objectContaining({ + index: expect.objectContaining({ + _index: expect.stringContaining('some-type-index__'), + }), + }), + ); + }); }); diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts index 75b880af67..b57524e13a 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts @@ -32,6 +32,7 @@ export type ElasticSearchSearchEngineIndexerOptions = { logger: LoggerService; elasticSearchClientWrapper: ElasticSearchClientWrapper; batchSize: number; + batchKeyField?: string; skipRefresh?: boolean; }; @@ -87,10 +88,13 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { // documents have been successfully written to ES. this.bulkResult = this.elasticSearchClientWrapper.bulk({ datasource: this.sourceStream, - onDocument() { + onDocument(doc) { that.processed++; return { index: { _index: that.indexName }, + ...(options.batchKeyField && doc[options.batchKeyField] + ? { _id: doc[options.batchKeyField] } + : {}), }; }, refreshOnCompletion: options.skipRefresh !== true, diff --git a/plugins/search-backend-module-explore/CHANGELOG.md b/plugins/search-backend-module-explore/CHANGELOG.md index f8fcf763a5..644c75f6f7 100644 --- a/plugins/search-backend-module-explore/CHANGELOG.md +++ b/plugins/search-backend-module-explore/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-search-backend-module-explore +## 0.3.7 + +### Patch Changes + +- 9a93520: Deprecate and mark explore collator as moved +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + ## 0.3.7-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-explore/package.json b/plugins/search-backend-module-explore/package.json index 925b489f41..08fe70b093 100644 --- a/plugins/search-backend-module-explore/package.json +++ b/plugins/search-backend-module-explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-explore", - "version": "0.3.7-next.1", + "version": "0.3.7", "description": "A module for the search backend that exports explore modules", "backstage": { "moved": "@backstage-community/plugin-search-backend-module-explore", diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index 48a7c12180..14afeae46a 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-backend-module-pg +## 0.5.48 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + ## 0.5.48-next.0 ### Patch Changes diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 40a56baab1..4a385fffeb 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-pg", - "version": "0.5.48-next.0", + "version": "0.5.48", "description": "A module for the search backend that implements search using PostgreSQL", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md b/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md index 68c7683dda..b5b6802e05 100644 --- a/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md +++ b/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-backend-module-stack-overflow-collator +## 0.3.13 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + ## 0.3.13-next.0 ### Patch Changes diff --git a/plugins/search-backend-module-stack-overflow-collator/package.json b/plugins/search-backend-module-stack-overflow-collator/package.json index 6cf96d8c18..3885882caa 100644 --- a/plugins/search-backend-module-stack-overflow-collator/package.json +++ b/plugins/search-backend-module-stack-overflow-collator/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-stack-overflow-collator", - "version": "0.3.13-next.0", + "version": "0.3.13", "description": "A module for the search backend that exports stack overflow modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-techdocs/CHANGELOG.md b/plugins/search-backend-module-techdocs/CHANGELOG.md index 1938883b9e..1a952bbb9b 100644 --- a/plugins/search-backend-module-techdocs/CHANGELOG.md +++ b/plugins/search-backend-module-techdocs/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-search-backend-module-techdocs +## 0.4.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + - @backstage/plugin-techdocs-node@1.13.7 + +## 0.4.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.4.6-next.0 ### Patch Changes diff --git a/plugins/search-backend-module-techdocs/package.json b/plugins/search-backend-module-techdocs/package.json index 1faffa2b90..89bb5f964c 100644 --- a/plugins/search-backend-module-techdocs/package.json +++ b/plugins/search-backend-module-techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-techdocs", - "version": "0.4.6-next.0", + "version": "0.4.6", "description": "A module for the search backend that exports techdocs modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index 39b21dd29c..f61d4f4318 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-search-backend-node +## 1.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + ## 1.3.15-next.0 ### Patch Changes diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index bf8375ffe1..e862163a08 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-node", - "version": "1.3.15-next.0", + "version": "1.3.15", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", "backstage": { "role": "node-library", diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index 93b283cc4a..323d97127b 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-search-backend +## 2.0.6 + +### Patch Changes + +- 0efcc97: Updated generated schemas +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + - @backstage/plugin-search-backend-node@1.3.15 + ## 2.0.6-next.0 ### Patch Changes diff --git a/plugins/search-backend/knip-report.md b/plugins/search-backend/knip-report.md index 90e0425294..406f00e7a8 100644 --- a/plugins/search-backend/knip-report.md +++ b/plugins/search-backend/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :-------------------------------- | :----------- | :------- | -| @backstage/plugin-permission-node | package.json | error | -| yn | package.json | error | +| @backstage/plugin-permission-node | plugins/search-backend/package.json | error | +| yn | plugins/search-backend/package.json | error | diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index d353a2c079..1802646e5d 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend", - "version": "2.0.6-next.0", + "version": "2.0.6", "description": "The Backstage backend plugin that provides your backstage app with search", "backstage": { "role": "backend-plugin", diff --git a/plugins/search-backend/src/schema/openapi/generated/apis/Api.server.ts b/plugins/search-backend/src/schema/openapi/generated/apis/Api.server.ts index d23648d8b5..26652de2cd 100644 --- a/plugins/search-backend/src/schema/openapi/generated/apis/Api.server.ts +++ b/plugins/search-backend/src/schema/openapi/generated/apis/Api.server.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/apis/index.ts b/plugins/search-backend/src/schema/openapi/generated/apis/index.ts index a3cdbbebd2..8d81cbaf39 100644 --- a/plugins/search-backend/src/schema/openapi/generated/apis/index.ts +++ b/plugins/search-backend/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/index.ts b/plugins/search-backend/src/schema/openapi/generated/index.ts index 69c39313c6..dec4b8804e 100644 --- a/plugins/search-backend/src/schema/openapi/generated/index.ts +++ b/plugins/search-backend/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/ErrorError.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/ErrorError.model.ts index 0a51c67fef..809cd3fa80 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts index d44dcb66d9..3eb5e15740 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts index 91c120483d..edbcc32df7 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/ModelError.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/ModelError.model.ts index 3e6af947ec..3914d73531 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/ModelError.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/Query200Response.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/Query200Response.model.ts index 8ddb2a0b63..190ab8dc59 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/Query200Response.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/Query200Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInner.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInner.model.ts index ec635c8e5b..7a1c9c155a 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInner.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInnerDocument.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInnerDocument.model.ts index 9e82274aa7..004ce76587 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInnerDocument.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInnerDocument.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/index.ts b/plugins/search-backend/src/schema/openapi/generated/models/index.ts index d38087bf8c..fc9f1408f7 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/index.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/index.ts b/plugins/search-backend/src/schema/openapi/index.ts index db98243cbf..196aad553a 100644 --- a/plugins/search-backend/src/schema/openapi/index.ts +++ b/plugins/search-backend/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md index 1f68efe604..d8133c742e 100644 --- a/plugins/search-react/CHANGELOG.md +++ b/plugins/search-react/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-search-react +## 1.9.4 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + ## 1.9.4-next.0 ### Patch Changes diff --git a/plugins/search-react/knip-report.md b/plugins/search-react/knip-report.md index 7f6a4d4ccd..74ef5ba5c2 100644 --- a/plugins/search-react/knip-report.md +++ b/plugins/search-react/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/frontend-app-api | package.json | error | +| @backstage/frontend-app-api | plugins/search-react/package.json | error | diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 9345d508c1..2fbfa9267f 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-react", - "version": "1.9.4-next.0", + "version": "1.9.4", "backstage": { "role": "web-library", "pluginId": "search", diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 8449247680..5bd43fd7c4 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-search +## 1.4.30 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-search-react@1.9.4 + +## 1.4.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 1.4.30-next.1 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index 7600d61b86..6ecbb32437 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "1.4.30-next.1", + "version": "1.4.30", "description": "The Backstage plugin that provides your backstage app with search", "backstage": { "role": "frontend-plugin", diff --git a/plugins/signals-backend/CHANGELOG.md b/plugins/signals-backend/CHANGELOG.md index ba744b3f13..86eb851f7a 100644 --- a/plugins/signals-backend/CHANGELOG.md +++ b/plugins/signals-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-signals-backend +## 0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-signals-node@0.1.24 + ## 0.3.8-next.0 ### Patch Changes diff --git a/plugins/signals-backend/knip-report.md b/plugins/signals-backend/knip-report.md index 8f8d01a437..2b66a8ba1a 100644 --- a/plugins/signals-backend/knip-report.md +++ b/plugins/signals-backend/knip-report.md @@ -4,14 +4,14 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/plugin-auth-node | package.json | error | -| http-proxy-middleware | package.json | error | -| winston | package.json | error | -| yn | package.json | error | +| @backstage/plugin-auth-node | plugins/signals-backend/package.json | error | +| http-proxy-middleware | plugins/signals-backend/package.json | error | +| winston | plugins/signals-backend/package.json | error | +| yn | plugins/signals-backend/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :-- | :----------- | :------- | -| msw | package.json | error | +| msw | plugins/signals-backend/package.json | error | diff --git a/plugins/signals-backend/package.json b/plugins/signals-backend/package.json index 2055b9f2d6..b5de8611df 100644 --- a/plugins/signals-backend/package.json +++ b/plugins/signals-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals-backend", - "version": "0.3.8-next.0", + "version": "0.3.8", "backstage": { "role": "backend-plugin", "pluginId": "signals", diff --git a/plugins/signals-node/CHANGELOG.md b/plugins/signals-node/CHANGELOG.md index 898182f99c..0bdbb32c87 100644 --- a/plugins/signals-node/CHANGELOG.md +++ b/plugins/signals-node/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-signals-node +## 0.1.24 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.1.24-next.0 ### Patch Changes diff --git a/plugins/signals-node/knip-report.md b/plugins/signals-node/knip-report.md index 34f37bfdb1..42ea194792 100644 --- a/plugins/signals-node/knip-report.md +++ b/plugins/signals-node/knip-report.md @@ -4,15 +4,15 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/plugin-auth-node | package.json | error | -| @backstage/config | package.json | error | -| express | package.json | error | -| uuid | package.json | error | -| ws | package.json | error | +| @backstage/plugin-auth-node | plugins/signals-node/package.json | error | +| @backstage/config | plugins/signals-node/package.json | error | +| express | plugins/signals-node/package.json | error | +| uuid | plugins/signals-node/package.json | error | +| ws | plugins/signals-node/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :------------- | :----------- | :------- | -| @types/express | package.json | error | +| @types/express | plugins/signals-node/package.json | error | diff --git a/plugins/signals-node/package.json b/plugins/signals-node/package.json index 6aabea9423..907d51c588 100644 --- a/plugins/signals-node/package.json +++ b/plugins/signals-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals-node", - "version": "0.1.24-next.0", + "version": "0.1.24", "description": "Node.js library for the signals plugin", "backstage": { "role": "node-library", diff --git a/plugins/signals-react/knip-report.md b/plugins/signals-react/knip-report.md index 0d1d20d45b..e210ba0b51 100644 --- a/plugins/signals-react/knip-report.md +++ b/plugins/signals-react/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :---------------- | :----------- | :------- | -| @material-ui/core | package.json | error | +| @material-ui/core | plugins/signals-react/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :--------------------- | :----------- | :------- | -| @testing-library/react | package.json | error | -| @backstage/test-utils | package.json | error | +| @testing-library/react | plugins/signals-react/package.json | error | +| @backstage/test-utils | plugins/signals-react/package.json | error | diff --git a/plugins/signals/CHANGELOG.md b/plugins/signals/CHANGELOG.md index 9eccc1148f..cb5c18da48 100644 --- a/plugins/signals/CHANGELOG.md +++ b/plugins/signals/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-signals +## 0.0.23 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + ## 0.0.23-next.1 ### Patch Changes diff --git a/plugins/signals/knip-report.md b/plugins/signals/knip-report.md index e9c2c167b1..a27d63358a 100644 --- a/plugins/signals/knip-report.md +++ b/plugins/signals/knip-report.md @@ -4,15 +4,15 @@ | Name | Location | Severity | | :----------------- | :----------- | :------- | -| @material-ui/icons | package.json | error | -| @material-ui/lab | package.json | error | -| react-use | package.json | error | +| @material-ui/icons | plugins/signals/package.json | error | +| @material-ui/lab | plugins/signals/package.json | error | +| react-use | plugins/signals/package.json | error | ## Unused devDependencies (3) | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @testing-library/user-event | package.json | error | -| @backstage/core-app-api | package.json | error | -| msw | package.json | error | +| @testing-library/user-event | plugins/signals/package.json | error | +| @backstage/core-app-api | plugins/signals/package.json | error | +| msw | plugins/signals/package.json | error | diff --git a/plugins/signals/package.json b/plugins/signals/package.json index 23b512c133..c3a3f9ad66 100644 --- a/plugins/signals/package.json +++ b/plugins/signals/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals", - "version": "0.0.23-next.1", + "version": "0.0.23", "backstage": { "role": "frontend-plugin", "pluginId": "signals", diff --git a/plugins/techdocs-addons-test-utils/CHANGELOG.md b/plugins/techdocs-addons-test-utils/CHANGELOG.md index a6b33f9837..11f82bfe63 100644 --- a/plugins/techdocs-addons-test-utils/CHANGELOG.md +++ b/plugins/techdocs-addons-test-utils/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-techdocs-addons-test-utils +## 1.1.0 + +### Minor Changes + +- 72543e9: Adding catalogApiRef to test-utils to support catalog API usage by TechDocs reader page. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.15.0 + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-app-api@1.19.0 + - @backstage/plugin-search-react@1.9.4 + - @backstage/integration-react@1.2.10 + +## 1.0.53-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + ## 1.0.53-next.1 ### Patch Changes diff --git a/plugins/techdocs-addons-test-utils/knip-report.md b/plugins/techdocs-addons-test-utils/knip-report.md index ca9b81ba38..2d84f97e2f 100644 --- a/plugins/techdocs-addons-test-utils/knip-report.md +++ b/plugins/techdocs-addons-test-utils/knip-report.md @@ -4,11 +4,11 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @backstage/plugin-catalog | package.json | error | +| @backstage/plugin-catalog | plugins/techdocs-addons-test-utils/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :------------------- | :----------- | :------- | -| @testing-library/dom | package.json | error | +| @testing-library/dom | plugins/techdocs-addons-test-utils/package.json | error | diff --git a/plugins/techdocs-addons-test-utils/package.json b/plugins/techdocs-addons-test-utils/package.json index eebe45a88a..2de0951a9c 100644 --- a/plugins/techdocs-addons-test-utils/package.json +++ b/plugins/techdocs-addons-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-addons-test-utils", - "version": "1.0.53-next.1", + "version": "1.1.0", "backstage": { "role": "web-library", "pluginId": "techdocs-addons", diff --git a/plugins/techdocs-addons-test-utils/src/test-utils.tsx b/plugins/techdocs-addons-test-utils/src/test-utils.tsx index 91b85e7885..a7cb74cb93 100644 --- a/plugins/techdocs-addons-test-utils/src/test-utils.tsx +++ b/plugins/techdocs-addons-test-utils/src/test-utils.tsx @@ -39,10 +39,12 @@ import { } from '@backstage/plugin-techdocs-react'; import { TechDocsReaderPage, techdocsPlugin } from '@backstage/plugin-techdocs'; import { + catalogApiRef, EntityPresentationApi, entityPresentationApiRef, entityRouteRef, } from '@backstage/plugin-catalog-react'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { searchApiRef } from '@backstage/plugin-search-react'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; @@ -235,8 +237,19 @@ export class TechDocsAddonTester { }), }; + const catalogApi = catalogApiMock({ + entities: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { namespace: 'default', name: 'docs' }, + }, + ], + }); + const apis: TechdocsAddonTesterApis = [ [fetchApiRef, fetchApi], + [catalogApiRef, catalogApi], [entityPresentationApiRef, entityPresentationApi], [discoveryApiRef, discoveryApi], [techdocsApiRef, techdocsApi], diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 036ba4387c..1234dd5b58 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-techdocs-backend +## 2.1.0 + +### Minor Changes + +- 063cdc5: Techdocs: support HumanDuration for cache TTL and timeout configuration +- a0b604c: Adding new entity that specifies an external entity in the techdocs-entity annotation and updates to documentation regarding TechDocs redirects. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-module-techdocs@0.4.6 + - @backstage/plugin-techdocs-node@1.13.7 + +## 2.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.4.6-next.1 + ## 2.1.0-next.0 ### Minor Changes diff --git a/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml b/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml index 6b30213a60..5b90693e23 100644 --- a/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml +++ b/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml @@ -10,3 +10,17 @@ spec: type: service lifecycle: experimental owner: user:guest +--- +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: techdocs-entity-documented-component + title: Example Entity Documented By TechDocs Entity Annotation + description: A Service with TechDocs documentation via the `backstage.io/techdocs-entity` annotation. + annotations: + backstage.io/techdocs-entity: component:default/documented-component + backstage.io/techdocs-entity-path: /inner-component-docs +spec: + type: service + lifecycle: experimental + owner: user:guest diff --git a/plugins/techdocs-backend/examples/documented-component/docs/inner-component-docs/index.md b/plugins/techdocs-backend/examples/documented-component/docs/inner-component-docs/index.md new file mode 100644 index 0000000000..8cb6498c55 --- /dev/null +++ b/plugins/techdocs-backend/examples/documented-component/docs/inner-component-docs/index.md @@ -0,0 +1,5 @@ +# Inner Component Docs + +This is a basic example of documentation within a larger suite of TechDocs that can be referenced by whatever entities necessary. It is intended as a showcase of the `backstage.io/techdocs-entity-path` annotation for linking to a "subpage" in TechDocs that are declared by another entity. + +Please review the [How-To Guides - Deep Linking Into TechDocs](../../../../../../docs/features/techdocs/how-to-guides.md#deep-linking-into-techdocs) section for more information and you can view the example usage on the "Example Entity Documented By TechDocs Entity Annotation" component in this [catalog-info.yaml](../../catalog-info.yaml) file. diff --git a/plugins/techdocs-backend/examples/documented-component/mkdocs.yml b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml index 1a159a4d12..fd954225a3 100644 --- a/plugins/techdocs-backend/examples/documented-component/mkdocs.yml +++ b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml @@ -7,6 +7,7 @@ nav: - Subpage: sub-page.md - 'Code Sample': code/code-sample.md - Extensions: extensions.md + - 'Inner Component Docs': inner-component-docs/index.md plugins: - techdocs-core diff --git a/plugins/techdocs-backend/knip-report.md b/plugins/techdocs-backend/knip-report.md index 764d9383f0..164f2d0595 100644 --- a/plugins/techdocs-backend/knip-report.md +++ b/plugins/techdocs-backend/knip-report.md @@ -4,9 +4,9 @@ | Name | Location | Severity | | :----------------------------------------------- | :----------- | :------- | -| @backstage/plugin-search-backend-module-techdocs | package.json | error | -| @backstage/plugin-permission-common | package.json | error | -| @backstage/plugin-techdocs-common | package.json | error | -| @backstage/plugin-catalog-common | package.json | error | -| lodash | package.json | error | +| @backstage/plugin-search-backend-module-techdocs | plugins/techdocs-backend/package.json | error | +| @backstage/plugin-permission-common | plugins/techdocs-backend/package.json | error | +| @backstage/plugin-techdocs-common | plugins/techdocs-backend/package.json | error | +| @backstage/plugin-catalog-common | plugins/techdocs-backend/package.json | error | +| lodash | plugins/techdocs-backend/package.json | error | diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index bc6bfd6175..c9d0218404 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "2.1.0-next.0", + "version": "2.1.0", "description": "The Backstage backend plugin that renders technical documentation for your components", "backstage": { "role": "backend-plugin", diff --git a/plugins/techdocs-module-addons-contrib/CHANGELOG.md b/plugins/techdocs-module-addons-contrib/CHANGELOG.md index 91ae7badb3..d3842b48d6 100644 --- a/plugins/techdocs-module-addons-contrib/CHANGELOG.md +++ b/plugins/techdocs-module-addons-contrib/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-techdocs-module-addons-contrib +## 1.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/integration@1.18.0 + - @backstage/core-components@0.18.0 + - @backstage/integration-react@1.2.10 + ## 1.1.28-next.0 ### Patch Changes diff --git a/plugins/techdocs-module-addons-contrib/package.json b/plugins/techdocs-module-addons-contrib/package.json index 308625938f..12522a0949 100644 --- a/plugins/techdocs-module-addons-contrib/package.json +++ b/plugins/techdocs-module-addons-contrib/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-module-addons-contrib", - "version": "1.1.28-next.0", + "version": "1.1.28", "description": "Plugin module for contributed TechDocs Addons", "backstage": { "role": "frontend-plugin-module", diff --git a/plugins/techdocs-node/CHANGELOG.md b/plugins/techdocs-node/CHANGELOG.md index efcbc4e394..6cc19abea5 100644 --- a/plugins/techdocs-node/CHANGELOG.md +++ b/plugins/techdocs-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-techdocs-node +## 1.13.7 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + ## 1.13.7-next.0 ### Patch Changes diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index c3081d2426..d7ecd3b661 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-node", - "version": "1.13.7-next.0", + "version": "1.13.7", "description": "Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", "backstage": { "role": "node-library", diff --git a/plugins/techdocs-react/CHANGELOG.md b/plugins/techdocs-react/CHANGELOG.md index f594c37f87..d03925b40d 100644 --- a/plugins/techdocs-react/CHANGELOG.md +++ b/plugins/techdocs-react/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-techdocs-react +## 1.3.3 + +### Patch Changes + +- a0b604c: Update to documentation regarding TechDocs redirects. +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + ## 1.3.3-next.0 ### Patch Changes diff --git a/plugins/techdocs-react/knip-report.md b/plugins/techdocs-react/knip-report.md index 651079cda4..3e7476ef55 100644 --- a/plugins/techdocs-react/knip-report.md +++ b/plugins/techdocs-react/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------------------------- | :----------- | :------- | -| @backstage/core-components | package.json | error | -| react-helmet | package.json | error | +| @backstage/core-components | plugins/techdocs-react/package.json | error | +| react-helmet | plugins/techdocs-react/package.json | error | diff --git a/plugins/techdocs-react/package.json b/plugins/techdocs-react/package.json index 0d9082b4d1..13e9dfc66c 100644 --- a/plugins/techdocs-react/package.json +++ b/plugins/techdocs-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-react", - "version": "1.3.3-next.0", + "version": "1.3.3", "description": "Shared frontend utilities for TechDocs and Addons", "backstage": { "role": "web-library", diff --git a/plugins/techdocs-react/src/helpers.ts b/plugins/techdocs-react/src/helpers.ts index e6daf99b26..aebb0b3e52 100644 --- a/plugins/techdocs-react/src/helpers.ts +++ b/plugins/techdocs-react/src/helpers.ts @@ -64,7 +64,7 @@ export function getEntityRootTechDocsPath(entity: Entity): string { /** * Build the TechDocs URL for the given entity. This helper should be used anywhere there - * is a link to an entities TechDocs. + * is a link to an entity's TechDocs. * * @public */ @@ -104,7 +104,7 @@ export const buildTechDocsURL = ( }); // Add on the external entity path to the url if one exists. This allows deep linking into another - // entities TechDocs. + // entity's TechDocs. const path = getEntityRootTechDocsPath(entity); return `${url}${path}`; diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index c4f6ed3cef..7bdefe0293 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-techdocs +## 1.15.0 + +### Minor Changes + +- a0b604c: Adding redirect handling for TechDocs URLs that reference entities that now reference an external entity for TechDocs. Including tests and documentation. + +### Patch Changes + +- 313cec7: Updated dependency `dompurify` to `^3.2.4`. +- 8d18d23: TechDocs page titles have been improved, especially for deeply nested pages. +- 1dfee19: Reverts a change in CSS layout that shifted the content of the Techdocs too far to the left. +- 4ce5831: Support Techdocs redirect with dompurify 3.2.6+ +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-auth-react@0.1.19 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/integration@1.18.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-search-react@1.9.4 + - @backstage/integration-react@1.2.10 + +## 1.14.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + ## 1.14.2-next.1 ### Patch Changes diff --git a/plugins/techdocs/knip-report.md b/plugins/techdocs/knip-report.md index 2b0b502c71..45a1d2af25 100644 --- a/plugins/techdocs/knip-report.md +++ b/plugins/techdocs/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-- | :----------- | :------- | -| jss | package.json | error | +| jss | plugins/techdocs/package.json | error | diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 7ecec3cd0a..7f03dd32f5 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "1.14.2-next.1", + "version": "1.15.0", "description": "The Backstage plugin that renders technical documentation for your components", "backstage": { "role": "frontend-plugin", @@ -81,7 +81,7 @@ "@material-ui/lab": "4.0.0-alpha.61", "@material-ui/styles": "^4.10.0", "@microsoft/fetch-event-source": "^2.0.1", - "dompurify": "^3.0.0", + "dompurify": "^3.2.4", "git-url-parse": "^15.0.0", "jss": "~10.10.0", "lodash": "^4.17.21", diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx index 2af4915ef5..aea45a60fa 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx @@ -18,6 +18,7 @@ import { ReactNode } from 'react'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { + catalogApiRef, entityPresentationApiRef, entityRouteRef, } from '@backstage/plugin-catalog-react'; @@ -26,13 +27,15 @@ import { renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; +import { catalogApiMock as catalogApiMockFactory } from '@backstage/plugin-catalog-react/testUtils'; import { techdocsApiRef, techdocsStorageApiRef } from '../../../api'; import { rootRouteRef, rootDocsRouteRef } from '../../../routes'; +import { TECHDOCS_EXTERNAL_ANNOTATION } from '@backstage/plugin-techdocs-common'; import { TechDocsReaderPage } from './TechDocsReaderPage'; -import { Route, useParams } from 'react-router-dom'; +import { Route, useNavigate, useParams } from 'react-router-dom'; import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; import { FlatRoutes } from '@backstage/core-app-api'; @@ -94,6 +97,8 @@ const entityPresentationApiMock: jest.Mocked< }), }; +const catalogApiMock = catalogApiMockFactory.mock(); + const fetchApiMock = { fetch: jest.fn().mockResolvedValue({ ok: true, @@ -114,6 +119,11 @@ jest.mock('@backstage/core-components', () => ({ Page: jest.fn(), })); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: jest.fn(), +})); + const configApi = mockApis.config({ data: { app: { baseUrl: 'http://localhost:3000' } }, }); @@ -129,6 +139,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => { [techdocsApiRef, techdocsApiMock], [techdocsStorageApiRef, techdocsStorageApiMock], [entityPresentationApiRef, entityPresentationApiMock], + [catalogApiRef, catalogApiMock], ]} > {children} @@ -143,6 +154,8 @@ const mountedRoutes = { }; describe('', () => { + const mockNavigate = jest.fn(); + beforeEach(() => { getEntityMetadata.mockResolvedValue(mockEntityMetadata); getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata); @@ -150,6 +163,8 @@ describe('', () => { // Expires in 10 minutes expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString(), }); + + (useNavigate as jest.Mock).mockReturnValue(mockNavigate); }); afterEach(() => { @@ -285,4 +300,142 @@ describe('', () => { expect(text).toHaveStyle('fontFamily: Comic Sans MS'); }); + + describe('external TechDocs redirect', () => { + beforeEach(() => { + mockNavigate.mockClear(); + catalogApiMock.getEntityByRef.mockReset(); + catalogApiMock.getEntityByRef.mockResolvedValue(mockEntityMetadata); + }); + + it('should navigate to external URL when entity has external techdocs annotation', async () => { + const mockEntityWithExternalAnnotation = { + ...mockEntityMetadata, + metadata: { + ...mockEntityMetadata.metadata, + annotations: { + [TECHDOCS_EXTERNAL_ANNOTATION]: + 'component:external-namespace/external-docs', + }, + }, + }; + + catalogApiMock.getEntityByRef.mockResolvedValue( + mockEntityWithExternalAnnotation, + ); + + await renderInTestApp( + + + , + { + mountedRoutes, + }, + ); + + expect(mockNavigate).toHaveBeenCalledWith( + '/docs/external-namespace/component/external-docs', + { replace: true }, + ); + }); + + it('should render normally when entity has no external techdocs annotation', async () => { + const mockEntityWithoutExternalAnnotation = { + ...mockEntityMetadata, + metadata: { + ...mockEntityMetadata.metadata, + annotations: undefined, + }, + }; + + catalogApiMock.getEntityByRef.mockResolvedValue( + mockEntityWithoutExternalAnnotation, + ); + + const rendered = await renderInTestApp( + + + , + { + mountedRoutes, + }, + ); + + expect(rendered.container.querySelector('header')).toBeInTheDocument(); + expect(rendered.container.querySelector('article')).toBeInTheDocument(); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + + it('should render normally when entity has external annotation but no value', async () => { + const mockEntityWithEmptyExternalAnnotation = { + ...mockEntityMetadata, + metadata: { + ...mockEntityMetadata.metadata, + annotations: { + [TECHDOCS_EXTERNAL_ANNOTATION]: '', + }, + }, + }; + + catalogApiMock.getEntityByRef.mockResolvedValue( + mockEntityWithEmptyExternalAnnotation, + ); + + const rendered = await renderInTestApp( + + + , + { + mountedRoutes, + }, + ); + + expect(rendered.container.querySelector('header')).toBeInTheDocument(); + expect(rendered.container.querySelector('article')).toBeInTheDocument(); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + + it('should render normally when catalog API throws an error', async () => { + catalogApiMock.getEntityByRef.mockRejectedValue( + new Error('Catalog API error'), + ); + + const rendered = await renderInTestApp( + + + , + { + mountedRoutes, + }, + ); + + expect(rendered.container.querySelector('header')).toBeInTheDocument(); + expect(rendered.container.querySelector('article')).toBeInTheDocument(); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx index 80545151d9..c67660fd79 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx @@ -14,19 +14,26 @@ * limitations under the License. */ -import { Children, ReactElement, ReactNode } from 'react'; -import { useOutlet } from 'react-router-dom'; - +import { + Children, + ReactElement, + ReactNode, + useEffect, + useMemo, + useCallback, +} from 'react'; +import { useOutlet, useNavigate } from 'react-router-dom'; import { Page } from '@backstage/core-components'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { TECHDOCS_ADDONS_KEY, TECHDOCS_ADDONS_WRAPPER_KEY, TechDocsReaderPageProvider, + buildTechDocsURL, } from '@backstage/plugin-techdocs-react'; - +import { TECHDOCS_EXTERNAL_ANNOTATION } from '@backstage/plugin-techdocs-common'; +import useAsync from 'react-use/esm/useAsync'; import { TechDocsReaderPageRenderFunction } from '../../../types'; - import { TechDocsReaderPageContent } from '../TechDocsReaderPageContent'; import { TechDocsReaderPageHeader } from '../TechDocsReaderPageHeader'; import { TechDocsReaderPageSubheader } from '../TechDocsReaderPageSubheader'; @@ -34,9 +41,11 @@ import { rootDocsRouteRef } from '../../../routes'; import { getComponentData, useRouteRefParams, + useApi, + useRouteRef, } from '@backstage/core-plugin-api'; - import { CookieAuthRefreshProvider } from '@backstage/plugin-auth-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { createTheme, styled, @@ -44,6 +53,7 @@ import { ThemeProvider, useTheme, } from '@material-ui/core/styles'; +import { Progress } from '@backstage/core-components'; /* An explanation for the multiple ways of customizing the TechDocs reader page @@ -177,44 +187,106 @@ const StyledPage = styled(Page)({ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => { const currentTheme = useTheme(); - const readerPageTheme = createTheme({ - ...currentTheme, - ...(props.overrideThemeOptions || {}), - }); + const readerPageTheme = useMemo( + () => + createTheme({ + ...currentTheme, + ...(props.overrideThemeOptions || {}), + }), + [currentTheme, props.overrideThemeOptions], + ); + const { kind, name, namespace } = useRouteRefParams(rootDocsRouteRef); const { children, entityRef = { kind, name, namespace } } = props; const outlet = useOutlet(); - if (!children) { + const catalogApi = useApi(catalogApiRef); + const navigate = useNavigate(); + const viewTechdocLink = useRouteRef(rootDocsRouteRef); + + const memoizedEntityRef = useMemo( + () => ({ + kind: entityRef.kind, + name: entityRef.name, + namespace: entityRef.namespace, + }), + [entityRef.kind, entityRef.name, entityRef.namespace], + ); + + const externalEntityTechDocsUrl = useAsync(async () => { + try { + const catalogEntity = await catalogApi.getEntityByRef(memoizedEntityRef); + + if ( + catalogEntity?.metadata?.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION] + ) { + return buildTechDocsURL(catalogEntity, viewTechdocLink); + } + } catch (error) { + // Ignore error and allow an attempt at loading the current entity's TechDocs when unable to fetch an external entity from the catalog. + } + + return undefined; + }, [memoizedEntityRef, catalogApi, viewTechdocLink]); + + const handleNavigation = useCallback( + (url: string) => { + navigate(url, { replace: true }); + }, + [navigate], + ); + + useEffect(() => { + if (!externalEntityTechDocsUrl.loading && externalEntityTechDocsUrl.value) { + handleNavigation(externalEntityTechDocsUrl.value); + } + }, [ + externalEntityTechDocsUrl.loading, + externalEntityTechDocsUrl.value, + handleNavigation, + ]); + + const page: ReactNode = useMemo(() => { + if (children) { + return null; + } + const childrenList = outlet ? Children.toArray(outlet.props.children) : []; const grandChildren = childrenList.flatMap( child => (child as ReactElement)?.props?.children ?? [], ); - const page: ReactNode = grandChildren.find( + return grandChildren.find( grandChild => !getComponentData(grandChild, TECHDOCS_ADDONS_WRAPPER_KEY) && !getComponentData(grandChild, TECHDOCS_ADDONS_KEY), ); + }, [children, outlet]); - // As explained above, "page" is configuration 4 and is 1 + if (externalEntityTechDocsUrl.loading || externalEntityTechDocsUrl.value) { + return ; + } + + // As explained above, "page" is configuration 4 and is 1 + if (!children) { return ( - + {(page as JSX.Element) || } ); } + // As explained above, a render function is configuration 3 and React element is 2 return ( - + {({ metadata, entityMetadata, onReady }) => ( { > {children instanceof Function ? children({ - entityRef, + entityRef: memoizedEntityRef, techdocsMetadataValue: metadata.value, entityMetadataValue: entityMetadata.value, onReady, diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx index 6f6ee81c8b..3c10b26f7f 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx @@ -52,13 +52,11 @@ const mockTechDocsMetadata = { site_description: 'test-site-desc', }; -const mockUseParams = jest.fn(); -mockUseParams.mockReturnValue({ '*': 'foo/bar/baz/' }); - +let useParamsPath = '/'; jest.mock('react-router-dom', () => { return { ...(jest.requireActual('react-router-dom') as any), - useParams: () => mockUseParams(), + useParams: () => ({ '*': useParamsPath }), }; }); @@ -189,6 +187,7 @@ describe('', () => { getEntityMetadata.mockResolvedValue(mockEntityMetadata); getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata); + useParamsPath = 'foo/bar/baz/'; await renderInTestApp( @@ -203,7 +202,31 @@ describe('', () => { await waitFor(() => { expect(document.title).toEqual( - 'Backstage | Test Entity | Foo | Bar | Baz', + 'Test Entity | Foo | Bar | Baz | Backstage', + ); + }); + }); + + it('The header title is abbreviated if path is too long', async () => { + getEntityMetadata.mockResolvedValue(mockEntityMetadata); + getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata); + + useParamsPath = 'foo/bar/baz/qux/quux/'; + await renderInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name/*': entityRouteRef, + '/docs': rootRouteRef, + }, + }, + ); + + await waitFor(() => { + expect(document.title).toEqual( + 'Test Entity | Foo | Bar | Baz | Qux | Quux | Backstage', ); }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx index bafcdd270c..6328becc73 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx @@ -172,17 +172,16 @@ export const TechDocsReaderPageHeader = ( const removeTrailingSlash = (str: string) => str.replace(/\/$/, ''); const normalizeAndSpace = (str: string) => - str.replace(/-/g, ' ').split(' ').map(capitalize).join(' '); + str.replace(/[-_]/g, ' ').split(' ').map(capitalize).join(' '); let techdocsTabTitleItems: string[] = []; if (path !== '') techdocsTabTitleItems = removeTrailingSlash(path) .split('/') - .slice(0, 3) .map(normalizeAndSpace); - const tabTitleItems = [appTitle, entityDisplayName, ...techdocsTabTitleItems]; + const tabTitleItems = [entityDisplayName, ...techdocsTabTitleItems, appTitle]; const tabTitle = tabTitleItems.join(' | '); return ( diff --git a/plugins/techdocs/src/reader/transformers/html/hooks/attributes.ts b/plugins/techdocs/src/reader/transformers/html/hooks/attributes.ts new file mode 100644 index 0000000000..ae0b4e2ce9 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/html/hooks/attributes.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { UponSanitizeAttributeHook } from 'dompurify'; + +/** + * Removes attributes that should only be present on meta tags from other elements. + * This ensures that http-equiv and content attributes are only allowed on meta tags + * where they are required for the redirect feature. + */ +export const removeRestrictedAttributes: UponSanitizeAttributeHook = ( + node, + data, +) => { + if (node.tagName !== 'META') { + if (data.attrName === 'http-equiv' || data.attrName === 'content') { + node.removeAttribute(data.attrName); + } + } +}; diff --git a/plugins/techdocs/src/reader/transformers/html/hooks/iframes.ts b/plugins/techdocs/src/reader/transformers/html/hooks/iframes.ts index 25259dbc43..23711b52b7 100644 --- a/plugins/techdocs/src/reader/transformers/html/hooks/iframes.ts +++ b/plugins/techdocs/src/reader/transformers/html/hooks/iframes.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { isElement } from '../utils'; + /** * Checks whether a node is iframe or not. * @param node - can be any element. @@ -42,9 +44,10 @@ const isSafe = (node: Element, hosts: string[]) => { * @param node - can be any element. * @param hosts - list of allowed hosts. */ -export const removeUnsafeIframes = (hosts: string[]) => (node: Element) => { +export const removeUnsafeIframes = (hosts: string[]) => (node: Node) => { + if (!isElement(node)) return; + if (isIframe(node) && !isSafe(node, hosts)) { node.remove(); } - return node; }; diff --git a/plugins/techdocs/src/reader/transformers/html/hooks/index.ts b/plugins/techdocs/src/reader/transformers/html/hooks/index.ts index a4356db2e9..ce237ec6d2 100644 --- a/plugins/techdocs/src/reader/transformers/html/hooks/index.ts +++ b/plugins/techdocs/src/reader/transformers/html/hooks/index.ts @@ -16,3 +16,5 @@ export { removeUnsafeLinks } from './links'; export { removeUnsafeIframes } from './iframes'; +export { removeUnsafeMetaTags } from './metatags'; +export { removeRestrictedAttributes } from './attributes'; diff --git a/plugins/techdocs/src/reader/transformers/html/hooks/links.ts b/plugins/techdocs/src/reader/transformers/html/hooks/links.ts index 38f775cfbe..eb2cbfc6de 100644 --- a/plugins/techdocs/src/reader/transformers/html/hooks/links.ts +++ b/plugins/techdocs/src/reader/transformers/html/hooks/links.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { isElement } from '../utils'; + const MKDOCS_CSS = /main\.[A-Fa-f0-9]{8}\.min\.css$/; const GOOGLE_FONTS = /^https:\/\/fonts\.googleapis\.com/; const GSTATIC_FONTS = /^https:\/\/fonts\.gstatic\.com/; @@ -41,11 +43,11 @@ const isSafe = (node: Element) => { /** * Function that removes unsafe link nodes. * @param node - can be any element. - * @param hosts - list of allowed hosts. */ -export const removeUnsafeLinks = (node: Element) => { +export const removeUnsafeLinks = (node: Node) => { + if (!isElement(node)) return; + if (isLink(node) && !isSafe(node)) { node.remove(); } - return node; }; diff --git a/plugins/techdocs/src/reader/transformers/html/hooks/metatags.ts b/plugins/techdocs/src/reader/transformers/html/hooks/metatags.ts new file mode 100644 index 0000000000..a207a5bca2 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/html/hooks/metatags.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { UponSanitizeElementHook } from 'dompurify'; +import { isElement } from '../utils'; + +/** + * Checks if a meta tag is a refresh redirect tag that should be allowed. + * These tags are required for the TechDocs redirect feature. + */ +const isAllowedMetaRefreshTag = (element: Element): boolean => { + const httpEquiv = element.getAttribute('http-equiv'); + const content = element.getAttribute('content'); + + return httpEquiv === 'refresh' && content?.includes('url=') === true; +}; + +/** + * Removes unsafe meta tags from the DOM while preserving allowed refresh redirect tags. + * Only meta tags used for page refreshing/redirects are allowed as they are required + * for the TechDocs redirect feature. + */ +export const removeUnsafeMetaTags: UponSanitizeElementHook = (node, data) => { + if (!isElement(node)) return; + + if (data.tagName === 'meta' && !isAllowedMetaRefreshTag(node)) { + node.parentNode?.removeChild(node); + } +}; diff --git a/plugins/techdocs/src/reader/transformers/html/transformer.sanitizer.test.tsx b/plugins/techdocs/src/reader/transformers/html/transformer.sanitizer.test.tsx index 39d3d229aa..ed02f86060 100644 --- a/plugins/techdocs/src/reader/transformers/html/transformer.sanitizer.test.tsx +++ b/plugins/techdocs/src/reader/transformers/html/transformer.sanitizer.test.tsx @@ -143,4 +143,35 @@ describe('Transformers > Html > Sanitizer Custom Elements', () => { expect(elements).toHaveLength(1); expect(elements[0].hasAttribute('dominant-baseline')).toBe(true); }); + + it('removes javascript: hrefs while preserving link text', async () => { + const { result } = renderHook(() => useSanitizerTransformer(), { wrapper }); + const dirtyDom = document.createElement('html'); + const dirtyHTML = ` + + + JS 1 + JS 2 (leading space) + JS 3 (whitespace) + + JS 4 (mixed case) + JS 5 (entity-encoded colon) + `; + dirtyDom.innerHTML = dirtyHTML; + const clearDom = await result.current(dirtyDom); // calling html transformer + const elements = Array.from( + clearDom.querySelectorAll('body > a'), + ); + expect(elements).toHaveLength(5); + for (const el of elements) { + // DOMPurify strips the dangerous href attribute + expect(el.getAttribute('href')).toBeNull(); + } + // link text remains + expect(clearDom.textContent).toContain('JS 1'); + expect(clearDom.textContent).toContain('JS 2 (leading space)'); + expect(clearDom.textContent).toContain('JS 3 (whitespace)'); + expect(clearDom.textContent).toContain('JS 4 (mixed case)'); + expect(clearDom.textContent).toContain('JS 5 (entity-encoded colon)'); + }); }); diff --git a/plugins/techdocs/src/reader/transformers/html/transformer.ts b/plugins/techdocs/src/reader/transformers/html/transformer.ts index 7c341c783e..6ebf53f26e 100644 --- a/plugins/techdocs/src/reader/transformers/html/transformer.ts +++ b/plugins/techdocs/src/reader/transformers/html/transformer.ts @@ -20,7 +20,12 @@ import { useCallback, useMemo } from 'react'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { Transformer } from '../transformer'; -import { removeUnsafeIframes, removeUnsafeLinks } from './hooks'; +import { + removeRestrictedAttributes, + removeUnsafeIframes, + removeUnsafeLinks, + removeUnsafeMetaTags, +} from './hooks'; /** * Returns html sanitizer configuration @@ -51,26 +56,9 @@ export const useSanitizerTransformer = (): Transformer => { DOMPurify.addHook('beforeSanitizeElements', removeUnsafeIframes(hosts)); } - // Only allow meta tags if they are used for refreshing the page. They are required for the redirect feature. - DOMPurify.addHook('uponSanitizeElement', (currNode, data) => { - if (data.tagName === 'meta') { - const isMetaRefreshTag = - currNode.getAttribute('http-equiv') === 'refresh' && - currNode.getAttribute('content')?.includes('url='); - if (!isMetaRefreshTag) { - currNode.parentNode?.removeChild(currNode); - } - } - }); + DOMPurify.addHook('uponSanitizeElement', removeUnsafeMetaTags); - // Only allow http-equiv and content attributes on meta tags. They are required for the redirect feature. - DOMPurify.addHook('uponSanitizeAttribute', (currNode, data) => { - if (currNode.tagName !== 'meta') { - if (data.attrName === 'http-equiv' || data.attrName === 'content') { - currNode.removeAttribute(data.attrName); - } - } - }); + DOMPurify.addHook('uponSanitizeAttribute', removeRestrictedAttributes); const tagNameCheck = config?.getOptionalString( 'allowedCustomElementTagNameRegExp', @@ -122,7 +110,7 @@ export const useSanitizerTransformer = (): Transformer => { ? new RegExp(attributeNameCheck) : undefined, }, - }); + }) as Element; }, [config], ); diff --git a/plugins/techdocs/src/reader/transformers/html/utils.ts b/plugins/techdocs/src/reader/transformers/html/utils.ts new file mode 100644 index 0000000000..b97b63743c --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/html/utils.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 isElement = (node: Node): node is Element => { + return node.nodeType === Node.ELEMENT_NODE; +}; diff --git a/plugins/user-settings-backend/CHANGELOG.md b/plugins/user-settings-backend/CHANGELOG.md index 6b69021f4c..20a635d42e 100644 --- a/plugins/user-settings-backend/CHANGELOG.md +++ b/plugins/user-settings-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-user-settings-backend +## 0.3.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-signals-node@0.1.24 + ## 0.3.6-next.0 ### Patch Changes diff --git a/plugins/user-settings-backend/knip-report.md b/plugins/user-settings-backend/knip-report.md index 89988530e6..d5bbb23d04 100644 --- a/plugins/user-settings-backend/knip-report.md +++ b/plugins/user-settings-backend/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/plugin-auth-node | package.json | error | +| @backstage/plugin-auth-node | plugins/user-settings-backend/package.json | error | diff --git a/plugins/user-settings-backend/package.json b/plugins/user-settings-backend/package.json index 16ccb92984..f1f8a3ba68 100644 --- a/plugins/user-settings-backend/package.json +++ b/plugins/user-settings-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings-backend", - "version": "0.3.6-next.0", + "version": "0.3.6", "description": "The Backstage backend plugin to manage user settings", "backstage": { "role": "backend-plugin", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index 5d4f80079f..fba325d67f 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-user-settings +## 0.8.26 + +### Patch Changes + +- 320a9ac: Add the OpenShift authenticator provider to the default `user-settings` providers page. +- b713b54: Tool-tip text correction for the Theme selection in settings page +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/core-app-api@1.19.0 + +## 0.8.26-next.2 + +### Patch Changes + +- b713b54: Tool-tip text correction for the Theme selection in settings page +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.8.26-next.1 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index d5224ac8b6..1ec4ac7072 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings", - "version": "0.8.26-next.1", + "version": "0.8.26", "description": "A Backstage plugin that provides a settings page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/user-settings/report-alpha.api.md b/plugins/user-settings/report-alpha.api.md index 5a28aacc02..1c8e844d05 100644 --- a/plugins/user-settings/report-alpha.api.md +++ b/plugins/user-settings/report-alpha.api.md @@ -123,7 +123,7 @@ export const userSettingsTranslationRef: TranslationRef< readonly 'languageToggle.select': 'Select language {{language}}'; readonly 'languageToggle.title': 'Language'; readonly 'languageToggle.description': 'Change the language'; - readonly 'themeToggle.select': 'Select theme {{theme}}'; + readonly 'themeToggle.select': 'Select {{theme}}'; readonly 'themeToggle.title': 'Theme'; readonly 'themeToggle.description': 'Change the theme mode'; readonly 'themeToggle.names.auto': 'Auto'; diff --git a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx index b0ddbf31cf..ce9c1093ca 100644 --- a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx +++ b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx @@ -26,6 +26,7 @@ import { bitbucketServerAuthApiRef, atlassianAuthApiRef, oneloginAuthApiRef, + openshiftAuthApiRef, } from '@backstage/core-plugin-api'; import { userSettingsTranslationRef } from '../../translation'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; @@ -128,6 +129,14 @@ export const DefaultProviderSettings = (props: { icon={Star} /> )} + {configuredProviders.includes('openshift') && ( + + )} ); }; diff --git a/plugins/user-settings/src/translation.ts b/plugins/user-settings/src/translation.ts index 0161508d14..7445a415f2 100644 --- a/plugins/user-settings/src/translation.ts +++ b/plugins/user-settings/src/translation.ts @@ -28,7 +28,7 @@ export const userSettingsTranslationRef = createTranslationRef({ themeToggle: { title: 'Theme', description: 'Change the theme mode', - select: 'Select theme {{theme}}', + select: 'Select {{theme}}', selectAuto: 'Select Auto Theme', names: { light: 'Light', diff --git a/yarn.lock b/yarn.lock index 24708a5699..f5856a65ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -170,16 +170,6 @@ __metadata: languageName: node linkType: hard -"@ampproject/remapping@npm:^2.2.0": - version: 2.2.1 - resolution: "@ampproject/remapping@npm:2.2.1" - dependencies: - "@jridgewell/gen-mapping": "npm:^0.3.0" - "@jridgewell/trace-mapping": "npm:^0.3.9" - checksum: 10/e15fecbf3b54c988c8b4fdea8ef514ab482537e8a080b2978cc4b47ccca7140577ca7b65ad3322dcce65bc73ee6e5b90cbfe0bbd8c766dad04d5c62ec9634c42 - languageName: node - linkType: hard - "@apidevtools/json-schema-ref-parser@npm:9.0.6": version: 9.0.6 resolution: "@apidevtools/json-schema-ref-parser@npm:9.0.6" @@ -1914,39 +1904,39 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.24.7": - version: 7.28.0 - resolution: "@babel/core@npm:7.28.0" +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.24.7, @babel/core@npm:^7.28.0": + version: 7.28.4 + resolution: "@babel/core@npm:7.28.4" dependencies: - "@ampproject/remapping": "npm:^2.2.0" "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.28.0" + "@babel/generator": "npm:^7.28.3" "@babel/helper-compilation-targets": "npm:^7.27.2" - "@babel/helper-module-transforms": "npm:^7.27.3" - "@babel/helpers": "npm:^7.27.6" - "@babel/parser": "npm:^7.28.0" + "@babel/helper-module-transforms": "npm:^7.28.3" + "@babel/helpers": "npm:^7.28.4" + "@babel/parser": "npm:^7.28.4" "@babel/template": "npm:^7.27.2" - "@babel/traverse": "npm:^7.28.0" - "@babel/types": "npm:^7.28.0" + "@babel/traverse": "npm:^7.28.4" + "@babel/types": "npm:^7.28.4" + "@jridgewell/remapping": "npm:^2.3.5" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10/1c86eec8d76053f7b1c5f65296d51d7b8ac00f80d169ff76d3cd2e7d85ab222eb100d40cc3314f41b96c8cc06e9abab21c63d246161f0f3f70ef14c958419c33 + checksum: 10/0593295241fac9be567145ef16f3858d34fc91390a9438c6d47476be9823af4cc0488c851c59702dd46b968e9fd46d17ddf0105ea30195ca85f5a66b4044c519 languageName: node linkType: hard -"@babel/generator@npm:^7.28.0, @babel/generator@npm:^7.7.2": - version: 7.28.0 - resolution: "@babel/generator@npm:7.28.0" +"@babel/generator@npm:^7.28.3, @babel/generator@npm:^7.7.2": + version: 7.28.3 + resolution: "@babel/generator@npm:7.28.3" dependencies: - "@babel/parser": "npm:^7.28.0" - "@babel/types": "npm:^7.28.0" + "@babel/parser": "npm:^7.28.3" + "@babel/types": "npm:^7.28.2" "@jridgewell/gen-mapping": "npm:^0.3.12" "@jridgewell/trace-mapping": "npm:^0.3.28" jsesc: "npm:^3.0.2" - checksum: 10/064c5ba4c07ecd7600377bd0022d5f6bdb3b35e9ff78d9378f6bd1e656467ca902c091647222ab2f0d2967f6d6c0ca33157d37dd9b1c51926c9b0e1527ab9b92 + checksum: 10/d00d1e6b51059e47594aab7920b88ec6fcef6489954a9172235ab57ad2e91b39c95376963a6e2e4cc7e8b88fa4f931018f71f9ab32bbc9c0bc0de35a0231f26c languageName: node linkType: hard @@ -2016,16 +2006,16 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.24.8, @babel/helper-module-transforms@npm:^7.27.3": - version: 7.27.3 - resolution: "@babel/helper-module-transforms@npm:7.27.3" +"@babel/helper-module-transforms@npm:^7.24.8, @babel/helper-module-transforms@npm:^7.28.3": + version: 7.28.3 + resolution: "@babel/helper-module-transforms@npm:7.28.3" dependencies: "@babel/helper-module-imports": "npm:^7.27.1" "@babel/helper-validator-identifier": "npm:^7.27.1" - "@babel/traverse": "npm:^7.27.3" + "@babel/traverse": "npm:^7.28.3" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/47abc90ceb181b4bdea9bf1717adf536d1b5e5acb6f6d8a7a4524080318b5ca8a99e6d58677268c596bad71077d1d98834d2c3815f2443e6d3f287962300f15d + checksum: 10/598fdd8aa5b91f08542d0ba62a737847d0e752c8b95ae2566bc9d11d371856d6867d93e50db870fb836a6c44cfe481c189d8a2b35ca025a224f070624be9fa87 languageName: node linkType: hard @@ -2099,13 +2089,13 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.27.6": - version: 7.28.2 - resolution: "@babel/helpers@npm:7.28.2" +"@babel/helpers@npm:^7.28.4": + version: 7.28.4 + resolution: "@babel/helpers@npm:7.28.4" dependencies: "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.28.2" - checksum: 10/09fd7965e83d4777a4331a082677a1a2261cec451bf3307cb0fb62b2d32c83d55fb1cac494a5dab5c6ad9da459883b8d4e49142812b10ef3e36b54022b2de3a4 + "@babel/types": "npm:^7.28.4" + checksum: 10/5a70a82e196cf8808f8a449cc4780c34d02edda2bb136d39ce9d26e63b615f18e89a95472230c3ce7695db0d33e7026efeee56f6454ed43480f223007ed205eb languageName: node linkType: hard @@ -2121,14 +2111,14 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.24.7, @babel/parser@npm:^7.26.7, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.5, @babel/parser@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/parser@npm:7.28.0" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.24.7, @babel/parser@npm:^7.26.7, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.5, @babel/parser@npm:^7.28.3, @babel/parser@npm:^7.28.4": + version: 7.28.4 + resolution: "@babel/parser@npm:7.28.4" dependencies: - "@babel/types": "npm:^7.28.0" + "@babel/types": "npm:^7.28.4" bin: parser: ./bin/babel-parser.js - checksum: 10/2c14a0d2600bae9ab81924df0a85bbd34e427caa099c260743f7c6c12b2042e743e776043a0d1a2573229ae648f7e66a80cfb26fc27e2a9eb59b55932d44c817 + checksum: 10/f54c46213ef180b149f6a17ea765bf40acc1aebe2009f594e2a283aec69a190c6dda1fdf24c61a258dbeb903abb8ffb7a28f1a378f8ab5d333846ce7b7e23bf1 languageName: node linkType: hard @@ -2439,7 +2429,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.28.2 resolution: "@babel/runtime@npm:7.28.2" checksum: 10/a0965fbdd6aaa40709290923bbe05e1c4314021f0cef608eb1d69f04f717c41829e50a53d79c4a0f461512b4be9b3c0190dc19387b219bcdaacdd793b2fe1b8a @@ -2457,28 +2447,28 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.24.7, @babel/traverse@npm:^7.24.8, @babel/traverse@npm:^7.25.0, @babel/traverse@npm:^7.25.4, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.27.3, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.4.5": - version: 7.28.0 - resolution: "@babel/traverse@npm:7.28.0" +"@babel/traverse@npm:^7.24.7, @babel/traverse@npm:^7.24.8, @babel/traverse@npm:^7.25.0, @babel/traverse@npm:^7.25.4, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4, @babel/traverse@npm:^7.4.5": + version: 7.28.4 + resolution: "@babel/traverse@npm:7.28.4" dependencies: "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.28.0" + "@babel/generator": "npm:^7.28.3" "@babel/helper-globals": "npm:^7.28.0" - "@babel/parser": "npm:^7.28.0" + "@babel/parser": "npm:^7.28.4" "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.28.0" + "@babel/types": "npm:^7.28.4" debug: "npm:^4.3.1" - checksum: 10/c1c24b12b6cb46241ec5d11ddbd2989d6955c282715cbd8ee91a09fe156b3bdb0b88353ac33329c2992113e3dfb5198f616c834f8805bb3fa85da1f864bec5f3 + checksum: 10/c3099364b7b1c36bcd111099195d4abeef16499e5defb1e56766b754e8b768c252e856ed9041665158aa1b31215fc6682632756803c8fa53405381ec08c4752b languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.27.1, @babel/types@npm:^7.28.0, @babel/types@npm:^7.28.2, @babel/types@npm:^7.3.3": - version: 7.28.2 - resolution: "@babel/types@npm:7.28.2" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.27.1, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4, @babel/types@npm:^7.3.3": + version: 7.28.4 + resolution: "@babel/types@npm:7.28.4" dependencies: "@babel/helper-string-parser": "npm:^7.27.1" "@babel/helper-validator-identifier": "npm:^7.27.1" - checksum: 10/a8de404a2e3109651f346d892dc020ce2c82046068f4ce24de7f487738dfbfa7bd716b35f1dcd6d6c32dde96208dc74a56b7f56a2c0bcb5af0ddc56cbee13533 + checksum: 10/db50bf257aafa5d845ad16dae0587f57d596e4be4cbb233ea539976a4c461f9fbcc0bf3d37adae3f8ce5dcb4001462aa608f3558161258b585f6ce6ce21a2e45 languageName: node linkType: hard @@ -3182,6 +3172,7 @@ __metadata: "@types/d3-selection": "npm:^3.0.1" "@types/d3-shape": "npm:^3.0.1" "@types/d3-zoom": "npm:^3.0.1" + "@types/fscreen": "npm:^1" "@types/google-protobuf": "npm:^3.7.2" "@types/react": "npm:^18.0.0" "@types/react-helmet": "npm:^6.1.0" @@ -3207,6 +3198,7 @@ __metadata: rc-progress: "npm:3.5.1" react: "npm:^18.0.2" react-dom: "npm:^18.0.2" + react-full-screen: "npm:^1.1.1" react-helmet: "npm:6.1.0" react-hook-form: "npm:^7.12.2" react-idle-timer: "npm:5.7.2" @@ -4120,6 +4112,27 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-auth-backend-module-openshift-provider@workspace:^, @backstage/plugin-auth-backend-module-openshift-provider@workspace:plugins/auth-backend-module-openshift-provider": + version: 0.0.0-use.local + resolution: "@backstage/plugin-auth-backend-module-openshift-provider@workspace:plugins/auth-backend-module-openshift-provider" + dependencies: + "@backstage/backend-defaults": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/plugin-auth-backend": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" + express: "npm:^4.18.2" + msw: "npm:^2.7.3" + passport-oauth2: "npm:^1.8.0" + supertest: "npm:^7.1.0" + zod: "npm:^3.24.2" + languageName: unknown + linkType: soft + "@backstage/plugin-auth-backend-module-pinniped-provider@workspace:plugins/auth-backend-module-pinniped-provider": version: 0.0.0-use.local resolution: "@backstage/plugin-auth-backend-module-pinniped-provider@workspace:plugins/auth-backend-module-pinniped-provider" @@ -4196,6 +4209,7 @@ __metadata: knex: "npm:^3.0.0" lodash: "npm:^4.17.21" luxon: "npm:^3.0.0" + matcher: "npm:^4.0.0" minimatch: "npm:^9.0.0" passport: "npm:^0.7.0" supertest: "npm:^7.0.0" @@ -4263,6 +4277,41 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-auth@workspace:^, @backstage/plugin-auth@workspace:plugins/auth": + version: 0.0.0-use.local + resolution: "@backstage/plugin-auth@workspace:plugins/auth" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/frontend-defaults": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": "npm:^4.12.2" + "@material-ui/icons": "npm:^4.9.1" + "@material-ui/lab": "npm:4.0.0-alpha.61" + "@testing-library/jest-dom": "npm:^6.0.0" + "@testing-library/react": "npm:^16.0.0" + "@testing-library/user-event": "npm:^14.0.0" + "@types/react": "npm:^18.0.0" + msw: "npm:^1.0.0" + react: "npm:^18.0.2" + react-dom: "npm:^18.0.2" + react-router-dom: "npm:^6.3.0" + react-use: "npm:^17.2.4" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + react-router-dom: ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + languageName: unknown + linkType: soft + "@backstage/plugin-bitbucket-cloud-common@workspace:^, @backstage/plugin-bitbucket-cloud-common@workspace:plugins/bitbucket-cloud-common": version: 0.0.0-use.local resolution: "@backstage/plugin-bitbucket-cloud-common@workspace:plugins/bitbucket-cloud-common" @@ -6602,6 +6651,9 @@ __metadata: "@backstage/plugin-permission-react": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/plugin-scaffolder-react": "workspace:^" + "@backstage/plugin-techdocs": "workspace:^" + "@backstage/plugin-techdocs-common": "workspace:^" + "@backstage/plugin-techdocs-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@codemirror/language": "npm:^6.0.0" @@ -7255,7 +7307,7 @@ __metadata: "@testing-library/user-event": "npm:^14.0.0" "@types/dompurify": "npm:^3.0.0" "@types/react": "npm:^18.0.0" - dompurify: "npm:^3.0.0" + dompurify: "npm:^3.2.4" git-url-parse: "npm:^15.0.0" jss: "npm:~10.10.0" lodash: "npm:^4.17.21" @@ -7513,14 +7565,12 @@ __metadata: "@backstage/cli": "workspace:^" "@base-ui-components/react": "npm:1.0.0-alpha.7" "@remixicon/react": "npm:^4.6.0" - "@storybook/react": "npm:^8.6.12" - "@storybook/test": "npm:^8.6.12" "@tanstack/react-table": "npm:^8.21.3" "@types/react": "npm:^18.0.0" "@types/react-dom": "npm:^18.0.0" chalk: "npm:^5.4.1" clsx: "npm:^2.1.1" - eslint-plugin-storybook: "npm:^0.12.0" + eslint-plugin-storybook: "npm:^9.1.5" glob: "npm:^11.0.1" globals: "npm:^15.11.0" lightningcss: "npm:^1.29.1" @@ -7529,7 +7579,7 @@ __metadata: react-aria-components: "npm:^1.10.1" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.3.0" - storybook: "npm:^8.6.12" + storybook: "npm:^9.1.5" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -7623,16 +7673,6 @@ __metadata: languageName: node linkType: hard -"@bundled-es-modules/tough-cookie@npm:^0.1.6": - version: 0.1.6 - resolution: "@bundled-es-modules/tough-cookie@npm:0.1.6" - dependencies: - "@types/tough-cookie": "npm:^4.0.5" - tough-cookie: "npm:^4.1.4" - checksum: 10/4f24a820f02c08c3ca0ff21272317357152093f76f9c8cc182517f61fa426ae53dadc4d68a3d6da5078e8d73f0ff8c0907a9f994c0be756162ba9c7358533e57 - languageName: node - linkType: hard - "@changesets/apply-release-plan@npm:^7.0.12": version: 7.0.12 resolution: "@changesets/apply-release-plan@npm:7.0.12" @@ -10044,24 +10084,24 @@ __metadata: languageName: node linkType: hard -"@joshwooding/vite-plugin-react-docgen-typescript@npm:0.5.0": - version: 0.5.0 - resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.5.0" +"@joshwooding/vite-plugin-react-docgen-typescript@npm:0.6.1": + version: 0.6.1 + resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.6.1" dependencies: glob: "npm:^10.0.0" - magic-string: "npm:^0.27.0" + magic-string: "npm:^0.30.0" react-docgen-typescript: "npm:^2.2.2" peerDependencies: typescript: ">= 4.3.x" - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 10/1dcb03f2df1723799a7a9c75ac8360990f75c44fd2425d2d52a9e21882fc3054d372892ab1cad0927864d8a934ad5b347f4ae00b01785649e2a8f1c4b861aa67 + checksum: 10/a82b6005378ccda13250fcfeaa04ec2ba17d1c2923b5bba5907d5b2fd658c661b29c1215eb1c0fe305b390bee89ec77c684aa68506262734c3bb3cd76d8a6963 languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.2": +"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.2, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.13 resolution: "@jridgewell/gen-mapping@npm:0.3.13" dependencies: @@ -10071,6 +10111,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/remapping@npm:^2.3.5": + version: 2.3.5 + resolution: "@jridgewell/remapping@npm:2.3.5" + dependencies: + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10/c2bb01856e65b506d439455f28aceacf130d6c023d1d4e3b48705e88def3571753e1a887daa04b078b562316c92d26ce36408a60534bceca3f830aec88a339ad + languageName: node + linkType: hard + "@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.1 resolution: "@jridgewell/resolve-uri@npm:3.1.1" @@ -10088,7 +10138,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.13, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15, @jridgewell/sourcemap-codec@npm:^1.5.0": +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15, @jridgewell/sourcemap-codec@npm:^1.5.0": version: 1.5.5 resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" checksum: 10/5d9d207b462c11e322d71911e55e21a4e2772f71ffe8d6f1221b8eb5ae6774458c1d242f897fb0814e8714ca9a6b498abfa74dfe4f434493342902b1a48b33a5 @@ -18299,277 +18349,82 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-actions@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-actions@npm:8.6.14" +"@storybook/addon-a11y@npm:^9.1.5": + version: 9.1.6 + resolution: "@storybook/addon-a11y@npm:9.1.6" dependencies: "@storybook/global": "npm:^5.0.0" - "@types/uuid": "npm:^9.0.1" - dequal: "npm:^2.0.2" - polished: "npm:^4.2.2" - uuid: "npm:^9.0.0" + axe-core: "npm:^4.2.0" peerDependencies: - storybook: ^8.6.14 - checksum: 10/5d96c1519ffb78ce663612e8ad850523fefe6f8aae3add662ab461015dfdd4e64c62720655526d471893018efdc5049807c3ec49998fb51dfab6cfb5aed30b73 + storybook: ^9.1.6 + checksum: 10/345c44673ccede4073e7415eb7c73f7817cb64b24b75d6f2edae73a0a6e50cc91e098a48dfd55996188aeb04d308c147b6d660bcdd4e5449d601271a83d4f178 languageName: node linkType: hard -"@storybook/addon-backgrounds@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-backgrounds@npm:8.6.14" - dependencies: - "@storybook/global": "npm:^5.0.0" - memoizerific: "npm:^1.11.3" - ts-dedent: "npm:^2.0.0" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/67343f7b2567b1831db44f3fe6872c9d72261da20eb43390a020ad4ec4420b5e44f1e8c60594bf39ddd0d290a6d05319874167692bd2676615a4f3902b7c897e - languageName: node - linkType: hard - -"@storybook/addon-controls@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-controls@npm:8.6.14" - dependencies: - "@storybook/global": "npm:^5.0.0" - dequal: "npm:^2.0.2" - ts-dedent: "npm:^2.0.0" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/2b63d6fb516886eec1342e18191dfc61f32203313be75bce547f30d69f8bc5084e002aae16275a0c355ffe49d476fe48497738f7b8c50272b1aac0e823b415c6 - languageName: node - linkType: hard - -"@storybook/addon-docs@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-docs@npm:8.6.14" +"@storybook/addon-docs@npm:^9.1.5": + version: 9.1.5 + resolution: "@storybook/addon-docs@npm:9.1.5" dependencies: "@mdx-js/react": "npm:^3.0.0" - "@storybook/blocks": "npm:8.6.14" - "@storybook/csf-plugin": "npm:8.6.14" - "@storybook/react-dom-shim": "npm:8.6.14" + "@storybook/csf-plugin": "npm:9.1.5" + "@storybook/icons": "npm:^1.4.0" + "@storybook/react-dom-shim": "npm:9.1.5" react: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" react-dom: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.6.14 - checksum: 10/a3d08b6418c93776091bd1619f0c821abd5dc620973f0f52025cc342c5d38377455ccd69a1df3f68f7e4e82dc57e039e627a1f4b6a4e856c4eef68a33fe0cbb4 + storybook: ^9.1.5 + checksum: 10/dcc2ed8df4593044667fc2a3bc62be2e6f7395e002eabee218d5376c0d0b38684508988823b5c75973a315ae1821d365879494c52e0c17daa814d9c0992c7ac3 languageName: node linkType: hard -"@storybook/addon-essentials@npm:^8.6.12": - version: 8.6.14 - resolution: "@storybook/addon-essentials@npm:8.6.14" - dependencies: - "@storybook/addon-actions": "npm:8.6.14" - "@storybook/addon-backgrounds": "npm:8.6.14" - "@storybook/addon-controls": "npm:8.6.14" - "@storybook/addon-docs": "npm:8.6.14" - "@storybook/addon-highlight": "npm:8.6.14" - "@storybook/addon-measure": "npm:8.6.14" - "@storybook/addon-outline": "npm:8.6.14" - "@storybook/addon-toolbars": "npm:8.6.14" - "@storybook/addon-viewport": "npm:8.6.14" - ts-dedent: "npm:^2.0.0" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/d32f2bad60736a277f5deef02adde28ec3aa188322760fe6ff4a0d726d1a9286e27e5442f3e7c55580514b0b64b08f3cd37eb12516cb7f035bafd4e26675e3bc - languageName: node - linkType: hard - -"@storybook/addon-highlight@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-highlight@npm:8.6.14" +"@storybook/addon-links@npm:^9.1.5": + version: 9.1.5 + resolution: "@storybook/addon-links@npm:9.1.5" dependencies: "@storybook/global": "npm:^5.0.0" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/bc3f9770c525ace543bd2b1967f11e460c11ff6379c488908d57fd0593cac66171cd5643809cca878559af2db51bb7b7c8556cfa847363b177c40d4e8435629e - languageName: node - linkType: hard - -"@storybook/addon-interactions@npm:^8.6.12": - version: 8.6.14 - resolution: "@storybook/addon-interactions@npm:8.6.14" - dependencies: - "@storybook/global": "npm:^5.0.0" - "@storybook/instrumenter": "npm:8.6.14" - "@storybook/test": "npm:8.6.14" - polished: "npm:^4.2.2" - ts-dedent: "npm:^2.2.0" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/e23724e61be80eae962c90ea60356b9a969f84ecf509a58e2a9bd8845a1fc284488b90d2376f514abc42de947b41231a5a3b470036e01e36d89346b18338fa81 - languageName: node - linkType: hard - -"@storybook/addon-links@npm:^8.6.12": - version: 8.6.14 - resolution: "@storybook/addon-links@npm:8.6.14" - dependencies: - "@storybook/global": "npm:^5.0.0" - ts-dedent: "npm:^2.0.0" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.14 + storybook: ^9.1.5 peerDependenciesMeta: react: optional: true - checksum: 10/d807623e9578f8793a71dc9a3d6988fc22784e4bfebfdd3dc1293cece3e6020caf711cf8fe5171b5ef4340856b50abcddb3a4b89adba0d16582b5f910dbdf884 + checksum: 10/c33630c61a4bbbc74ddfa5f2c9dc5abe310d0de39064df739f4b441208b4df0900edd800ecd8b275f3613a9a8412f8f250dc322ec7fc5aa7aad8c6cf94efae3f languageName: node linkType: hard -"@storybook/addon-measure@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-measure@npm:8.6.14" - dependencies: - "@storybook/global": "npm:^5.0.0" - tiny-invariant: "npm:^1.3.1" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/13fba65df34858fab4976d9dde00ada07a6a80825ec3dc0cf43ccc1fdfbb64d2df4d188a61478b4f648609ddb63de0152ec93666c737bca37dfbb5823b8051fc - languageName: node - linkType: hard - -"@storybook/addon-outline@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-outline@npm:8.6.14" - dependencies: - "@storybook/global": "npm:^5.0.0" - ts-dedent: "npm:^2.0.0" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/8d0351f86cb974fd90cf26041aebbb3683b9ed9b2088d4a3e72939921dde4f81825a3454210b537733ee615c2f113a37f153a510ab8abdd465ab0b97e70abfd0 - languageName: node - linkType: hard - -"@storybook/addon-storysource@npm:^8.6.12": - version: 8.6.14 - resolution: "@storybook/addon-storysource@npm:8.6.14" - dependencies: - "@storybook/source-loader": "npm:8.6.14" - estraverse: "npm:^5.2.0" - tiny-invariant: "npm:^1.3.1" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/46ade211014668fb028f810d4d82fe1ba1ebed7bff89e5e5a96fc189be7d5406ae5598da07b358a77551809b4659a951b025daf732a20f781c4dbfbf487c5716 - languageName: node - linkType: hard - -"@storybook/addon-themes@npm:^8.6.12": - version: 8.6.14 - resolution: "@storybook/addon-themes@npm:8.6.14" +"@storybook/addon-themes@npm:^9.1.5": + version: 9.1.5 + resolution: "@storybook/addon-themes@npm:9.1.5" dependencies: ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.6.14 - checksum: 10/b2f5e813bf95afc9b3c050cd4e06f620392a8cdb2102c60fae26ec6aa61bc11e1a16c5a2ce2bf0eaf6a608411c60c601df65958f0b9ff7af0131e9d5b83d0948 + storybook: ^9.1.5 + checksum: 10/c214cbfe5c94a9c58d0240f6009c62dc3189164dae19679d5503fe8cbf2603cfcf9b58cb500be63c6d92897563cbccef63b9413f14513b1be9f51f8c5b60438e languageName: node linkType: hard -"@storybook/addon-toolbars@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-toolbars@npm:8.6.14" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/890ea37b9270580ea531209d58a3552b87810364ed16f8a6898e339d00a9af6da755421aad2ae6e306ba0272f9cebe2e3d666db6135f7aa3108b2aa3c13bd351 - languageName: node - linkType: hard - -"@storybook/addon-viewport@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-viewport@npm:8.6.14" +"@storybook/builder-vite@npm:9.1.5": + version: 9.1.5 + resolution: "@storybook/builder-vite@npm:9.1.5" dependencies: - memoizerific: "npm:^1.11.3" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/4b3d1ead7fa827de9c97fea41ae4926ab27980dac5651c209a28d8b9fad69799f230e3fafc38b98a1b6d95c0e6b51d5ca00b413da2d7f43eed23fd3c9c794e4e - languageName: node - linkType: hard - -"@storybook/blocks@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/blocks@npm:8.6.14" - dependencies: - "@storybook/icons": "npm:^1.2.12" + "@storybook/csf-plugin": "npm:9.1.5" ts-dedent: "npm:^2.0.0" peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^8.6.14 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - checksum: 10/9bf85f9dd40d8eeb0f012d13a7c88d3d828b680f47c86e8472b76edda3127a87e9dbbf4e08cd8478a855558e5e58204479a08102a97ddf7b834850e760ba3f1c + storybook: ^9.1.5 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + checksum: 10/adf191098abd4685a9451cc9a4da17ecde59e6be951c049add6ab379c26325710d2f15e4191709029b055560bbf59e2845225b85f8d1fd49c1a0d04e82e4d57f languageName: node linkType: hard -"@storybook/builder-vite@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/builder-vite@npm:8.6.14" - dependencies: - "@storybook/csf-plugin": "npm:8.6.14" - browser-assert: "npm:^1.2.1" - ts-dedent: "npm:^2.0.0" - peerDependencies: - storybook: ^8.6.14 - vite: ^4.0.0 || ^5.0.0 || ^6.0.0 - checksum: 10/d49212238277b911c205b06cff12ea0c65158d089a4f29b0e5184c7e484f0b3d2e83845cf99df48b5f427e12b436d69bb9d8f9535e115a0ba8727bd2c8961bc4 - languageName: node - linkType: hard - -"@storybook/components@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/components@npm:8.6.14" - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/d3647505510313aa3c32fd1f8f202eda723ad99bd353b20aa929c5ecab4edc3c86ad06d0eac02f3c6d948e88f13f65ca5fa35ef27c079ef1b92abe8692f23699 - languageName: node - linkType: hard - -"@storybook/core@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/core@npm:8.6.14" - dependencies: - "@storybook/theming": "npm:8.6.14" - better-opn: "npm:^3.0.2" - browser-assert: "npm:^1.2.1" - esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0" - esbuild-register: "npm:^3.5.0" - jsdoc-type-pratt-parser: "npm:^4.0.0" - process: "npm:^0.11.10" - recast: "npm:^0.23.5" - semver: "npm:^7.6.2" - util: "npm:^0.12.5" - ws: "npm:^8.2.3" - peerDependencies: - prettier: ^2 || ^3 - peerDependenciesMeta: - prettier: - optional: true - checksum: 10/8f8c811edd4ea8dcedcc63a79b3168dc83aa0401e3760990cb5995ea6beaac4026c6ccc1046182f41eed7c09333005f3348d8b42f89fc5dd5b2ce011a9d2a48f - languageName: node - linkType: hard - -"@storybook/csf-plugin@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/csf-plugin@npm:8.6.14" +"@storybook/csf-plugin@npm:9.1.5": + version: 9.1.5 + resolution: "@storybook/csf-plugin@npm:9.1.5" dependencies: unplugin: "npm:^1.3.1" peerDependencies: - storybook: ^8.6.14 - checksum: 10/a0983268e6e77ff1bd6b06ad5a895d22ae534fab9cec74960e70a70023dcee44b2d6be4a77ad692a8ecb2e05e3d65bf4ef58dd0bdc52329b0599ba1e093a07c7 - languageName: node - linkType: hard - -"@storybook/csf@npm:^0.1.11": - version: 0.1.13 - resolution: "@storybook/csf@npm:0.1.13" - dependencies: - type-fest: "npm:^2.19.0" - checksum: 10/8a590703c44180798869fd12c1f314cb96de18349415a33bcfe30ef6af11fdc1cdb755ea620dedfd5eb7666cf05af5647b77fe28b63000aa52b53b0dc3c77bb5 + storybook: ^9.1.5 + checksum: 10/f86e178a51d9ea72903392767a7f2fdef98074e251ef90ef50035c9b74f40d9cc62d4ff74079b5e9e0aa9d68d337559beea77f3458ba928b379bfdedda2e5258 languageName: node linkType: hard @@ -18580,7 +18435,7 @@ __metadata: languageName: node linkType: hard -"@storybook/icons@npm:^1.2.12": +"@storybook/icons@npm:^1.4.0": version: 1.4.0 resolution: "@storybook/icons@npm:1.4.0" peerDependencies: @@ -18590,134 +18445,72 @@ __metadata: languageName: node linkType: hard -"@storybook/instrumenter@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/instrumenter@npm:8.6.14" - dependencies: - "@storybook/global": "npm:^5.0.0" - "@vitest/utils": "npm:^2.1.1" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/7e2381ffbbd9065c021892f6f24fbf950aa57857da33a215cd7ffd662b76a282df3da7c664bf3eff14858995e44dc649b4d62069b694d8c69b8de0e43d31efc7 - languageName: node - linkType: hard - -"@storybook/manager-api@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/manager-api@npm:8.6.14" - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/4544b317050b81574f1cd6f911dcb99ee2adbd7190555209171a7dd2233cd331165fa11c16a5977ebe58eeaf26c86bbfcb23f701cfd79a10f0d034dae65197bd - languageName: node - linkType: hard - -"@storybook/preview-api@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/preview-api@npm:8.6.14" - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/9b77288f2f627a7c70cfd3e88bdc4348c1425fab6b333ed77efb913e45881bcdf2ca67f1174e47fe978a7993c2a653fb8accf518ba51441cfb9450145101e9d8 - languageName: node - linkType: hard - -"@storybook/react-dom-shim@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/react-dom-shim@npm:8.6.14" +"@storybook/react-dom-shim@npm:9.1.5": + version: 9.1.5 + resolution: "@storybook/react-dom-shim@npm:9.1.5" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.14 - checksum: 10/d14d970b98dc8266d4df80225538fb537449c15877f0f54465dfe52242f0c3f041c975824cc2894f72a3a8023fdffa0a281d90325b95297858e3684913b3cc15 + storybook: ^9.1.5 + checksum: 10/8aa85b15a2d5584da0af285e52cf78b132fc772846cc2d4369349ecf6d966331ee05a8162c5b1ecf42acefe8d9659f7e0f7cfdd2e1230dc983cb989eefcfb7fe languageName: node linkType: hard -"@storybook/react-vite@npm:^8.6.12": - version: 8.6.14 - resolution: "@storybook/react-vite@npm:8.6.14" +"@storybook/react-vite@npm:^9.1.5": + version: 9.1.5 + resolution: "@storybook/react-vite@npm:9.1.5" dependencies: - "@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.5.0" + "@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.6.1" "@rollup/pluginutils": "npm:^5.0.2" - "@storybook/builder-vite": "npm:8.6.14" - "@storybook/react": "npm:8.6.14" - find-up: "npm:^5.0.0" + "@storybook/builder-vite": "npm:9.1.5" + "@storybook/react": "npm:9.1.5" + find-up: "npm:^7.0.0" magic-string: "npm:^0.30.0" - react-docgen: "npm:^7.0.0" + react-docgen: "npm:^8.0.0" resolve: "npm:^1.22.8" tsconfig-paths: "npm:^4.2.0" peerDependencies: - "@storybook/test": 8.6.14 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.14 - vite: ^4.0.0 || ^5.0.0 || ^6.0.0 - peerDependenciesMeta: - "@storybook/test": - optional: true - checksum: 10/061be89410b3835bb19e82e6770c93dee14cb2fa00616321cdd360e4f808f9927d951bca2e714be255e8b1aaff72dbc1af97e3e7c18878f0685858fd3cc85ec7 + storybook: ^9.1.5 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + checksum: 10/77ed64e8187636299b6b7de1a4db96aa2fed8b5be5cee844bbdeeab64b4b231aeb5c6bc5b84014983da095a4e3be782e5ea167126e7867c26d7c5d47263b20be languageName: node linkType: hard -"@storybook/react@npm:8.6.14, @storybook/react@npm:^8.6.12": - version: 8.6.14 - resolution: "@storybook/react@npm:8.6.14" +"@storybook/react@npm:9.1.5": + version: 9.1.5 + resolution: "@storybook/react@npm:9.1.5" dependencies: - "@storybook/components": "npm:8.6.14" "@storybook/global": "npm:^5.0.0" - "@storybook/manager-api": "npm:8.6.14" - "@storybook/preview-api": "npm:8.6.14" - "@storybook/react-dom-shim": "npm:8.6.14" - "@storybook/theming": "npm:8.6.14" + "@storybook/react-dom-shim": "npm:9.1.5" peerDependencies: - "@storybook/test": 8.6.14 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.14 - typescript: ">= 4.2.x" + storybook: ^9.1.5 + typescript: ">= 4.9.x" peerDependenciesMeta: - "@storybook/test": - optional: true typescript: optional: true - checksum: 10/a8710dcb80da9df4a78cfb3560a8c0c31c6f6c04902aa95446d4e40f6a9179770ead6f9a0a06fa44d00677cd1cd92eb60fdc57f433035ec722275847139671a4 + checksum: 10/a5b80438976011498998035b6cffb513246dfd6852b035639212a89f8a9114b16757803ce750feb863393af0b9f47ff170d202c5341b083ee4440663976f2939 languageName: node linkType: hard -"@storybook/source-loader@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/source-loader@npm:8.6.14" - dependencies: - es-toolkit: "npm:^1.22.0" - estraverse: "npm:^5.2.0" - prettier: "npm:^3.1.1" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/8c96c3e39f19944db297d1ea8a8560b101f3b7be6ab8e2ac1222155454f8fb3ff3a4db3c6646bad514eaf5c28fce51b037f3cc55f34edd419c4d8502caec88ea - languageName: node - linkType: hard - -"@storybook/test@npm:8.6.14, @storybook/test@npm:^8.6.12": - version: 8.6.14 - resolution: "@storybook/test@npm:8.6.14" +"@storybook/react@patch:@storybook/react@npm%3A9.1.5#~/.yarn/patches/@storybook-react-npm-9.1.5-2331f18b6b.patch": + version: 9.1.5 + resolution: "@storybook/react@patch:@storybook/react@npm%3A9.1.5#~/.yarn/patches/@storybook-react-npm-9.1.5-2331f18b6b.patch::version=9.1.5&hash=8c1145" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/instrumenter": "npm:8.6.14" - "@testing-library/dom": "npm:10.4.0" - "@testing-library/jest-dom": "npm:6.5.0" - "@testing-library/user-event": "npm:14.5.2" - "@vitest/expect": "npm:2.0.5" - "@vitest/spy": "npm:2.0.5" + "@storybook/react-dom-shim": "npm:9.1.5" peerDependencies: - storybook: ^8.6.14 - checksum: 10/47f65b441389b497a01cd3c3830cc46eb984836bb3c13848799c6074529f5609b284eb16f5473ac9029c7d857def1f13e998325083c4d09bb48a0b171d4221a3 - languageName: node - linkType: hard - -"@storybook/theming@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/theming@npm:8.6.14" - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/6936ea3348968fe598ad47421c11a78c6ee2ce62336ea1ce9cb8257e9faa2553d3ac3e443f8a36d35a41b0d60eb169231516649c710582ec68fdead4f23ffc0e + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.1.5 + typescript: ">= 4.9.x" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/17edd1c58208ca26ed805656fadda3b0797a995afc0d64005ce50b4383dc1b3bbfead2c9c4523f1450ee7b82570b0302acadedb17ea3d8aeacfb1b66f7f436d1 languageName: node linkType: hard @@ -19479,22 +19272,6 @@ __metadata: languageName: unknown linkType: soft -"@testing-library/dom@npm:10.4.0": - version: 10.4.0 - resolution: "@testing-library/dom@npm:10.4.0" - dependencies: - "@babel/code-frame": "npm:^7.10.4" - "@babel/runtime": "npm:^7.12.5" - "@types/aria-query": "npm:^5.0.1" - aria-query: "npm:5.3.0" - chalk: "npm:^4.1.0" - dom-accessibility-api: "npm:^0.5.9" - lz-string: "npm:^1.5.0" - pretty-format: "npm:^27.0.2" - checksum: 10/05825ee9a15b88cbdae12c137db7111c34069ed3c7a1bd03b6696cb1b37b29f6f2d2de581ebf03033e7df1ab7ebf08399310293f440a4845d95c02c0a9ecc899 - languageName: node - linkType: hard - "@testing-library/dom@npm:^10.0.0": version: 10.4.1 resolution: "@testing-library/dom@npm:10.4.1" @@ -19527,33 +19304,17 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:6.5.0": - version: 6.5.0 - resolution: "@testing-library/jest-dom@npm:6.5.0" - dependencies: - "@adobe/css-tools": "npm:^4.4.0" - aria-query: "npm:^5.0.0" - chalk: "npm:^3.0.0" - css.escape: "npm:^1.5.1" - dom-accessibility-api: "npm:^0.6.3" - lodash: "npm:^4.17.21" - redent: "npm:^3.0.0" - checksum: 10/3d2080888af5fd7306f57448beb5a23f55d965e265b5e53394fffc112dfb0678d616a5274ff0200c46c7618f293520f86fc8562eecd8bdbc0dbb3294d63ec431 - languageName: node - linkType: hard - -"@testing-library/jest-dom@npm:^6.0.0": - version: 6.6.4 - resolution: "@testing-library/jest-dom@npm:6.6.4" +"@testing-library/jest-dom@npm:^6.0.0, @testing-library/jest-dom@npm:^6.6.3": + version: 6.8.0 + resolution: "@testing-library/jest-dom@npm:6.8.0" dependencies: "@adobe/css-tools": "npm:^4.4.0" aria-query: "npm:^5.0.0" css.escape: "npm:^1.5.1" dom-accessibility-api: "npm:^0.6.3" - lodash: "npm:^4.17.21" picocolors: "npm:^1.1.1" redent: "npm:^3.0.0" - checksum: 10/5e67112c789f884fb75b279c2cddfdd0995a012a7847a03c474e4134f0d213934ee70c97433bca26b45e3a5ffa56faafe6499c8e57841179c4f2bd80eef429cd + checksum: 10/d9bebf1f32e46fdde7e12a2b1ee1f8d113b2fb56e86720f185c32ccae2b76e74c76ecaa69c3aeee98a5fded3417b47032891e7ec9be83d4e6bf888ed8356032f languageName: node linkType: hard @@ -19613,16 +19374,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/user-event@npm:14.5.2": - version: 14.5.2 - resolution: "@testing-library/user-event@npm:14.5.2" - peerDependencies: - "@testing-library/dom": ">=7.21.4" - checksum: 10/49821459d81c6bc435d97128d6386ca24f1e4b3ba8e46cb5a96fe3643efa6e002d88c1b02b7f2ec58da593e805c59b78d7fdf0db565c1f02ba782f63ee984040 - languageName: node - linkType: hard - -"@testing-library/user-event@npm:^14.0.0": +"@testing-library/user-event@npm:^14.0.0, @testing-library/user-event@npm:^14.6.1": version: 14.6.1 resolution: "@testing-library/user-event@npm:14.6.1" peerDependencies: @@ -19853,7 +19605,7 @@ __metadata: languageName: node linkType: hard -"@types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.18.0": +"@types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.20.5": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" dependencies: @@ -19885,7 +19637,7 @@ __metadata: languageName: node linkType: hard -"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.0.6, @types/babel__traverse@npm:^7.18.0": +"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.0.6, @types/babel__traverse@npm:^7.20.7": version: 7.28.0 resolution: "@types/babel__traverse@npm:7.28.0" dependencies: @@ -20326,6 +20078,13 @@ __metadata: languageName: node linkType: hard +"@types/fscreen@npm:^1": + version: 1.0.4 + resolution: "@types/fscreen@npm:1.0.4" + checksum: 10/78459a457ce7a6b7d72a5f17fdb54bbeb93c58ab77fd2858aac610fed2435bc4be9e5d2fb9883b6669b7f3a1204115cc2be59a027ab937ee8b5186225d2ea53d + languageName: node + linkType: hard + "@types/git-url-parse@npm:^9.0.0": version: 9.0.3 resolution: "@types/git-url-parse@npm:9.0.3" @@ -21505,7 +21264,7 @@ __metadata: languageName: node linkType: hard -"@types/tough-cookie@npm:*, @types/tough-cookie@npm:^4.0.5": +"@types/tough-cookie@npm:*": version: 4.0.5 resolution: "@types/tough-cookie@npm:4.0.5" checksum: 10/01fd82efc8202670865928629697b62fe9bf0c0dcbc5b1c115831caeb073a2c0abb871ff393d7df1ae94ea41e256cb87d2a5a91fd03cdb1b0b4384e08d4ee482 @@ -21526,10 +21285,10 @@ __metadata: languageName: node linkType: hard -"@types/trusted-types@npm:*": - version: 2.0.3 - resolution: "@types/trusted-types@npm:2.0.3" - checksum: 10/4794804bc4a4a173d589841b6d26cf455ff5dc4f3e704e847de7d65d215f2e7043d8757e4741ce3a823af3f08260a8d04a1a6e9c5ec9b20b7b04586956a6b005 +"@types/trusted-types@npm:*, @types/trusted-types@npm:^2.0.7": + version: 2.0.7 + resolution: "@types/trusted-types@npm:2.0.7" + checksum: 10/8e4202766a65877efcf5d5a41b7dd458480b36195e580a3b1085ad21e948bc417d55d6f8af1fd2a7ad008015d4117d5fdfe432731157da3c68678487174e4ba3 languageName: node linkType: hard @@ -21561,13 +21320,6 @@ __metadata: languageName: node linkType: hard -"@types/uuid@npm:^9.0.1": - version: 9.0.8 - resolution: "@types/uuid@npm:9.0.8" - checksum: 10/b8c60b7ba8250356b5088302583d1704a4e1a13558d143c549c408bf8920535602ffc12394ede77f8a8083511b023704bc66d1345792714002bfa261b17c5275 - languageName: node - linkType: hard - "@types/vinyl@npm:^2.0.4": version: 2.0.6 resolution: "@types/vinyl@npm:2.0.6" @@ -22230,19 +21982,7 @@ __metadata: languageName: node linkType: hard -"@vitest/expect@npm:2.0.5": - version: 2.0.5 - resolution: "@vitest/expect@npm:2.0.5" - dependencies: - "@vitest/spy": "npm:2.0.5" - "@vitest/utils": "npm:2.0.5" - chai: "npm:^5.1.1" - tinyrainbow: "npm:^1.2.0" - checksum: 10/ca9a218f50254b2259fd16166b2d8c9ccc8ee2cc068905e6b3d6281da10967b1590cc7d34b5fa9d429297f97e740450233745583b4cc12272ff11705faf70a37 - languageName: node - linkType: hard - -"@vitest/expect@npm:>1.6.0": +"@vitest/expect@npm:3.2.4, @vitest/expect@npm:>1.6.0": version: 3.2.4 resolution: "@vitest/expect@npm:3.2.4" dependencies: @@ -22255,21 +21995,22 @@ __metadata: languageName: node linkType: hard -"@vitest/pretty-format@npm:2.0.5": - version: 2.0.5 - resolution: "@vitest/pretty-format@npm:2.0.5" +"@vitest/mocker@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/mocker@npm:3.2.4" dependencies: - tinyrainbow: "npm:^1.2.0" - checksum: 10/70bf452dd0b8525e658795125b3f11110bd6baadfaa38c5bb91ca763bded35ec6dc80e27964ad4e91b91be6544d35e18ea7748c1997693988f975a7283c3e9a0 - languageName: node - linkType: hard - -"@vitest/pretty-format@npm:2.1.9": - version: 2.1.9 - resolution: "@vitest/pretty-format@npm:2.1.9" - dependencies: - tinyrainbow: "npm:^1.2.0" - checksum: 10/557dc637c5825abd62ccb15080e59e04d22121e746d8020a0815d7c0c45132fed81b1ff36b26f5991e57a9f1d36e52aa19712abbfe1d0cbcd14252b449a919dc + "@vitest/spy": "npm:3.2.4" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.17" + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10/5e92431b6ed9fc1679060e4caef3e4623f4750542a5d7cd944774f8217c4d231e273202e8aea00bab33260a5a9222ecb7005d80da0348c3c829bd37d123071a8 languageName: node linkType: hard @@ -22282,15 +22023,6 @@ __metadata: languageName: node linkType: hard -"@vitest/spy@npm:2.0.5": - version: 2.0.5 - resolution: "@vitest/spy@npm:2.0.5" - dependencies: - tinyspy: "npm:^3.0.0" - checksum: 10/ed19f4c3bb4d3853241e8070979615138e24403ce4c137fa48c903b3af2c8b3ada2cc26aca9c1aa323bb314a457a8130a29acbb18dafd4e42737deefb2abf1ca - languageName: node - linkType: hard - "@vitest/spy@npm:3.2.4": version: 3.2.4 resolution: "@vitest/spy@npm:3.2.4" @@ -22300,18 +22032,6 @@ __metadata: languageName: node linkType: hard -"@vitest/utils@npm:2.0.5": - version: 2.0.5 - resolution: "@vitest/utils@npm:2.0.5" - dependencies: - "@vitest/pretty-format": "npm:2.0.5" - estree-walker: "npm:^3.0.3" - loupe: "npm:^3.1.1" - tinyrainbow: "npm:^1.2.0" - checksum: 10/d631d56d29c33bc8de631166b2b6691c470187a345469dfef7048befe6027e1c6ff9552f2ee11c8a247522c325c4a64bfcc73f8f0f0c525da39cb9f190f119f8 - languageName: node - linkType: hard - "@vitest/utils@npm:3.2.4": version: 3.2.4 resolution: "@vitest/utils@npm:3.2.4" @@ -22323,17 +22043,6 @@ __metadata: languageName: node linkType: hard -"@vitest/utils@npm:^2.1.1": - version: 2.1.9 - resolution: "@vitest/utils@npm:2.1.9" - dependencies: - "@vitest/pretty-format": "npm:2.1.9" - loupe: "npm:^3.1.2" - tinyrainbow: "npm:^1.2.0" - checksum: 10/83d62d5703a3210a2f137c25dc4e797a7a1d74d5d2e14ecc33b274c7710304fa8b5099101c98bc8d66cc2bf18a14f88ebf21f0996a99d0ee1439ae23b49f3961 - languageName: node - linkType: hard - "@vue/compiler-core@npm:3.5.17": version: 3.5.17 resolution: "@vue/compiler-core@npm:3.5.17" @@ -24258,16 +23967,7 @@ __metadata: languageName: node linkType: hard -"ast-types@npm:^0.13.4": - version: 0.13.4 - resolution: "ast-types@npm:0.13.4" - dependencies: - tslib: "npm:^2.0.1" - checksum: 10/c55b375b9aaf44713d8c0f77a08215ab6d44f368b13e44f2141c421022af3c62b615a30c8ea629457f0cbaec409c713401c0188a124552c8fe4a5ad6b17ff3c3 - languageName: node - linkType: hard - -"ast-types@npm:^0.16.1": +"ast-types@npm:0.16.1": version: 0.16.1 resolution: "ast-types@npm:0.16.1" dependencies: @@ -24285,6 +23985,15 @@ __metadata: languageName: node linkType: hard +"ast-types@patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch::locator=root%40workspace%3A.": + version: 0.16.1 + resolution: "ast-types@patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch::version=0.16.1&hash=538845&locator=root%40workspace%3A." + dependencies: + tslib: "npm:^2.0.1" + checksum: 10/15c639f8c04116cc3ef0941569549e7cc389b47901ca8b851ad87d3461d9e1dbf85da25a84c2e135864efdcc1bc833b90be216868b7dc7ac4613afcaa98939e0 + languageName: node + linkType: hard + "astral-regex@npm:^2.0.0": version: 2.0.0 resolution: "astral-regex@npm:2.0.0" @@ -24468,10 +24177,10 @@ __metadata: languageName: node linkType: hard -"axe-core@npm:^4.10.0": - version: 4.10.0 - resolution: "axe-core@npm:4.10.0" - checksum: 10/6158489a7a704edc98bd30ed56243b8280c5203c60e095a2feb5bff95d9bf2ef10becfe359b1cbc8601338418999c26cf4eee704181dedbcb487f4d63a06d8d5 +"axe-core@npm:^4.10.0, axe-core@npm:^4.2.0": + version: 4.10.3 + resolution: "axe-core@npm:4.10.3" + checksum: 10/9ff51ad0fd0fdec5c0247ea74e8ace5990b54c7f01f8fa3e5cd8ba98b0db24d8ebd7bab4a9bd4d75c28c4edcd1eac455b44c8c6c258c6a98f3d2f88bc60af4cc languageName: node linkType: hard @@ -24500,14 +24209,14 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.0.0, axios@npm:^1.11.0, axios@npm:^1.6.0, axios@npm:^1.7.4, axios@npm:^1.8.3": - version: 1.11.0 - resolution: "axios@npm:1.11.0" +"axios@npm:^1.0.0, axios@npm:^1.12.0, axios@npm:^1.6.0, axios@npm:^1.7.4, axios@npm:^1.8.3": + version: 1.12.2 + resolution: "axios@npm:1.12.2" dependencies: follow-redirects: "npm:^1.15.6" form-data: "npm:^4.0.4" proxy-from-env: "npm:^1.1.0" - checksum: 10/232df4af7a4e4e07baa84621b9cc4b0c518a757b4eacc7f635c0eb3642cb98dff347326739f24b891b3b4481b7b838c79a3a0c4819c9fbc1fc40232431b9c5dc + checksum: 10/886a79770594eaad76493fecf90344b567bd956240609b5dcd09bd0afe8d3e6f1ad6d3257a93a483b6192b409d4b673d9515a34619e3e3ed1b2c0ec2a83b20ba languageName: node linkType: hard @@ -25103,13 +24812,6 @@ __metadata: languageName: node linkType: hard -"browser-assert@npm:^1.2.1": - version: 1.2.1 - resolution: "browser-assert@npm:1.2.1" - checksum: 10/8b2407cd04c1ed592cf892dec35942b7d72635829221e0788c9a16c4d2afa8b7156bc9705b1c4b32c30d88136c576fda3cbcb8f494d6f865264c706ea8798d92 - languageName: node - linkType: hard - "browser-headers@npm:^0.4.1": version: 0.4.1 resolution: "browser-headers@npm:0.4.1" @@ -25651,7 +25353,7 @@ __metadata: languageName: node linkType: hard -"chai@npm:^5.1.1, chai@npm:^5.2.0": +"chai@npm:^5.2.0": version: 5.2.1 resolution: "chai@npm:5.2.1" dependencies: @@ -26460,10 +26162,10 @@ __metadata: languageName: node linkType: hard -"component-emitter@npm:^1.3.1": - version: 1.3.1 - resolution: "component-emitter@npm:1.3.1" - checksum: 10/94550aa462c7bd5a61c1bc480e28554aa306066930152d1b1844a0dd3845d4e5db7e261ddec62ae184913b3e59b55a2ad84093b9d3596a8f17c341514d6c483d +"component-emitter@npm:^1.3.0": + version: 1.3.0 + resolution: "component-emitter@npm:1.3.0" + checksum: 10/dfc1ec2e7aa2486346c068f8d764e3eefe2e1ca0b24f57506cd93b2ae3d67829a7ebd7cc16e2bf51368fac2f45f78fcff231718e40b1975647e4a86be65e1d05 languageName: node linkType: hard @@ -27687,15 +27389,15 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0": - version: 4.4.1 - resolution: "debug@npm:4.4.1" +"debug@npm:4, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0": + version: 4.4.0 + resolution: "debug@npm:4.4.0" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10/8e2709b2144f03c7950f8804d01ccb3786373df01e406a0f66928e47001cf2d336cbed9ee137261d4f90d68d8679468c755e3548ed83ddacdc82b194d2468afe + checksum: 10/1847944c2e3c2c732514b93d11886575625686056cd765336212dc15de2d2b29612b6cd80e1afba767bb8e1803b778caf9973e98169ef1a24a7a7009e1820367 languageName: node linkType: hard @@ -28019,7 +27721,7 @@ __metadata: languageName: node linkType: hard -"dequal@npm:^2.0.0, dequal@npm:^2.0.2, dequal@npm:^2.0.3": +"dequal@npm:^2.0.0, dequal@npm:^2.0.3": version: 2.0.3 resolution: "dequal@npm:2.0.3" checksum: 10/6ff05a7561f33603df87c45e389c9ac0a95e3c056be3da1a0c4702149e3a7f6fe5ffbb294478687ba51a9e95f3a60e8b6b9005993acd79c292c7d15f71964b6b @@ -28477,10 +28179,15 @@ __metadata: languageName: node linkType: hard -"dompurify@npm:^3.0.0, dompurify@npm:^3.1.7": - version: 3.1.7 - resolution: "dompurify@npm:3.1.7" - checksum: 10/dc637a064306f83cf911caa267ffe1f973552047602020e3b6723c90f67962813edf8a65a0b62e8c9bc13fcd173a2691212a3719bc116226967f46bcd6181277 +"dompurify@npm:^3.1.7, dompurify@npm:^3.2.4": + version: 3.2.6 + resolution: "dompurify@npm:3.2.6" + dependencies: + "@types/trusted-types": "npm:^2.0.7" + dependenciesMeta: + "@types/trusted-types": + optional: true + checksum: 10/b91631ed0e4d17fae950ef53613cc009ed7e73adc43ac94a41dd52f35483f7538d13caebdafa7626e0da145fc8184e7ac7935f14f25b7e841b32fda777e40447 languageName: node linkType: hard @@ -29103,18 +28810,6 @@ __metadata: languageName: node linkType: hard -"es-toolkit@npm:^1.22.0": - version: 1.39.9 - resolution: "es-toolkit@npm:1.39.9" - dependenciesMeta: - "@trivago/prettier-plugin-sort-imports@4.3.0": - unplugged: true - prettier-plugin-sort-re-exports@0.0.1: - unplugged: true - checksum: 10/39b2fb9173ae2782a31b7bda896809261078744f5c658e5f4a7d7121a221a9e0c0b816610c17fec02629b92556a86e4834c7d6c956de05b09f418cc42dba3936 - languageName: node - linkType: hard - "es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.46, es5-ext@npm:^0.10.50, es5-ext@npm:^0.10.53, es5-ext@npm:^0.10.61, es5-ext@npm:^0.10.62, es5-ext@npm:~0.10.14, es5-ext@npm:~0.10.2, es5-ext@npm:~0.10.46": version: 0.10.63 resolution: "es5-ext@npm:0.10.63" @@ -29639,16 +29334,15 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-storybook@npm:^0.12.0": - version: 0.12.0 - resolution: "eslint-plugin-storybook@npm:0.12.0" +"eslint-plugin-storybook@npm:^9.1.5": + version: 9.1.5 + resolution: "eslint-plugin-storybook@npm:9.1.5" dependencies: - "@storybook/csf": "npm:^0.1.11" "@typescript-eslint/utils": "npm:^8.8.1" - ts-dedent: "npm:^2.2.0" peerDependencies: eslint: ">=8" - checksum: 10/278ea59565e30b74ee1d57f0a8f704906eaf40973b13999ec2c44872bb90c7505dfb12777b264940e2b480e81ace85c0532af69666e76a783b8ffa898a1d49ad + storybook: ^9.1.5 + checksum: 10/e852f8e667f056b07a47182bc2d6e931469e27087df099da3340cfedd61e5bb6e529e0e7c793d1eab318214d34f0ac9c437f96bf1df92e9b3eada39d2a6d2839 languageName: node linkType: hard @@ -30008,6 +29702,7 @@ __metadata: "@backstage/plugin-api-docs": "workspace:^" "@backstage/plugin-app": "workspace:^" "@backstage/plugin-app-visualizer": "workspace:^" + "@backstage/plugin-auth": "workspace:^" "@backstage/plugin-auth-react": "workspace:^" "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" @@ -30118,7 +29813,7 @@ __metadata: "@types/react": "npm:*" "@types/react-dom": "npm:*" "@types/zen-observable": "npm:^0.8.0" - axios: "npm:^1.11.0" + axios: "npm:^1.12.0" cross-env: "npm:^7.0.0" history: "npm:^5.0.0" msw: "npm:^1.0.0" @@ -30143,6 +29838,7 @@ __metadata: "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-backend-module-github-provider": "workspace:^" "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^" + "@backstage/plugin-auth-backend-module-openshift-provider": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-catalog-backend": "workspace:^" "@backstage/plugin-catalog-backend-module-backstage-openapi": "workspace:^" @@ -30685,15 +30381,15 @@ __metadata: languageName: node linkType: hard -"fdir@npm:^6.4.4, fdir@npm:^6.4.6": - version: 6.4.6 - resolution: "fdir@npm:6.4.6" +"fdir@npm:^6.5.0": + version: 6.5.0 + resolution: "fdir@npm:6.5.0" peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: picomatch: optional: true - checksum: 10/c186ba387e7b75ccf874a098d9bc5fe0af0e9c52fc56f8eac8e80aa4edb65532684bf2bf769894ff90f53bf221d6136692052d31f07a9952807acae6cbe7ee50 + checksum: 10/14ca1c9f0a0e8f4f2e9bf4e8551065a164a09545dae548c12a18d238b72e51e5a7b39bd8e5494b56463a0877672d0a6c1ef62c6fa0677db1b0c847773be939b1 languageName: node linkType: hard @@ -30967,6 +30663,17 @@ __metadata: languageName: node linkType: hard +"find-up@npm:^7.0.0": + version: 7.0.0 + resolution: "find-up@npm:7.0.0" + dependencies: + locate-path: "npm:^7.2.0" + path-exists: "npm:^5.0.0" + unicorn-magic: "npm:^0.1.0" + checksum: 10/7e6b08fbc05a10677e25e74bb0a020054a86b31d1806c5e6a9e32e75472bbf177210bc16e5f97453be8bda7ae2e3d97669dbb2901f8c30b39ce53929cbea6746 + languageName: node + linkType: hard + "find-yarn-workspace-root2@npm:1.2.16": version: 1.2.16 resolution: "find-yarn-workspace-root2@npm:1.2.16" @@ -31197,7 +30904,7 @@ __metadata: languageName: node linkType: hard -"formidable@npm:^3.5.4": +"formidable@npm:^3.5.1": version: 3.5.4 resolution: "formidable@npm:3.5.4" dependencies: @@ -31401,6 +31108,13 @@ __metadata: languageName: node linkType: hard +"fscreen@npm:^1.0.2": + version: 1.2.0 + resolution: "fscreen@npm:1.2.0" + checksum: 10/ac50f9ac52a157b8fe6aaecdf9efa7c1cfa90b42a76c3bc6b85372fab05c5a9cd72c1b7f4c2e273eba1a0e630e381fd72ae135fcc57acd05a0943d5d0c21b451 + languageName: node + linkType: hard + "fsevents@npm:2.3.2": version: 2.3.2 resolution: "fsevents@npm:2.3.2" @@ -35242,13 +34956,6 @@ __metadata: languageName: node linkType: hard -"jsdoc-type-pratt-parser@npm:^4.0.0": - version: 4.1.0 - resolution: "jsdoc-type-pratt-parser@npm:4.1.0" - checksum: 10/30d88f95f6cbb4a1aa6d4b0d0ae46eb1096e606235ecaf9bab7a3ed5da860516b5d1cd967182765002f292c627526db918f3e56d34637bcf810e6ef84d403f3f - languageName: node - linkType: hard - "jsdom@npm:^20.0.0": version: 20.0.0 resolution: "jsdom@npm:20.0.0" @@ -36463,6 +36170,15 @@ __metadata: languageName: node linkType: hard +"locate-path@npm:^7.2.0": + version: 7.2.0 + resolution: "locate-path@npm:7.2.0" + dependencies: + p-locate: "npm:^6.0.0" + checksum: 10/1c6d269d4efec555937081be964e8a9b4a136319c79ca1d45ac6382212a8466113c75bd89e44521ca8ecd1c47fb08523b56eee5c0712bc7d14fec5f729deeb42 + languageName: node + linkType: hard + "lodash-es@npm:^4.17.21": version: 4.17.21 resolution: "lodash-es@npm:4.17.21" @@ -36907,7 +36623,7 @@ __metadata: languageName: node linkType: hard -"loupe@npm:^3.1.0, loupe@npm:^3.1.1, loupe@npm:^3.1.2, loupe@npm:^3.1.4": +"loupe@npm:^3.1.0, loupe@npm:^3.1.4": version: 3.2.0 resolution: "loupe@npm:3.2.0" checksum: 10/80d48e35b014c2ba5886e25a02ee4cc9c1f659b0eca9c4fa8f07051cdc689e0507a763fbe05a63abbd3b7d4640774a722c905d4b1681b4b92c3ba8f87d96fea2 @@ -37075,15 +36791,6 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.27.0": - version: 0.27.0 - resolution: "magic-string@npm:0.27.0" - dependencies: - "@jridgewell/sourcemap-codec": "npm:^1.4.13" - checksum: 10/10a18a48d22fb14467d6cb4204aba58d6790ae7ba023835dc7a65e310cf216f042a17fab1155ba43e47117310a9b7c3fd3bb79f40be40f5124d6b1af9e96399b - languageName: node - linkType: hard - "magic-string@npm:^0.30.0, magic-string@npm:^0.30.10, magic-string@npm:^0.30.17, magic-string@npm:^0.30.3": version: 0.30.17 resolution: "magic-string@npm:0.30.17" @@ -37223,13 +36930,6 @@ __metadata: languageName: node linkType: hard -"map-or-similar@npm:^1.5.0": - version: 1.5.0 - resolution: "map-or-similar@npm:1.5.0" - checksum: 10/3cf43bcd0e7af41d7bade5f8b5be6bb9d021cc47e6008ad545d071cf3a709ba782884002f9eec6ccd51f572fc17841e07bf74628e0bc3694c33f4622b03e4b4c - languageName: node - linkType: hard - "markdown-escape@npm:^2.0.0": version: 2.0.0 resolution: "markdown-escape@npm:2.0.0" @@ -37302,6 +37002,15 @@ __metadata: languageName: node linkType: hard +"matcher@npm:^4.0.0": + version: 4.0.0 + resolution: "matcher@npm:4.0.0" + dependencies: + escape-string-regexp: "npm:^4.0.0" + checksum: 10/d338aff31d8dfd3626873e43777f46b123579734d53bb8d18d64b08a822ba5e8d39f5fe2e23403258e6143aa0cbe20a15662720d825cd0d3af961d5a44230328 + languageName: node + linkType: hard + "material-ui-confirm@npm:^3.0.12": version: 3.0.18 resolution: "material-ui-confirm@npm:3.0.18" @@ -37625,15 +37334,6 @@ __metadata: languageName: node linkType: hard -"memoizerific@npm:^1.11.3": - version: 1.11.3 - resolution: "memoizerific@npm:1.11.3" - dependencies: - map-or-similar: "npm:^1.5.0" - checksum: 10/72b6b80699777d000f03db6e15fdabcd4afe77feb45be51fe195cb230c64a368fcfcfbb976375eac3283bd8193d6b1a67ac3081cae07f64fca73f1aa568d59e3 - languageName: node - linkType: hard - "merge-descriptors@npm:1.0.3, merge-descriptors@npm:^1.0.1": version: 1.0.3 resolution: "merge-descriptors@npm:1.0.3" @@ -38727,13 +38427,12 @@ __metadata: languageName: node linkType: hard -"msw@npm:^2.0.0, msw@npm:^2.0.8": - version: 2.10.4 - resolution: "msw@npm:2.10.4" +"msw@npm:^2.0.0, msw@npm:^2.0.8, msw@npm:^2.7.3": + version: 2.11.2 + resolution: "msw@npm:2.11.2" dependencies: "@bundled-es-modules/cookie": "npm:^2.0.1" "@bundled-es-modules/statuses": "npm:^1.0.1" - "@bundled-es-modules/tough-cookie": "npm:^0.1.6" "@inquirer/confirm": "npm:^5.0.0" "@mswjs/interceptors": "npm:^0.39.1" "@open-draft/deferred-promise": "npm:^2.2.0" @@ -38746,7 +38445,9 @@ __metadata: outvariant: "npm:^1.4.3" path-to-regexp: "npm:^6.3.0" picocolors: "npm:^1.1.1" + rettime: "npm:^0.7.0" strict-event-emitter: "npm:^0.5.1" + tough-cookie: "npm:^6.0.0" type-fest: "npm:^4.26.1" yargs: "npm:^17.7.2" peerDependencies: @@ -38756,7 +38457,7 @@ __metadata: optional: true bin: msw: cli/index.js - checksum: 10/e2f25dda1aba66c7444c29c41d3157cb15c0332055ab7ebfb74ef4b506e7b90098cf37c577768edb5b2b2dbf0d6ed6a7a3ca8ee6da3d72df5a25823d82f33316 + checksum: 10/cd31627e79bfdc22d6695b5a001804ee4fc7145a157616589c72bdf95c60f1f12c5abee05730a9683e1a68d613c2e767a235fdadc21d30e398a1996c4b699d29 languageName: node linkType: hard @@ -40257,6 +39958,15 @@ __metadata: languageName: node linkType: hard +"p-limit@npm:^4.0.0": + version: 4.0.0 + resolution: "p-limit@npm:4.0.0" + dependencies: + yocto-queue: "npm:^1.0.0" + checksum: 10/01d9d70695187788f984226e16c903475ec6a947ee7b21948d6f597bed788e3112cc7ec2e171c1d37125057a5f45f3da21d8653e04a3a793589e12e9e80e756b + languageName: node + linkType: hard + "p-locate@npm:^3.0.0": version: 3.0.0 resolution: "p-locate@npm:3.0.0" @@ -40284,6 +39994,15 @@ __metadata: languageName: node linkType: hard +"p-locate@npm:^6.0.0": + version: 6.0.0 + resolution: "p-locate@npm:6.0.0" + dependencies: + p-limit: "npm:^4.0.0" + checksum: 10/2bfe5234efa5e7a4e74b30a5479a193fdd9236f8f6b4d2f3f69e3d286d9a7d7ab0c118a2a50142efcf4e41625def635bd9332d6cbf9cc65d85eb0718c579ab38 + languageName: node + linkType: hard + "p-map@npm:^2.0.0": version: 2.1.0 resolution: "p-map@npm:2.1.0" @@ -40706,7 +40425,7 @@ __metadata: languageName: node linkType: hard -"passport-oauth2@npm:1.8.0, passport-oauth2@npm:1.x.x, passport-oauth2@npm:^1.1.2, passport-oauth2@npm:^1.4.0, passport-oauth2@npm:^1.6.0, passport-oauth2@npm:^1.6.1, passport-oauth2@npm:^1.7.0": +"passport-oauth2@npm:1.8.0, passport-oauth2@npm:1.x.x, passport-oauth2@npm:^1.1.2, passport-oauth2@npm:^1.4.0, passport-oauth2@npm:^1.6.0, passport-oauth2@npm:^1.6.1, passport-oauth2@npm:^1.7.0, passport-oauth2@npm:^1.8.0": version: 1.8.0 resolution: "passport-oauth2@npm:1.8.0" dependencies: @@ -40793,6 +40512,13 @@ __metadata: languageName: node linkType: hard +"path-exists@npm:^5.0.0": + version: 5.0.0 + resolution: "path-exists@npm:5.0.0" + checksum: 10/8ca842868cab09423994596eb2c5ec2a971c17d1a3cb36dbf060592c730c725cd524b9067d7d2a1e031fef9ba7bd2ac6dc5ec9fb92aa693265f7be3987045254 + languageName: node + linkType: hard + "path-is-absolute@npm:^1.0.0": version: 1.0.1 resolution: "path-is-absolute@npm:1.0.1" @@ -41287,15 +41013,6 @@ __metadata: languageName: node linkType: hard -"polished@npm:^4.2.2": - version: 4.3.1 - resolution: "polished@npm:4.3.1" - dependencies: - "@babel/runtime": "npm:^7.17.8" - checksum: 10/0902fe2eb16aecde1587a00efee7db8081b1331ac7bcfb6e61214d266388723a84858d732ad9395028e0aecd2bb8d0c39cc03d14b4c24c22329a0e40c38141eb - languageName: node - linkType: hard - "pony-cause@npm:^1.1.1": version: 1.1.1 resolution: "pony-cause@npm:1.1.1" @@ -41929,15 +41646,6 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^3.1.1": - version: 3.6.2 - resolution: "prettier@npm:3.6.2" - bin: - prettier: bin/prettier.cjs - checksum: 10/1213691706bcef1371d16ef72773c8111106c3533b660b1cc8ec158bd109cdf1462804125f87f981f23c4a3dba053b6efafda30ab0114cc5b4a725606bb9ff26 - languageName: node - linkType: hard - "pretty-bytes@npm:^5.1.0, pretty-bytes@npm:^5.3.0": version: 5.6.0 resolution: "pretty-bytes@npm:5.6.0" @@ -42362,7 +42070,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.10.1, qs@npm:^6.10.3, qs@npm:^6.11.2, qs@npm:^6.12.2, qs@npm:^6.12.3, qs@npm:^6.14.0, qs@npm:^6.7.0, qs@npm:^6.9.4": +"qs@npm:^6.10.1, qs@npm:^6.10.3, qs@npm:^6.11.0, qs@npm:^6.11.2, qs@npm:^6.12.2, qs@npm:^6.12.3, qs@npm:^6.14.0, qs@npm:^6.7.0, qs@npm:^6.9.4": version: 6.14.0 resolution: "qs@npm:6.14.0" dependencies: @@ -42828,21 +42536,21 @@ __metadata: languageName: node linkType: hard -"react-docgen@npm:^7.0.0": - version: 7.1.1 - resolution: "react-docgen@npm:7.1.1" +"react-docgen@npm:^8.0.0": + version: 8.0.1 + resolution: "react-docgen@npm:8.0.1" dependencies: - "@babel/core": "npm:^7.18.9" - "@babel/traverse": "npm:^7.18.9" - "@babel/types": "npm:^7.18.9" - "@types/babel__core": "npm:^7.18.0" - "@types/babel__traverse": "npm:^7.18.0" + "@babel/core": "npm:^7.28.0" + "@babel/traverse": "npm:^7.28.0" + "@babel/types": "npm:^7.28.2" + "@types/babel__core": "npm:^7.20.5" + "@types/babel__traverse": "npm:^7.20.7" "@types/doctrine": "npm:^0.0.9" "@types/resolve": "npm:^1.20.2" doctrine: "npm:^3.0.0" resolve: "npm:^1.22.1" strip-indent: "npm:^4.0.0" - checksum: 10/501e5fa0d00e32ee27559f44462a34e9531018ccb46c51efbe60b98a4c077f43dbe8999da5bb91d2ab45a83a34099436a3b725fdabd3f218dbb4493c0b1c9f95 + checksum: 10/6eb9ec3870c0f527fd05b048a842e2338825d99703312d6f4e84f3ae0e5eda427e6b4c7c07a93b21d8740e2be23e779e0237c4b2783160e285f0121711e2f47c languageName: node linkType: hard @@ -42941,6 +42649,17 @@ __metadata: languageName: node linkType: hard +"react-full-screen@npm:^1.1.1": + version: 1.1.1 + resolution: "react-full-screen@npm:1.1.1" + dependencies: + fscreen: "npm:^1.0.2" + peerDependencies: + react: ">= 16.8.0" + checksum: 10/70ad927b9d6c485ac46b5bb4b1639ef9a860da28290b3a1c419c42b9c427d78b80e8dba403eb6451458af56838012c81d5e12ef05097395f154defc32fe06c34 + languageName: node + linkType: hard + "react-grid-layout@npm:1.3.4": version: 1.3.4 resolution: "react-grid-layout@npm:1.3.4" @@ -44226,6 +43945,13 @@ __metadata: languageName: node linkType: hard +"rettime@npm:^0.7.0": + version: 0.7.0 + resolution: "rettime@npm:0.7.0" + checksum: 10/a8037f2bb4db77ba7a919be008eb8468cd69b23bfc87f5a324919ce441ab5c4560549642e82367bc0cdac01ae2534b680f3abba87e11a13d1123533f17069442 + languageName: node + linkType: hard + "reusify@npm:^1.0.4": version: 1.0.4 resolution: "reusify@npm:1.0.4" @@ -44518,13 +44244,11 @@ __metadata: "@octokit/rest": "npm:^19.0.3" "@playwright/test": "npm:^1.32.3" "@spotify/eslint-plugin": "npm:^15.0.0" - "@storybook/addon-essentials": "npm:^8.6.12" - "@storybook/addon-interactions": "npm:^8.6.12" - "@storybook/addon-links": "npm:^8.6.12" - "@storybook/addon-storysource": "npm:^8.6.12" - "@storybook/addon-themes": "npm:^8.6.12" - "@storybook/react": "npm:^8.6.12" - "@storybook/react-vite": "npm:^8.6.12" + "@storybook/addon-a11y": "npm:^9.1.5" + "@storybook/addon-docs": "npm:^9.1.5" + "@storybook/addon-links": "npm:^9.1.5" + "@storybook/addon-themes": "npm:^9.1.5" + "@storybook/react-vite": "npm:^9.1.5" "@techdocs/cli": "workspace:*" "@types/cacheable-request": "npm:^8.3.6" "@types/global-agent": "npm:^2.1.3" @@ -44551,10 +44275,10 @@ __metadata: shx: "npm:^0.4.0" sloc: "npm:^0.3.1" sort-package-json: "npm:^2.8.0" - storybook: "npm:^8.6.12" + storybook: "npm:^9.1.5" typedoc: "npm:^0.28.0" typescript: "npm:~5.7.0" - vite: "npm:^7.1.2" + vite: "npm:^7.1.5" yaml: "npm:^2.7.0" languageName: unknown linkType: soft @@ -45986,21 +45710,30 @@ __metadata: languageName: node linkType: hard -"storybook@npm:^8.6.12": - version: 8.6.14 - resolution: "storybook@npm:8.6.14" +"storybook@npm:^9.1.5": + version: 9.1.5 + resolution: "storybook@npm:9.1.5" dependencies: - "@storybook/core": "npm:8.6.14" + "@storybook/global": "npm:^5.0.0" + "@testing-library/jest-dom": "npm:^6.6.3" + "@testing-library/user-event": "npm:^14.6.1" + "@vitest/expect": "npm:3.2.4" + "@vitest/mocker": "npm:3.2.4" + "@vitest/spy": "npm:3.2.4" + better-opn: "npm:^3.0.2" + esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0" + esbuild-register: "npm:^3.5.0" + recast: "npm:^0.23.5" + semver: "npm:^7.6.2" + ws: "npm:^8.18.0" peerDependencies: prettier: ^2 || ^3 peerDependenciesMeta: prettier: optional: true bin: - getstorybook: ./bin/index.cjs - sb: ./bin/index.cjs storybook: ./bin/index.cjs - checksum: 10/104932fe29ebf49bef24c90285741cec964d1c36b3f0b38da1dace31ac664be457f2a510e41ba69fe8aa0d90c20e3446fec27d1ced41e5eefb3a9ca713d99e79 + checksum: 10/bf205e9daacbb3570484480a8c3ad0ef36e5cbbecde50126a579678b79194ececd598823e5747ee8a77f6a71ff82a4994f7540ed4d15adae30d66f13928f118c languageName: node linkType: hard @@ -46601,30 +46334,30 @@ __metadata: languageName: node linkType: hard -"superagent@npm:^10.2.3": - version: 10.2.3 - resolution: "superagent@npm:10.2.3" +"superagent@npm:^9.0.1": + version: 9.0.2 + resolution: "superagent@npm:9.0.2" dependencies: - component-emitter: "npm:^1.3.1" + component-emitter: "npm:^1.3.0" cookiejar: "npm:^2.1.4" - debug: "npm:^4.3.7" + debug: "npm:^4.3.4" fast-safe-stringify: "npm:^2.1.1" - form-data: "npm:^4.0.4" - formidable: "npm:^3.5.4" + form-data: "npm:^4.0.0" + formidable: "npm:^3.5.1" methods: "npm:^1.1.2" mime: "npm:2.6.0" - qs: "npm:^6.11.2" - checksum: 10/377bf938e68927dd772169c5285be27872bf6e84fac01c52bcd9396bc5b348c9ded8f8be54649510ec09a67bc5096055847b37cb01b3bca0eb06ff1856170e35 + qs: "npm:^6.11.0" + checksum: 10/d3c0c9051ceec84d5b431eaa410ad81bcd53255cea57af1fc66d683a24c34f3ba4761b411072a9bf489a70e3d5b586a78a0e6f2eac6a561067e7d196ddab0907 languageName: node linkType: hard -"supertest@npm:^7.0.0": - version: 7.1.4 - resolution: "supertest@npm:7.1.4" +"supertest@npm:^7.0.0, supertest@npm:^7.1.0": + version: 7.1.0 + resolution: "supertest@npm:7.1.0" dependencies: methods: "npm:^1.1.2" - superagent: "npm:^10.2.3" - checksum: 10/ecb5d41f2b62b257dbdcabac245c32b8e8fb264fe2636dd85c2c883569d23dc14adc0a471abb84187cbdb49bc36ad870ad355b4a0b85973f510fd57fc229e6cc + superagent: "npm:^9.0.1" + checksum: 10/20069f739a44821dfa4f7f397b9086ef31a358366331138f97945eedb2e231796e7c55b032125d3bd12f9839f089fbb809893dbc0f98edc57e12333b9f42b726 languageName: node linkType: hard @@ -47205,7 +46938,7 @@ __metadata: languageName: node linkType: hard -"tiny-invariant@npm:^1.0.6, tiny-invariant@npm:^1.3.1, tiny-invariant@npm:^1.3.3": +"tiny-invariant@npm:^1.0.6, tiny-invariant@npm:^1.3.3": version: 1.3.3 resolution: "tiny-invariant@npm:1.3.3" checksum: 10/5e185c8cc2266967984ce3b352a4e57cb89dad5a8abb0dea21468a6ecaa67cd5bb47a3b7a85d08041008644af4f667fb8b6575ba38ba5fb00b3b5068306e59fe @@ -47219,13 +46952,13 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.9": - version: 0.2.14 - resolution: "tinyglobby@npm:0.2.14" +"tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.9": + version: 0.2.15 + resolution: "tinyglobby@npm:0.2.15" dependencies: - fdir: "npm:^6.4.4" - picomatch: "npm:^4.0.2" - checksum: 10/3d306d319718b7cc9d79fb3f29d8655237aa6a1f280860a217f93417039d0614891aee6fc47c5db315f4fcc6ac8d55eb8e23e2de73b2c51a431b42456d9e5764 + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.3" + checksum: 10/d72bd826a8b0fa5fa3929e7fe5ba48fceb2ae495df3a231b6c5408cd7d8c00b58ab5a9c2a76ba56a62ee9b5e083626f1f33599734bed1ffc4b792406408f0ca2 languageName: node linkType: hard @@ -47236,13 +46969,6 @@ __metadata: languageName: node linkType: hard -"tinyrainbow@npm:^1.2.0": - version: 1.2.0 - resolution: "tinyrainbow@npm:1.2.0" - checksum: 10/2924444db6804355e5ba2b6e586c7f77329d93abdd7257a069a0f4530dff9f16de484e80479094e3f39273462541b003a65ee3a6afc2d12555aa745132deba5d - languageName: node - linkType: hard - "tinyrainbow@npm:^2.0.0": version: 2.0.0 resolution: "tinyrainbow@npm:2.0.0" @@ -47250,13 +46976,6 @@ __metadata: languageName: node linkType: hard -"tinyspy@npm:^3.0.0": - version: 3.0.2 - resolution: "tinyspy@npm:3.0.2" - checksum: 10/5db671b2ff5cd309de650c8c4761ca945459d7204afb1776db9a04fb4efa28a75f08517a8620c01ee32a577748802231ad92f7d5b194dc003ee7f987a2a06337 - languageName: node - linkType: hard - "tinyspy@npm:^4.0.3": version: 4.0.3 resolution: "tinyspy@npm:4.0.3" @@ -47271,6 +46990,13 @@ __metadata: languageName: node linkType: hard +"tldts-core@npm:^7.0.14": + version: 7.0.14 + resolution: "tldts-core@npm:7.0.14" + checksum: 10/753b573ea972b9da2deb04df2d2fb4631e33b898cb36506bb4ae0dde272d155f92d19aba3011c296544d1548408ec93289e29ad7d57b9f0bc8de339f7b2ddc4b + languageName: node + linkType: hard + "tldts@npm:^6.1.32": version: 6.1.51 resolution: "tldts@npm:6.1.51" @@ -47282,6 +47008,17 @@ __metadata: languageName: node linkType: hard +"tldts@npm:^7.0.5": + version: 7.0.14 + resolution: "tldts@npm:7.0.14" + dependencies: + tldts-core: "npm:^7.0.14" + bin: + tldts: bin/cli.js + checksum: 10/fbee0768cc35446465c4d2e3c166a7a66b89b033f7b3fc8bfd7e1125eb691d243601ff8efae3f581606db13afd74c172d7fea6b7ce69d1a6acd3d0a1789a3c91 + languageName: node + linkType: hard + "tmp-promise@npm:^3.0.2": version: 3.0.3 resolution: "tmp-promise@npm:3.0.3" @@ -47393,7 +47130,7 @@ __metadata: languageName: node linkType: hard -"tough-cookie@npm:^4.0.0, tough-cookie@npm:^4.1.4": +"tough-cookie@npm:^4.0.0": version: 4.1.4 resolution: "tough-cookie@npm:4.1.4" dependencies: @@ -47414,6 +47151,15 @@ __metadata: languageName: node linkType: hard +"tough-cookie@npm:^6.0.0": + version: 6.0.0 + resolution: "tough-cookie@npm:6.0.0" + dependencies: + tldts: "npm:^7.0.5" + checksum: 10/1b0592241655912eb972e1c284ccf975af154576b8e9912cad4ed7b4b408a60ccfdad1bc53eef10d376f6a5ef9d84e2f8ea0b46c92263d52de855247ff100e27 + languageName: node + linkType: hard + "tr46@npm:^3.0.0": version: 3.0.0 resolution: "tr46@npm:3.0.0" @@ -47565,7 +47311,7 @@ __metadata: languageName: node linkType: hard -"ts-dedent@npm:^2.0.0, ts-dedent@npm:^2.2.0": +"ts-dedent@npm:^2.0.0": version: 2.2.0 resolution: "ts-dedent@npm:2.2.0" checksum: 10/93ed8f7878b6d5ed3c08d99b740010eede6bccfe64bce61c5a4da06a2c17d6ddbb80a8c49c2d15251de7594a4f93ffa21dd10e7be75ef66a4dc9951b4a94e2af @@ -49014,17 +48760,17 @@ __metadata: languageName: node linkType: hard -"vite@npm:^7.1.2": - version: 7.1.2 - resolution: "vite@npm:7.1.2" +"vite@npm:^7.1.5": + version: 7.1.5 + resolution: "vite@npm:7.1.5" dependencies: esbuild: "npm:^0.25.0" - fdir: "npm:^6.4.6" + fdir: "npm:^6.5.0" fsevents: "npm:~2.3.3" picomatch: "npm:^4.0.3" postcss: "npm:^8.5.6" rollup: "npm:^4.43.0" - tinyglobby: "npm:^0.2.14" + tinyglobby: "npm:^0.2.15" peerDependencies: "@types/node": ^20.19.0 || >=22.12.0 jiti: ">=1.21.0" @@ -49065,7 +48811,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10/942188896db181bd5f95c576303eb617cd08580eb129d0223bc87a8ed9b8dc0d60d8487c077fd9a01087c55600af132c5fc677e51b4b48a4a54a376056969edb + checksum: 10/59edeef7e98757a668b2ad8a1731a5657fa83e22a165a36b7359225ea98a9be39b2f486710c0cf5085edb85daee7c8b6b6b0bd85d0ef32a1aa84aef71aabd0f0 languageName: node linkType: hard @@ -49734,7 +49480,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:*, ws@npm:^8.12.0, ws@npm:^8.13.0, ws@npm:^8.17.1, ws@npm:^8.18.0, ws@npm:^8.2.3, ws@npm:^8.8.0": +"ws@npm:*, ws@npm:^8.12.0, ws@npm:^8.13.0, ws@npm:^8.17.1, ws@npm:^8.18.0, ws@npm:^8.8.0": version: 8.18.3 resolution: "ws@npm:8.18.3" peerDependencies: @@ -49954,6 +49700,7 @@ __metadata: "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" + "@backstage/errors": "workspace:^" "@backstage/release-manifests": "workspace:^" "@yarnpkg/builder": "npm:^4.2.1" "@yarnpkg/core": "npm:^4.4.1" @@ -50064,6 +49811,13 @@ __metadata: languageName: node linkType: hard +"yocto-queue@npm:^1.0.0": + version: 1.2.1 + resolution: "yocto-queue@npm:1.2.1" + checksum: 10/0843d6c2c0558e5c06e98edf9c17942f25c769e21b519303a5c2adefd5b738c9b2054204dc856ac0cd9d134b1bc27d928ce84fd23c9e2423b7e013d5a6f50577 + languageName: node + linkType: hard + "yoctocolors-cjs@npm:^2.1.2": version: 2.1.2 resolution: "yoctocolors-cjs@npm:2.1.2" @@ -50123,10 +49877,10 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.22.4, zod@npm:^3.23.8": - version: 3.25.76 - resolution: "zod@npm:3.25.76" - checksum: 10/f0c963ec40cd96858451d1690404d603d36507c1fc9682f2dae59ab38b578687d542708a7fdbf645f77926f78c9ed558f57c3d3aa226c285f798df0c4da16995 +"zod@npm:^3.22.4, zod@npm:^3.23.8, zod@npm:^3.24.2": + version: 3.25.67 + resolution: "zod@npm:3.25.67" + checksum: 10/0e35432dcca7f053e63f5dd491a87c78abe0d981817547252c3b6d05f0f58788695d1a69724759c6501dff3fd62929be24c9f314a3625179bee889150f7a61fa languageName: node linkType: hard