diff --git a/.changeset/beige-eagles-cheat.md b/.changeset/beige-eagles-cheat.md new file mode 100644 index 0000000000..5b5081d2ee --- /dev/null +++ b/.changeset/beige-eagles-cheat.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-node': patch +--- + +Updated doc link. diff --git a/.changeset/brown-bananas-talk.md b/.changeset/brown-bananas-talk.md new file mode 100644 index 0000000000..71020b787a --- /dev/null +++ b/.changeset/brown-bananas-talk.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Having tooltip inherit font size for consistency in catalog table columns diff --git a/.changeset/calm-cars-serve.md b/.changeset/calm-cars-serve.md new file mode 100644 index 0000000000..5df2a18cd4 --- /dev/null +++ b/.changeset/calm-cars-serve.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-tasks': patch +--- + +Marked all exports as deprecated and pointed at `@backstage/backend-plugin-api` and `@backstage/backend-defaults` diff --git a/.changeset/chilled-planes-sniff.md b/.changeset/chilled-planes-sniff.md new file mode 100644 index 0000000000..e4084ea561 --- /dev/null +++ b/.changeset/chilled-planes-sniff.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +The `registerComponent` external route will now by default bind to the catalog import page if it is available. diff --git a/.changeset/clever-radios-yawn.md b/.changeset/clever-radios-yawn.md new file mode 100644 index 0000000000..e5600183f5 --- /dev/null +++ b/.changeset/clever-radios-yawn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Make sure that the toggle button state is properly reflected in API cards diff --git a/.changeset/cold-seas-end.md b/.changeset/cold-seas-end.md new file mode 100644 index 0000000000..96fa048fbd --- /dev/null +++ b/.changeset/cold-seas-end.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Updated configuration schema to include the `useRedisSets` cache config option. diff --git a/.changeset/create-app-1716302437.md b/.changeset/create-app-1716302437.md new file mode 100644 index 0000000000..b50d431d4b --- /dev/null +++ b/.changeset/create-app-1716302437.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Bumped create-app version. diff --git a/.changeset/create-app-1716903719.md b/.changeset/create-app-1716903719.md new file mode 100644 index 0000000000..b50d431d4b --- /dev/null +++ b/.changeset/create-app-1716903719.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Bumped create-app version. diff --git a/.changeset/cuddly-eggs-sin.md b/.changeset/cuddly-eggs-sin.md new file mode 100644 index 0000000000..bfe7de9680 --- /dev/null +++ b/.changeset/cuddly-eggs-sin.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-techdocs-node': patch +--- + +Allow defining custom build log transport for techdocs builder diff --git a/.changeset/cuddly-jobs-kick.md b/.changeset/cuddly-jobs-kick.md new file mode 100644 index 0000000000..aa62efb88b --- /dev/null +++ b/.changeset/cuddly-jobs-kick.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-compat-api': patch +--- + +Add support for forwarding default target from legacy external route refs. diff --git a/.changeset/cyan-jobs-visit.md b/.changeset/cyan-jobs-visit.md new file mode 100644 index 0000000000..ccc8c90adf --- /dev/null +++ b/.changeset/cyan-jobs-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': minor +--- + +Added `TestCaches` that functions just like `TestDatabases` diff --git a/.changeset/cyan-paws-beg.md b/.changeset/cyan-paws-beg.md new file mode 100644 index 0000000000..87c2b746a4 --- /dev/null +++ b/.changeset/cyan-paws-beg.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Added handling for dry run to githubPullRequest and githubWebhook and added tests for this functionality diff --git a/.changeset/cyan-snails-peel.md b/.changeset/cyan-snails-peel.md new file mode 100644 index 0000000000..4da79258fb --- /dev/null +++ b/.changeset/cyan-snails-peel.md @@ -0,0 +1,12 @@ +--- +'@backstage/plugin-user-settings-backend': patch +'@backstage/plugin-devtools-backend': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-search-backend': patch +'@backstage/plugin-proxy-backend': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-app-backend': patch +--- + +Updated local development setup. diff --git a/.changeset/eighty-kings-dress.md b/.changeset/eighty-kings-dress.md new file mode 100644 index 0000000000..52aab627ab --- /dev/null +++ b/.changeset/eighty-kings-dress.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +In preparation to the new backend system stable release, the `isDatabaseConflictError` helper have been moved to the `@backstage/backend-plugin-api` package and deprecated from `@backstage/backend-common`. diff --git a/.changeset/eighty-yaks-switch.md b/.changeset/eighty-yaks-switch.md new file mode 100644 index 0000000000..30e3374d70 --- /dev/null +++ b/.changeset/eighty-yaks-switch.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fix readme for new plugins created using cli diff --git a/.changeset/empty-avocados-tease.md b/.changeset/empty-avocados-tease.md new file mode 100644 index 0000000000..f202af2b82 --- /dev/null +++ b/.changeset/empty-avocados-tease.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-techdocs': patch +--- + +Internal updates to allow reusing Backstage's `fetchApi` implementation for event source requests. This allows you to for example, override the `Authorization` header. diff --git a/.changeset/empty-spoons-tell.md b/.changeset/empty-spoons-tell.md new file mode 100644 index 0000000000..44e3dceb6f --- /dev/null +++ b/.changeset/empty-spoons-tell.md @@ -0,0 +1,46 @@ +--- +'@backstage/plugin-proxy-backend': minor +--- + +**BREAKING**: The proxy backend plugin is now protected by Backstage auth, by +default. Unless specifically configured (see below), all proxy endpoints will +reject requests immediately unless a valid Backstage user or service token is +passed along with the request. This aligns the proxy with how other Backstage +backends behave out of the box, and serves to protect your upstreams from +unauthorized access. + +A proxy configuration section can now look as follows: + +```yaml +proxy: + endpoints: + '/pagerduty': + target: https://api.pagerduty.com + credentials: require # NEW! + headers: + Authorization: Token token=${PAGERDUTY_TOKEN} +``` + +There are three possible `credentials` settings at this point: + +- `require`: Callers must provide Backstage user or service credentials with + each request. The credentials are not forwarded to the proxy target. +- `forward`: Callers must provide Backstage user or service credentials with + each request, and those credentials are forwarded to the proxy target. +- `dangerously-allow-unauthenticated`: No Backstage credentials are required to + access this proxy target. The target can still apply its own credentials + checks, but the proxy will not help block non-Backstage-blessed callers. If + you also add `allowedHeaders: ['Authorization']` to an endpoint configuration, + then the Backstage token (if provided) WILL be forwarded. + +The value `dangerously-allow-unauthenticated` was the old default. + +The value `require` is the new default, so requests that were previously +permitted may now start resulting in `401 Unauthorized` responses. If you have +`backend.auth.dangerouslyDisableDefaultAuthPolicy` set to `true`, this does not +apply; the proxy will behave as if all endpoints were set to +`dangerously-allow-unauthenticated`. + +If you have proxy endpoints that require unauthenticated access still, please +add `credentials: dangerously-allow-unauthenticated` to their declarations in +your app-config. diff --git a/.changeset/empty-tables-ring.md b/.changeset/empty-tables-ring.md new file mode 100644 index 0000000000..c83a987228 --- /dev/null +++ b/.changeset/empty-tables-ring.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-kubernetes-react': minor +'@backstage/plugin-catalog-import': minor +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-search': patch +--- + +Migrate from identityApi to fetchApi in frontend plugins. diff --git a/.changeset/famous-monkeys-count.md b/.changeset/famous-monkeys-count.md new file mode 100644 index 0000000000..c5151b38c3 --- /dev/null +++ b/.changeset/famous-monkeys-count.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Add support for JWKS tokens in ExternalTokenHandler. diff --git a/.changeset/forty-adults-roll.md b/.changeset/forty-adults-roll.md new file mode 100644 index 0000000000..50dd60da9e --- /dev/null +++ b/.changeset/forty-adults-roll.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-tasks': patch +--- + +Updated the `TaskScheduleDefinitionConfig` deprecated comment to point to `SchedulerServiceTaskScheduleDefinitionConfig` diff --git a/.changeset/forty-experts-live.md b/.changeset/forty-experts-live.md new file mode 100644 index 0000000000..11fc3b688f --- /dev/null +++ b/.changeset/forty-experts-live.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-graph': patch +--- + +Add function to `EntityRelationsGraph` filter that excludes entities from graph diff --git a/.changeset/friendly-keys-fold.md b/.changeset/friendly-keys-fold.md new file mode 100644 index 0000000000..218abe4efe --- /dev/null +++ b/.changeset/friendly-keys-fold.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-plugin-api': patch +'@backstage/backend-app-api': patch +--- + +Added an optional `accessRestrictions` to external access service tokens and service principals in general, such that you can limit their access to certain plugins or permissions. diff --git a/.changeset/gentle-baboons-peel.md b/.changeset/gentle-baboons-peel.md new file mode 100644 index 0000000000..b51ad77283 --- /dev/null +++ b/.changeset/gentle-baboons-peel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +`TechDocsIndexPage` now accepts an optional `ownerPickerMode` for toggling the behavior of the `EntityOwnerPicker`, exposing a new mode `` particularly suitable for larger catalogs. In this new mode, `EntityOwnerPicker` will display all the users and groups present in the catalog. diff --git a/.changeset/gentle-llamas-cross.md b/.changeset/gentle-llamas-cross.md new file mode 100644 index 0000000000..42a6d70cd6 --- /dev/null +++ b/.changeset/gentle-llamas-cross.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Use `inherit` variant on OverflowTooltip underlying Typography component. diff --git a/.changeset/gold-teachers-wink.md b/.changeset/gold-teachers-wink.md new file mode 100644 index 0000000000..0578e8d02b --- /dev/null +++ b/.changeset/gold-teachers-wink.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fix issue with `esm` loaded dependencies being different from the `cjs` import for Vite dependencies diff --git a/.changeset/great-cougars-guess.md b/.changeset/great-cougars-guess.md new file mode 100644 index 0000000000..af1de3e8ce --- /dev/null +++ b/.changeset/great-cougars-guess.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Made it possible to give access restrictions to `mockCredentials.service` diff --git a/.changeset/green-apricots-invite.md b/.changeset/green-apricots-invite.md new file mode 100644 index 0000000000..b5e92021b4 --- /dev/null +++ b/.changeset/green-apricots-invite.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +The `catalogIndex` external route is now optional and will by default bind to the catalog index page if it is available. diff --git a/.changeset/honest-pandas-chew.md b/.changeset/honest-pandas-chew.md new file mode 100644 index 0000000000..d5ae9e8cea --- /dev/null +++ b/.changeset/honest-pandas-chew.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': patch +--- + +Added a new `defaultTarget` option to `createExternalRouteRef`. I lets you specify a default target of the route by name, for example `'catalog.catalogIndex'`, which will be used if the target route is present in the app and there is no explicit route binding. diff --git a/.changeset/large-months-decide.md b/.changeset/large-months-decide.md new file mode 100644 index 0000000000..e9d4c47bbe --- /dev/null +++ b/.changeset/large-months-decide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-node': minor +--- + +add notifications filtering by processors diff --git a/.changeset/late-ants-impress.md b/.changeset/late-ants-impress.md new file mode 100644 index 0000000000..44b2cbefb3 --- /dev/null +++ b/.changeset/late-ants-impress.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch +'@backstage/plugin-scaffolder-node-test-utils': patch +'@backstage/plugin-search-backend-module-elasticsearch': patch +'@backstage/plugin-search-backend-module-pg': patch +'@backstage/plugin-search-backend-node': patch +'@backstage/plugin-signals-backend': patch +'@backstage/plugin-events-node': patch +--- + +Replace the usage of `getVoidLogger` with `mockServices.logger.mock` in order to remove the dependency with the soon-to-be-deprecated `backend-common` package. diff --git a/.changeset/light-rice-argue.md b/.changeset/light-rice-argue.md new file mode 100644 index 0000000000..23572b4cdd --- /dev/null +++ b/.changeset/light-rice-argue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added `gitlab:issue:edit` action to edit existing GitLab issues diff --git a/.changeset/little-cooks-approve.md b/.changeset/little-cooks-approve.md new file mode 100644 index 0000000000..d4088208ae --- /dev/null +++ b/.changeset/little-cooks-approve.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications': patch +--- + +Fixes performance issue with Notifications title counter. diff --git a/.changeset/lucky-taxis-rule.md b/.changeset/lucky-taxis-rule.md new file mode 100644 index 0000000000..eacbaaefbe --- /dev/null +++ b/.changeset/lucky-taxis-rule.md @@ -0,0 +1,9 @@ +--- +'@backstage/backend-defaults': patch +--- + +Added core service factories and implementations from +`@backstage/backend-app-api`. They are now available as subpath exports, e.g. +`@backstage/backend-defaults/scheduler` is where the service factory and default +implementation of `coreServices.scheduler` now lives. They have been marked as +deprecated in their old locations. diff --git a/.changeset/many-moles-sing.md b/.changeset/many-moles-sing.md new file mode 100644 index 0000000000..8b49f674c7 --- /dev/null +++ b/.changeset/many-moles-sing.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +We are deprecating the legacy `createServiceBuilder` factory, so if you are still using it, please checkout the migration guide and [migrate](https://backstage.io/docs/backend-system/building-plugins-and-modules/migrating) your plugin to use the new backend system. diff --git a/.changeset/many-windows-attack.md b/.changeset/many-windows-attack.md new file mode 100644 index 0000000000..d0516244f4 --- /dev/null +++ b/.changeset/many-windows-attack.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch +--- + +Remove debug console logging statement diff --git a/.changeset/mean-laws-lay.md b/.changeset/mean-laws-lay.md new file mode 100644 index 0000000000..a7c629a9a5 --- /dev/null +++ b/.changeset/mean-laws-lay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Pass through `EventsService` too in the new backend system diff --git a/.changeset/moody-buttons-hope.md b/.changeset/moody-buttons-hope.md new file mode 100644 index 0000000000..0b8f57f9bc --- /dev/null +++ b/.changeset/moody-buttons-hope.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Marked the `TokenManagerService` and `IdentityService` types as deprecated diff --git a/.changeset/neat-rivers-share.md b/.changeset/neat-rivers-share.md new file mode 100644 index 0000000000..42649de977 --- /dev/null +++ b/.changeset/neat-rivers-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +Ensure that service token access restrictions, when present, are taken into account diff --git a/.changeset/nervous-queens-fly.md b/.changeset/nervous-queens-fly.md new file mode 100644 index 0000000000..802034765c --- /dev/null +++ b/.changeset/nervous-queens-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Refactored `TestDatabases` to no longer depend on `backend-common` diff --git a/.changeset/new-numbers-hug.md b/.changeset/new-numbers-hug.md new file mode 100644 index 0000000000..e4e35a2825 --- /dev/null +++ b/.changeset/new-numbers-hug.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Moved the declaration of the `SchedulerService` here, along with prefixed versions of all of the types it depends on, from `@backstage/backend-tasks` diff --git a/.changeset/nice-pants-shave.md b/.changeset/nice-pants-shave.md new file mode 100644 index 0000000000..500888a817 --- /dev/null +++ b/.changeset/nice-pants-shave.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-auth-backend-module-cloudflare-access-provider': patch +'@backstage/plugin-scaffolder-backend-module-sentry': patch +'@backstage/plugin-scaffolder-backend-module-gitea': patch +'@backstage/plugin-notifications-node': patch +--- + +Use `node-fetch` instead of native fetch, as per https://backstage.io/docs/architecture-decisions/adrs-adr013 diff --git a/.changeset/nine-hairs-kick.md b/.changeset/nine-hairs-kick.md new file mode 100644 index 0000000000..e4160640b4 --- /dev/null +++ b/.changeset/nine-hairs-kick.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': patch +--- + +Fixed an issue in `GitlabOrgDiscoveryEntityProvider` where a missing `orgEnabled` config key was throwing an error. diff --git a/.changeset/old-trees-check.md b/.changeset/old-trees-check.md new file mode 100644 index 0000000000..b4d745ffb5 --- /dev/null +++ b/.changeset/old-trees-check.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-tasks': patch +--- + +Deprecate the legacy `TaskScheduler.fromConfig` method and stop using the `getVoidlogger` in tests files to reduce the dependency on the soon-to-deprecate `backstage-common` package. diff --git a/.changeset/olive-mangos-tickle.md b/.changeset/olive-mangos-tickle.md new file mode 100644 index 0000000000..6033ec3ed6 --- /dev/null +++ b/.changeset/olive-mangos-tickle.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Stop using `getVoidLogger` in tests to reduce the dependency on the soon-to-deprecate `backstage-common` package. diff --git a/.changeset/orange-chefs-end.md b/.changeset/orange-chefs-end.md new file mode 100644 index 0000000000..9ba2a29685 --- /dev/null +++ b/.changeset/orange-chefs-end.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-oidc-provider': patch +--- + +if oidc server do not provide revocation_endpoint,we should not call revoke function diff --git a/.changeset/orange-pianos-eat.md b/.changeset/orange-pianos-eat.md new file mode 100644 index 0000000000..5e085a612b --- /dev/null +++ b/.changeset/orange-pianos-eat.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-common': minor +'@backstage/integration': minor +--- + +Implemented `readTree` for Harness provider to support TechDocs functionality diff --git a/.changeset/perfect-bikes-invite.md b/.changeset/perfect-bikes-invite.md new file mode 100644 index 0000000000..96a4ad5659 --- /dev/null +++ b/.changeset/perfect-bikes-invite.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': patch +--- + +Fixed an issue in `GitlabDiscoveryEntityProvider` where the fallback branch was taking precedence over the GitLab default branch. diff --git a/.changeset/polite-otters-talk.md b/.changeset/polite-otters-talk.md new file mode 100644 index 0000000000..e56b935c1e --- /dev/null +++ b/.changeset/polite-otters-talk.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-email': minor +--- + +add notification filters diff --git a/.changeset/popular-carrots-love.md b/.changeset/popular-carrots-love.md new file mode 100644 index 0000000000..22490e0b1a --- /dev/null +++ b/.changeset/popular-carrots-love.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Fixed an issue causing `SidebarSubmenu` text to not follow the theme color diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000..ce72063500 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,281 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "example-app": "0.2.97", + "@backstage/app-defaults": "1.5.5", + "example-app-next": "0.0.11", + "app-next-example-plugin": "0.0.11", + "example-backend": "0.0.26", + "@backstage/backend-app-api": "0.7.3", + "@backstage/backend-common": "0.22.0", + "@backstage/backend-defaults": "0.2.18", + "@backstage/backend-dev-utils": "0.1.4", + "@backstage/backend-dynamic-feature-service": "0.2.10", + "example-backend-legacy": "0.2.98", + "@backstage/backend-openapi-utils": "0.1.11", + "@backstage/backend-plugin-api": "0.6.18", + "@backstage/backend-tasks": "0.5.23", + "@backstage/backend-test-utils": "0.3.8", + "@backstage/catalog-client": "1.6.5", + "@backstage/catalog-model": "1.5.0", + "@backstage/cli": "0.26.5", + "@backstage/cli-common": "0.1.13", + "@backstage/cli-node": "0.2.5", + "@backstage/codemods": "0.1.48", + "@backstage/config": "1.2.0", + "@backstage/config-loader": "1.8.0", + "@backstage/core-app-api": "1.12.5", + "@backstage/core-compat-api": "0.2.5", + "@backstage/core-components": "0.14.7", + "@backstage/core-plugin-api": "1.9.2", + "@backstage/create-app": "0.5.15", + "@backstage/dev-utils": "1.0.32", + "e2e-test": "0.2.16", + "@backstage/e2e-test-utils": "0.1.1", + "@backstage/errors": "1.2.4", + "@backstage/eslint-plugin": "0.1.8", + "@backstage/frontend-app-api": "0.7.0", + "@backstage/frontend-plugin-api": "0.6.5", + "@backstage/frontend-test-utils": "0.1.7", + "@backstage/integration": "1.11.0", + "@backstage/integration-aws-node": "0.1.12", + "@backstage/integration-react": "1.1.27", + "@backstage/release-manifests": "0.0.11", + "@backstage/repo-tools": "0.9.0", + "@techdocs/cli": "1.8.11", + "techdocs-cli-embedded-app": "0.2.96", + "@backstage/test-utils": "1.5.5", + "@backstage/theme": "0.5.4", + "@backstage/types": "1.1.1", + "@backstage/version-bridge": "1.0.8", + "@backstage/plugin-api-docs": "0.11.5", + "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.6", + "@backstage/plugin-app-backend": "0.3.66", + "@backstage/plugin-app-node": "0.1.18", + "@backstage/plugin-app-visualizer": "0.1.6", + "@backstage/plugin-auth-backend": "0.22.5", + "@backstage/plugin-auth-backend-module-atlassian-provider": "0.1.10", + "@backstage/plugin-auth-backend-module-aws-alb-provider": "0.1.10", + "@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.1.1", + "@backstage/plugin-auth-backend-module-bitbucket-provider": "0.1.1", + "@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.1.1", + "@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.2.13", + "@backstage/plugin-auth-backend-module-github-provider": "0.1.15", + "@backstage/plugin-auth-backend-module-gitlab-provider": "0.1.15", + "@backstage/plugin-auth-backend-module-google-provider": "0.1.15", + "@backstage/plugin-auth-backend-module-guest-provider": "0.1.4", + "@backstage/plugin-auth-backend-module-microsoft-provider": "0.1.13", + "@backstage/plugin-auth-backend-module-oauth2-provider": "0.1.15", + "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.1.11", + "@backstage/plugin-auth-backend-module-oidc-provider": "0.1.9", + "@backstage/plugin-auth-backend-module-okta-provider": "0.0.11", + "@backstage/plugin-auth-backend-module-pinniped-provider": "0.1.12", + "@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.1.10", + "@backstage/plugin-auth-node": "0.4.13", + "@backstage/plugin-auth-react": "0.1.2", + "@backstage/plugin-bitbucket-cloud-common": "0.2.19", + "@backstage/plugin-catalog": "1.20.0", + "@backstage/plugin-catalog-backend": "1.22.0", + "@backstage/plugin-catalog-backend-module-aws": "0.3.13", + "@backstage/plugin-catalog-backend-module-azure": "0.1.38", + "@backstage/plugin-catalog-backend-module-backstage-openapi": "0.2.1", + "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.2.5", + "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.1.32", + "@backstage/plugin-catalog-backend-module-gcp": "0.1.19", + "@backstage/plugin-catalog-backend-module-gerrit": "0.1.35", + "@backstage/plugin-catalog-backend-module-github": "0.6.1", + "@backstage/plugin-catalog-backend-module-github-org": "0.1.13", + "@backstage/plugin-catalog-backend-module-gitlab": "0.3.16", + "@backstage/plugin-catalog-backend-module-gitlab-org": "0.0.1", + "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.4.23", + "@backstage/plugin-catalog-backend-module-ldap": "0.5.34", + "@backstage/plugin-catalog-backend-module-msgraph": "0.5.26", + "@backstage/plugin-catalog-backend-module-openapi": "0.1.36", + "@backstage/plugin-catalog-backend-module-puppetdb": "0.1.24", + "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.1.16", + "@backstage/plugin-catalog-backend-module-unprocessed": "0.4.5", + "@backstage/plugin-catalog-common": "1.0.23", + "@backstage/plugin-catalog-graph": "0.4.5", + "@backstage/plugin-catalog-import": "0.11.0", + "@backstage/plugin-catalog-node": "1.12.0", + "@backstage/plugin-catalog-react": "1.12.0", + "@backstage/plugin-catalog-unprocessed-entities": "0.2.4", + "@backstage/plugin-catalog-unprocessed-entities-common": "0.0.1", + "@backstage/plugin-config-schema": "0.1.55", + "@backstage/plugin-devtools": "0.1.14", + "@backstage/plugin-devtools-backend": "0.3.4", + "@backstage/plugin-devtools-common": "0.1.9", + "@backstage/plugin-events-backend": "0.3.5", + "@backstage/plugin-events-backend-module-aws-sqs": "0.3.4", + "@backstage/plugin-events-backend-module-azure": "0.2.4", + "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.4", + "@backstage/plugin-events-backend-module-gerrit": "0.2.4", + "@backstage/plugin-events-backend-module-github": "0.2.4", + "@backstage/plugin-events-backend-module-gitlab": "0.2.4", + "@backstage/plugin-events-backend-test-utils": "0.1.28", + "@backstage/plugin-events-node": "0.3.4", + "@internal/plugin-todo-list": "1.0.27", + "@internal/plugin-todo-list-backend": "1.0.27", + "@internal/plugin-todo-list-common": "1.0.18", + "@backstage/plugin-home": "0.7.4", + "@backstage/plugin-home-react": "0.1.13", + "@backstage/plugin-kubernetes": "0.11.10", + "@backstage/plugin-kubernetes-backend": "0.17.1", + "@backstage/plugin-kubernetes-cluster": "0.0.11", + "@backstage/plugin-kubernetes-common": "0.7.6", + "@backstage/plugin-kubernetes-node": "0.1.12", + "@backstage/plugin-kubernetes-react": "0.3.5", + "@backstage/plugin-notifications": "0.2.1", + "@backstage/plugin-notifications-backend": "0.2.1", + "@backstage/plugin-notifications-backend-module-email": "0.0.1", + "@backstage/plugin-notifications-common": "0.0.3", + "@backstage/plugin-notifications-node": "0.1.4", + "@backstage/plugin-org": "0.6.25", + "@backstage/plugin-org-react": "0.1.24", + "@backstage/plugin-permission-backend": "0.5.42", + "@backstage/plugin-permission-backend-module-allow-all-policy": "0.1.15", + "@backstage/plugin-permission-common": "0.7.13", + "@backstage/plugin-permission-node": "0.7.29", + "@backstage/plugin-permission-react": "0.4.22", + "@backstage/plugin-proxy-backend": "0.4.16", + "@backstage/plugin-scaffolder": "1.20.0", + "@backstage/plugin-scaffolder-backend": "1.22.6", + "@backstage/plugin-scaffolder-backend-module-azure": "0.1.10", + "@backstage/plugin-scaffolder-backend-module-bitbucket": "0.2.8", + "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.1.8", + "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.1.8", + "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.2.19", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.42", + "@backstage/plugin-scaffolder-backend-module-gerrit": "0.1.10", + "@backstage/plugin-scaffolder-backend-module-gitea": "0.1.8", + "@backstage/plugin-scaffolder-backend-module-github": "0.2.8", + "@backstage/plugin-scaffolder-backend-module-gitlab": "0.4.0", + "@backstage/plugin-scaffolder-backend-module-notifications": "0.0.1", + "@backstage/plugin-scaffolder-backend-module-rails": "0.4.35", + "@backstage/plugin-scaffolder-backend-module-sentry": "0.1.26", + "@backstage/plugin-scaffolder-backend-module-yeoman": "0.3.1", + "@backstage/plugin-scaffolder-common": "1.5.2", + "@backstage/plugin-scaffolder-node": "0.4.4", + "@backstage/plugin-scaffolder-node-test-utils": "0.1.4", + "@backstage/plugin-scaffolder-react": "1.8.5", + "@backstage/plugin-search": "1.4.11", + "@backstage/plugin-search-backend": "1.5.8", + "@backstage/plugin-search-backend-module-catalog": "0.1.24", + "@backstage/plugin-search-backend-module-elasticsearch": "1.4.1", + "@backstage/plugin-search-backend-module-explore": "0.1.24", + "@backstage/plugin-search-backend-module-pg": "0.5.27", + "@backstage/plugin-search-backend-module-stack-overflow-collator": "0.1.11", + "@backstage/plugin-search-backend-module-techdocs": "0.1.23", + "@backstage/plugin-search-backend-node": "1.2.22", + "@backstage/plugin-search-common": "1.2.11", + "@backstage/plugin-search-react": "1.7.11", + "@backstage/plugin-signals": "0.0.6", + "@backstage/plugin-signals-backend": "0.1.4", + "@backstage/plugin-signals-node": "0.1.4", + "@backstage/plugin-signals-react": "0.0.3", + "@backstage/plugin-techdocs": "1.10.5", + "@backstage/plugin-techdocs-addons-test-utils": "1.0.32", + "@backstage/plugin-techdocs-backend": "1.10.5", + "@backstage/plugin-techdocs-module-addons-contrib": "1.1.10", + "@backstage/plugin-techdocs-node": "1.12.4", + "@backstage/plugin-techdocs-react": "1.2.4", + "@backstage/plugin-user-settings": "0.8.6", + "@backstage/plugin-user-settings-backend": "0.2.17" + }, + "changesets": [ + "beige-eagles-cheat", + "brave-apples-move", + "breezy-badgers-train", + "brown-bananas-talk", + "calm-cars-serve", + "calm-plums-wink", + "chilled-planes-sniff", + "clever-radios-yawn", + "cold-seas-end", + "create-app-1716302437", + "create-app-1716903719", + "cuddly-eggs-sin", + "cuddly-jobs-kick", + "cyan-jobs-visit", + "cyan-paws-beg", + "cyan-snails-peel", + "eighty-kings-dress", + "eighty-yaks-switch", + "empty-avocados-tease", + "empty-spoons-tell", + "empty-tables-ring", + "famous-monkeys-count", + "forty-adults-roll", + "four-adults-mix", + "friendly-keys-fold", + "gentle-baboons-peel", + "gold-teachers-wink", + "great-cougars-guess", + "green-apricots-invite", + "honest-pandas-chew", + "itchy-spoons-cry", + "large-months-decide", + "late-ants-impress", + "late-students-live", + "light-rice-argue", + "little-cooks-approve", + "loud-pumpkins-bow", + "lovely-hats-pay", + "lucky-taxis-rule", + "many-moles-sing", + "many-windows-attack", + "mean-laws-lay", + "moody-buttons-hope", + "neat-rivers-share", + "nervous-queens-fly", + "new-numbers-hug", + "nice-pants-shave", + "nine-hairs-kick", + "nine-ties-type", + "old-trees-check", + "olive-mangos-tickle", + "orange-chefs-end", + "orange-pianos-eat", + "perfect-bikes-invite", + "polite-otters-talk", + "popular-carrots-love", + "pretty-berries-live", + "proud-readers-move", + "real-ghosts-return", + "rich-teachers-agree", + "rude-kings-press", + "selfish-pugs-lick", + "serious-yaks-sit", + "seven-geese-raise", + "shaggy-jokes-promise", + "six-llamas-give", + "slimy-fans-raise", + "smooth-gifts-nail", + "soft-flies-live", + "sour-colts-juggle", + "spicy-brooms-hang", + "spicy-camels-happen", + "spotty-plants-switch", + "strong-moose-work", + "stupid-tigers-bake", + "swift-islands-leave", + "tall-lies-fetch", + "tall-pumas-teach", + "tender-seas-listen", + "thirty-plums-shout", + "tiny-pandas-return", + "twenty-masks-dance", + "warm-bees-hope", + "warm-mayflies-yell", + "weak-gifts-occur", + "wet-crabs-guess", + "wild-doors-cheat", + "wild-ears-walk", + "wise-beers-doubt", + "wise-vans-sin", + "wise-wasps-look", + "young-apes-thank", + "young-camels-return" + ] +} diff --git a/.changeset/pretty-berries-live.md b/.changeset/pretty-berries-live.md new file mode 100644 index 0000000000..e83ebeef13 --- /dev/null +++ b/.changeset/pretty-berries-live.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-defaults': patch +'@backstage/backend-common': patch +--- + +Deprecated `dropDatabase` diff --git a/.changeset/pretty-spoons-knock.md b/.changeset/pretty-spoons-knock.md new file mode 100644 index 0000000000..5eddce54d9 --- /dev/null +++ b/.changeset/pretty-spoons-knock.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-catalog-react': patch +--- + +Prevents Autocomplete dropdown from overlapping sidebar on hovering it diff --git a/.changeset/proud-readers-move.md b/.changeset/proud-readers-move.md new file mode 100644 index 0000000000..70d94b2e6d --- /dev/null +++ b/.changeset/proud-readers-move.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-graph': patch +--- + +The `catalogEntity` external route will now by default bind to the catalog entity page if it is available. diff --git a/.changeset/purple-mugs-beg.md b/.changeset/purple-mugs-beg.md new file mode 100644 index 0000000000..2507b5d706 --- /dev/null +++ b/.changeset/purple-mugs-beg.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-common': patch +--- + +The monorepo root check in `findPaths` will now accept a shorthand `workspaces` config in `package.json`, no longer requiring `workspaces.packages`. diff --git a/.changeset/real-ghosts-return.md b/.changeset/real-ghosts-return.md new file mode 100644 index 0000000000..9f81649254 --- /dev/null +++ b/.changeset/real-ghosts-return.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Bumped TypeScript to version `5.4`. diff --git a/.changeset/rich-teachers-agree.md b/.changeset/rich-teachers-agree.md new file mode 100644 index 0000000000..eed8517619 --- /dev/null +++ b/.changeset/rich-teachers-agree.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-catalog': minor +--- + +Added the following default targets for external routes: + +- `createComponent` binds to the Scaffolder page. +- `viewTechDoc` binds to the TechDocs entity documentation page. +- `createFromTemplate` binds to the Scaffolder selected template page. diff --git a/.changeset/rude-kings-press.md b/.changeset/rude-kings-press.md new file mode 100644 index 0000000000..5c10753f8a --- /dev/null +++ b/.changeset/rude-kings-press.md @@ -0,0 +1,8 @@ +--- +'@backstage/backend-app-api': patch +--- + +Deprecated core service factories and implementations and moved them over to +subpath exports on `@backstage/backend-defaults` instead. E.g. +`@backstage/backend-defaults/scheduler` is where the service factory and default +implementation of `coreServices.scheduler` now lives. diff --git a/.changeset/selfish-pugs-lick.md b/.changeset/selfish-pugs-lick.md new file mode 100644 index 0000000000..1ac7ac7321 --- /dev/null +++ b/.changeset/selfish-pugs-lick.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +Import `tokenManager` definition from `@backstage/backend-plugin-api` diff --git a/.changeset/serious-yaks-sit.md b/.changeset/serious-yaks-sit.md new file mode 100644 index 0000000000..5afc74f955 --- /dev/null +++ b/.changeset/serious-yaks-sit.md @@ -0,0 +1,29 @@ +--- +'@backstage/core-app-api': patch +--- + +Added support for configuration of route bindings through static configuration, and default targets for external route refs. + +In addition to configuring route bindings through code, it is now also possible to configure route bindings under the `app.routes.bindings` key, for example: + +```yaml +app: + routes: + bindings: + catalog.createComponent: catalog-import.importPage +``` + +Each key in the route binding object is of the form `.`, where the route name is key used in the `externalRoutes` object passed to `createPlugin`. The value is of the same form, but with the name taken from the plugin `routes` option instead. + +The equivalent of the above configuration in code is the following: + +```ts +const app = createApp({ + // ... + bindRoutes({ bind }) { + bind(catalogPlugin.externalRoutes, { + createComponent: catalogImportPlugin.routes.importPage, + }); + }, +}); +``` diff --git a/.changeset/shaggy-jokes-promise.md b/.changeset/shaggy-jokes-promise.md new file mode 100644 index 0000000000..e7788b69f4 --- /dev/null +++ b/.changeset/shaggy-jokes-promise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend': patch +--- + +Move @backstage/repo-tools to devDependencies diff --git a/.changeset/six-llamas-give.md b/.changeset/six-llamas-give.md new file mode 100644 index 0000000000..9bfbf52625 --- /dev/null +++ b/.changeset/six-llamas-give.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-defaults': minor +'@backstage/backend-common': minor +--- + +Deprecated and moved over core services to `@backstage/backend-defaults` diff --git a/.changeset/smooth-gifts-nail.md b/.changeset/smooth-gifts-nail.md new file mode 100644 index 0000000000..aa2585bc4f --- /dev/null +++ b/.changeset/smooth-gifts-nail.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +'@backstage/backend-app-api': patch +--- + +Updating the logger redaction message to something less dramatic diff --git a/.changeset/soft-flies-live.md b/.changeset/soft-flies-live.md new file mode 100644 index 0000000000..b331269647 --- /dev/null +++ b/.changeset/soft-flies-live.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Add new `gitlab:pipeline:trigger` action to trigger GitLab pipelines. diff --git a/.changeset/sour-colts-juggle.md b/.changeset/sour-colts-juggle.md new file mode 100644 index 0000000000..35dc79452a --- /dev/null +++ b/.changeset/sour-colts-juggle.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Fix the logger service mock to prevent returning `undefined` from the `child` method. diff --git a/.changeset/spicy-brooms-hang.md b/.changeset/spicy-brooms-hang.md new file mode 100644 index 0000000000..23f7da309b --- /dev/null +++ b/.changeset/spicy-brooms-hang.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-scaffolder': patch +--- + +Fixing bug in `formData` type as it should be `optional` as it's possibly undefined diff --git a/.changeset/spicy-camels-happen.md b/.changeset/spicy-camels-happen.md new file mode 100644 index 0000000000..1017f042ad --- /dev/null +++ b/.changeset/spicy-camels-happen.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +We are deprecating the legacy router handlers and contexts in preparation for the new backend system stable release. diff --git a/.changeset/spotty-plants-switch.md b/.changeset/spotty-plants-switch.md new file mode 100644 index 0000000000..a5951de05d --- /dev/null +++ b/.changeset/spotty-plants-switch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-ldap': minor +--- + +Migrate LDAP catalog module to the new backend system. diff --git a/.changeset/strong-moose-work.md b/.changeset/strong-moose-work.md new file mode 100644 index 0000000000..443b708e51 --- /dev/null +++ b/.changeset/strong-moose-work.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Add `--client-additional-properties` option to `openapi generate` command diff --git a/.changeset/stupid-tigers-bake.md b/.changeset/stupid-tigers-bake.md new file mode 100644 index 0000000000..a4579ab05a --- /dev/null +++ b/.changeset/stupid-tigers-bake.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-kubernetes-backend': minor +'@backstage/plugin-kubernetes-common': minor +'@backstage/plugin-kubernetes-react': minor +--- + +Update kubernetes plugins to use autoscaling/v2 diff --git a/.changeset/swift-islands-leave.md b/.changeset/swift-islands-leave.md new file mode 100644 index 0000000000..3b0b460733 --- /dev/null +++ b/.changeset/swift-islands-leave.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Removed max width from `Select` component. diff --git a/.changeset/tall-bananas-reflect.md b/.changeset/tall-bananas-reflect.md new file mode 100644 index 0000000000..00e8fd8b43 --- /dev/null +++ b/.changeset/tall-bananas-reflect.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Remove Tech Radar menu item from sidebar of scaffolded app to align with removal of tech-radar plugin from backend diff --git a/.changeset/tall-lies-fetch.md b/.changeset/tall-lies-fetch.md new file mode 100644 index 0000000000..a4484a5687 --- /dev/null +++ b/.changeset/tall-lies-fetch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Export `catalogTranslationRef` under `/alpha` diff --git a/.changeset/tall-pumas-teach.md b/.changeset/tall-pumas-teach.md new file mode 100644 index 0000000000..39e7c06297 --- /dev/null +++ b/.changeset/tall-pumas-teach.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Fixed a typo '$' in the review step label diff --git a/.changeset/tender-seas-listen.md b/.changeset/tender-seas-listen.md new file mode 100644 index 0000000000..86b0bf4618 --- /dev/null +++ b/.changeset/tender-seas-listen.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-catalog': patch +--- + +updated the ContextMenu, ActionsPage, OngoingTask and TemplateCard frontend components to support the new scaffolder permissions: + +- `scaffolder.task.create` +- `scaffolder.task.cancel` +- `scaffolder.task.read` diff --git a/.changeset/thirty-plums-shout.md b/.changeset/thirty-plums-shout.md new file mode 100644 index 0000000000..56ac640326 --- /dev/null +++ b/.changeset/thirty-plums-shout.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Added a regex test to check commit hash. If url is from git commit branch ignore the edit url. diff --git a/.changeset/tiny-pandas-return.md b/.changeset/tiny-pandas-return.md new file mode 100644 index 0000000000..cc9210a368 --- /dev/null +++ b/.changeset/tiny-pandas-return.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +`DefaultApiExplorerPage` now accepts an optional `ownerPickerMode` for toggling the behavior of the `EntityOwnerPicker`, exposing a new mode `` particularly suitable for larger catalogs. In this new mode, `EntityOwnerPicker` will display all the users and groups present in the catalog. diff --git a/.changeset/twenty-masks-dance.md b/.changeset/twenty-masks-dance.md new file mode 100644 index 0000000000..f08e6301c9 --- /dev/null +++ b/.changeset/twenty-masks-dance.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Fixed a bug in `SupportButton` where the title was rendered with the characteristics of a button. diff --git a/.changeset/warm-bees-hope.md b/.changeset/warm-bees-hope.md new file mode 100644 index 0000000000..2a3de7fda7 --- /dev/null +++ b/.changeset/warm-bees-hope.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Renamed `BackendPluginConfig`, `BackendModuleConfig`, and `ExtensionPointConfig` respectively to `CreateBackendPluginOptions`, `CreateBackendModuleOptions`, and `CreateExtensionPointOptions` to standardize frontend and backend factories signatures. diff --git a/.changeset/warm-mayflies-yell.md b/.changeset/warm-mayflies-yell.md new file mode 100644 index 0000000000..a2068668f7 --- /dev/null +++ b/.changeset/warm-mayflies-yell.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Added the following default targets for external routes: + +- `registerComponent` binds to the catalog import page. +- `viewTechDoc` binds to the TechDocs entity documentation page. diff --git a/.changeset/weak-gifts-occur.md b/.changeset/weak-gifts-occur.md new file mode 100644 index 0000000000..7834c0a9d9 --- /dev/null +++ b/.changeset/weak-gifts-occur.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-common': patch +--- + +added the following new permissions to the scaffolder backend endpoints: + +- `scaffolder.task.create` +- `scaffolder.task.cancel` +- `scaffolder.task.read` diff --git a/.changeset/wet-crabs-guess.md b/.changeset/wet-crabs-guess.md new file mode 100644 index 0000000000..c8a9282a47 --- /dev/null +++ b/.changeset/wet-crabs-guess.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-plugin-api': patch +'@backstage/plugin-catalog-backend': patch +--- + +Start using the `isDatabaseConflictError` helper from the `@backstage/backend-plugin-api` package in order to avoid dependency with the soon to deprecate `@backstage/backend-common` package. diff --git a/.changeset/wild-ears-walk.md b/.changeset/wild-ears-walk.md new file mode 100644 index 0000000000..072c545f1a --- /dev/null +++ b/.changeset/wild-ears-walk.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend': minor +--- + +adding filtering of notifications by processors diff --git a/.changeset/wise-beers-doubt.md b/.changeset/wise-beers-doubt.md new file mode 100644 index 0000000000..be70f326e0 --- /dev/null +++ b/.changeset/wise-beers-doubt.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Updated `node-gyp` to v10 diff --git a/.changeset/wise-vans-sin.md b/.changeset/wise-vans-sin.md new file mode 100644 index 0000000000..37565c9300 --- /dev/null +++ b/.changeset/wise-vans-sin.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Deprecate legacy service logger helpers and stop using `getVoidLogger` in tests. diff --git a/.changeset/wise-wasps-look.md b/.changeset/wise-wasps-look.md new file mode 100644 index 0000000000..34f52b3a7b --- /dev/null +++ b/.changeset/wise-wasps-look.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Change owner to project for azure host diff --git a/.changeset/young-apes-thank.md b/.changeset/young-apes-thank.md new file mode 100644 index 0000000000..570316b2e2 --- /dev/null +++ b/.changeset/young-apes-thank.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': minor +--- + +Allow empty author info in createPullRequest action for Github diff --git a/.github/uffizzi/k8s/manifests/backstage-crb.yaml b/.github/uffizzi/k8s/manifests/backstage-crb.yaml deleted file mode 100644 index a5dad3408a..0000000000 --- a/.github/uffizzi/k8s/manifests/backstage-crb.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: backstage-cluster-ro -subjects: - - namespace: backstage - kind: ServiceAccount - name: backstage-service-account -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: system:aggregate-to-view diff --git a/.github/uffizzi/k8s/manifests/backstage-deploy.yaml b/.github/uffizzi/k8s/manifests/backstage-deploy.yaml deleted file mode 100644 index 2c1458cfea..0000000000 --- a/.github/uffizzi/k8s/manifests/backstage-deploy.yaml +++ /dev/null @@ -1,44 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: backstage -spec: - replicas: 1 - selector: - matchLabels: - app: backstage - template: - metadata: - labels: - app: backstage - spec: - serviceAccountName: backstage-service-account - containers: - - name: backstage - image: backstage - command: - - /bin/sh - args: - - '-c' - - "APP_CONFIG_app_baseUrl=$$UFFIZZI_URL APP_CONFIG_backend_baseUrl=$$UFFIZZI_URL APP_CONFIG_auth_environment='production' node packages/backend --config app-config.yaml" - imagePullPolicy: IfNotPresent - ports: - - name: http - containerPort: 7007 - envFrom: - - secretRef: - name: postgres-secrets - env: - - name: POSTGRES_PORT - value: '5432' - - name: POSTGRES_HOST - value: 'postgres.default.svc.cluster.local' - - name: NODE_ENV - value: production - resources: - limits: - cpu: 1000m - memory: 384Mi - requests: - cpu: 20m - memory: 160Mi diff --git a/.github/uffizzi/k8s/manifests/backstage-ingress.yaml b/.github/uffizzi/k8s/manifests/backstage-ingress.yaml deleted file mode 100644 index 8a7f58c524..0000000000 --- a/.github/uffizzi/k8s/manifests/backstage-ingress.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: backstage -spec: - ingressClassName: uffizzi - rules: - - host: backstage.example.com - http: - paths: - - backend: - service: - name: backstage - port: - number: 80 - path: / - pathType: Prefix - tls: - - hosts: - - backstage.example.com diff --git a/.github/uffizzi/k8s/manifests/backstage-sa.yaml b/.github/uffizzi/k8s/manifests/backstage-sa.yaml deleted file mode 100644 index db0b5bde38..0000000000 --- a/.github/uffizzi/k8s/manifests/backstage-sa.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: backstage-service-account - namespace: default diff --git a/.github/uffizzi/k8s/manifests/backstage-svc.yaml b/.github/uffizzi/k8s/manifests/backstage-svc.yaml deleted file mode 100644 index 1f4abc79b4..0000000000 --- a/.github/uffizzi/k8s/manifests/backstage-svc.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: backstage -spec: - selector: - app: backstage - ports: - - name: http - port: 80 - targetPort: http diff --git a/.github/uffizzi/k8s/manifests/kustomization.yaml b/.github/uffizzi/k8s/manifests/kustomization.yaml deleted file mode 100644 index d4db0e97b2..0000000000 --- a/.github/uffizzi/k8s/manifests/kustomization.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - backstage-deploy.yaml - - backstage-crb.yaml - - backstage-ingress.yaml - - backstage-sa.yaml - - backstage-svc.yaml - - pg-svc.yaml - - pg-deploy.yaml - - pg-secret.yaml - - pg-volume.yaml diff --git a/.github/uffizzi/k8s/manifests/pg-deploy.yaml b/.github/uffizzi/k8s/manifests/pg-deploy.yaml deleted file mode 100644 index 950e64283f..0000000000 --- a/.github/uffizzi/k8s/manifests/pg-deploy.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# kubernetes/postgres.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: postgres -spec: - replicas: 1 - selector: - matchLabels: - app: postgres - template: - metadata: - labels: - app: postgres - spec: - containers: - - name: postgres - image: postgres:12-alpine - imagePullPolicy: 'IfNotPresent' - ports: - - containerPort: 5432 - env: - - name: PGDATA - value: '/data/pgdata' - envFrom: - - secretRef: - name: postgres-secrets - resources: - limits: - cpu: 1000m - memory: 256Mi - requests: - cpu: 10m - memory: 96Mi - volumeMounts: - - mountPath: /data - name: postgresdb - volumes: - - name: postgresdb - persistentVolumeClaim: - claimName: postgres-storage-claim diff --git a/.github/uffizzi/k8s/manifests/pg-secret.yaml b/.github/uffizzi/k8s/manifests/pg-secret.yaml deleted file mode 100644 index 28a31f7c53..0000000000 --- a/.github/uffizzi/k8s/manifests/pg-secret.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: postgres-secrets -type: Opaque -data: - POSTGRES_USER: YmFja3N0YWdl - POSTGRES_PASSWORD: aHVudGVyMg== diff --git a/.github/uffizzi/k8s/manifests/pg-svc.yaml b/.github/uffizzi/k8s/manifests/pg-svc.yaml deleted file mode 100644 index ab7e192a65..0000000000 --- a/.github/uffizzi/k8s/manifests/pg-svc.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: postgres -spec: - selector: - app: postgres - ports: - - port: 5432 diff --git a/.github/uffizzi/k8s/manifests/pg-volume.yaml b/.github/uffizzi/k8s/manifests/pg-volume.yaml deleted file mode 100644 index 0dcb317e50..0000000000 --- a/.github/uffizzi/k8s/manifests/pg-volume.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: postgres-storage-claim -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 2G diff --git a/.github/uffizzi/uffizzi.production.app-config.yaml b/.github/uffizzi/uffizzi.production.app-config.yaml deleted file mode 100644 index 53944256e1..0000000000 --- a/.github/uffizzi/uffizzi.production.app-config.yaml +++ /dev/null @@ -1,297 +0,0 @@ -app: - title: Backstage Uffizzi Environment - baseUrl: ${UFFIZZI_URL} - -organization: - name: My Company - -backend: - baseUrl: ${UFFIZZI_URL} - auth: - keys: - # random mock key for Uffizzi deployments - - secret: 5TXvdjVZFxF7qf9K5RAYRDoGrLzJooqa - listen: - port: 7007 - database: - client: pg - connection: - host: ${POSTGRES_HOST} - port: ${POSTGRES_PORT} - user: ${POSTGRES_USER} - password: ${POSTGRES_PASSWORD} - cors: - origin: ${UFFIZZI_URL} - methods: [GET, HEAD, PATCH, POST, PUT, DELETE] - credentials: true - csp: - connect-src: ["'self'", 'http:', 'https:'] - img-src: ["'self'", 'data:', 'https://cdnjs.cloudflare.com'] - # Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference - # Default Helmet Content-Security-Policy values can be removed by setting the key to false - -auth: - environment: production - providers: {} - proxy: - enabled: true - url: https://demo.backstage.io/api/auth - -catalog: - rules: - - allow: - - Component - - API - - Resource - - System - - Domain - - Location - locations: - - type: url - target: https://github.com/backstage/backstage/blob/${REF_NAME}/packages/catalog-model/examples/all.yaml - - - type: url - target: https://github.com/backstage/backstage/blob/${REF_NAME}/plugins/techdocs-backend/examples/documented-component-uffizzi/catalog-info.yaml - - - type: url - target: https://github.com/backstage/backstage/blob/${REF_NAME}/packages/catalog-model/examples/acme-corp.yaml - rules: - - allow: [User, Group] - - - type: url - target: https://github.com/backstage/backstage/blob/${REF_NAME}/plugins/scaffolder-backend/sample-templates/all-templates.yaml - rules: - - allow: [Template] - -proxy: - endpoints: - '/circleci/api': - target: https://circleci.com/api/v1.1 - headers: - Circle-Token: ${CIRCLECI_AUTH_TOKEN} - - '/jenkins/api': - target: http://localhost:8080 - headers: - Authorization: ${JENKINS_BASIC_AUTH_HEADER} - - '/travisci/api': - target: https://api.travis-ci.com - changeOrigin: true - headers: - Authorization: ${TRAVISCI_AUTH_TOKEN} - travis-api-version: '3' - - '/newrelic/apm/api': - target: https://api.newrelic.com/v2 - headers: - X-Api-Key: ${NEW_RELIC_REST_API_KEY} - - '/newrelic/api': - target: https://api.newrelic.com - headers: - X-Api-Key: ${NEW_RELIC_USER_KEY} - - '/pagerduty': - target: https://api.pagerduty.com - headers: - Authorization: Token token=${PAGERDUTY_TOKEN} - - '/buildkite/api': - target: https://api.buildkite.com/v2/ - headers: - Authorization: ${BUILDKITE_TOKEN} - - '/sentry/api': - target: https://sentry.io/api/ - allowedMethods: ['GET'] - headers: - Authorization: ${SENTRY_TOKEN} - - '/ilert': - target: https://api.ilert.com - allowedMethods: ['GET', 'POST', 'PUT'] - allowedHeaders: ['Authorization'] - headers: - Authorization: ${ILERT_AUTH_HEADER} - - '/airflow': - target: https://your.airflow.instance.com/api/v1 - headers: - Authorization: ${AIRFLOW_BASIC_AUTH_HEADER} - - '/gocd': - target: https://your.gocd.instance.com/go/api - allowedMethods: ['GET'] - allowedHeaders: ['Authorization'] - headers: - Authorization: Basic ${GOCD_AUTH_CREDENTIALS} - - '/dynatrace': - target: https://your.dynatrace.instance.com/api/v2 - headers: - Authorization: 'Api-Token ${DYNATRACE_ACCESS_TOKEN}' - -techdocs: - builder: 'local' # Alternatives - 'external' - generator: - runIn: 'local' - # dockerImage: my-org/techdocs # use a custom docker image - # pullImage: true # or false to disable automatic pulling of image (e.g. if custom docker login is required) - publisher: - type: 'local' # Alternatives - 'googleGcs' or 'awsS3' or 'azureBlobStorage' or 'openStackSwift'. Read documentation for using alternatives. - -dynatrace: - baseUrl: https://your.dynatrace.instance.com - -nomad: - addr: 0.0.0.0 - -# Score-cards sample configuration. -scorecards: - jsonDataUrl: https://raw.githubusercontent.com/Oriflame/backstage-plugins/main/plugins/score-card/sample-data/ - wikiLinkTemplate: https://link-to-wiki/{id} - -sentry: - organization: my-company - -rollbar: - organization: my-company - # NOTE: The rollbar-backend & accountToken key may be deprecated in the future (replaced by a proxy config) - accountToken: my-rollbar-account-token - -lighthouse: - baseUrl: http://localhost:3003 - -kubernetes: - serviceLocatorMethod: - type: 'multiTenant' - clusterLocatorMethods: - - type: 'config' - clusters: - - url: ${UFFIZZI_CLUSTER_APISERVER} - name: uffizzi - authProvider: 'serviceAccount' - -kafka: - clientId: backstage - clusters: - - name: cluster - dashboardUrl: https://akhq.io/ - brokers: - - localhost:9092 - -allure: - baseUrl: http://localhost:5050/allure-docker-service - -integrations: - github: - - host: github.com - token: ${GITHUB_TOKEN} - ### Example for how to add your GitHub Enterprise instance using the API: - # - host: ghe.example.net - # apiBaseUrl: https://ghe.example.net/api/v3 - # token: ${GHE_TOKEN} - ### Example for how to add your GitHub Enterprise instance using raw HTTP fetches (token is optional): - # - host: ghe.example.net - # rawBaseUrl: https://ghe.example.net/raw - # token: ${GHE_TOKEN} - gitlab: - - host: gitlab.com - token: ${GITLAB_TOKEN} - ### Example for how to add a bitbucket cloud integration - # bitbucketCloud: - # - username: ${BITBUCKET_USERNAME} - # appPassword: ${BITBUCKET_APP_PASSWORD} - ### Example for how to add your bitbucket server instance using the API: - # - host: server.bitbucket.com - # apiBaseUrl: server.bitbucket.com - # username: ${BITBUCKET_SERVER_USERNAME} - # appPassword: ${BITBUCKET_SERVER_APP_PASSWORD} - azure: - - host: dev.azure.com - token: ${AZURE_TOKEN} - # googleGcs: - # clientEmail: 'example@example.com' - # privateKey: ${GCS_PRIVATE_KEY} - awsS3: - - host: amazonaws.com - accessKeyId: ${AWS_ACCESS_KEY_ID} - secretAccessKey: ${AWS_SECRET_ACCESS_KEY} - -costInsights: - engineerCost: 200000 - engineerThreshold: 0.5 - products: - computeEngine: - name: Compute Engine - icon: compute - cloudDataflow: - name: Cloud Dataflow - icon: data - cloudStorage: - name: Cloud Storage - icon: storage - bigQuery: - name: BigQuery - icon: search - events: - name: Events - icon: data - metrics: - DAU: - name: Daily Active Users - default: true - MSC: - name: Monthly Subscribers - currencies: - engineers: - label: 'Engineers 🛠' - unit: 'engineer' - usd: - label: 'US Dollars 💵' - kind: 'USD' - unit: 'dollar' - prefix: '$' - rate: 1 - carbonOffsetTons: - label: 'Carbon Offset Tons ♻️⚖️s' - kind: 'CARBON_OFFSET_TONS' - unit: 'carbon offset ton' - rate: 3.5 - beers: - label: 'Beers 🍺' - kind: 'BEERS' - unit: 'beer' - rate: 4.5 - pintsIceCream: - label: 'Pints of Ice Cream 🍦' - kind: 'PINTS_OF_ICE_CREAM' - unit: 'ice cream pint' - rate: 5.5 -pagerduty: - eventsBaseUrl: 'https://events.pagerduty.com/v2' -jenkins: - instances: - - name: default - baseUrl: https://jenkins.example.com - username: backstage-bot - apiKey: 123456789abcdef0123456789abcedf012 - -azureDevOps: - host: dev.azure.com - token: my-token - organization: my-company - -apacheAirflow: - baseUrl: https://your.airflow.instance.com - -gocd: - baseUrl: https://your.gocd.instance.com - -permission: - enabled: true - -search: - query: - pageLimit: 50 diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index aa11568564..f2fe96f3ab 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -435,7 +435,6 @@ transpilers truthy TSDoc typeahead -Uffizzi ui unbreak Unconference @@ -447,6 +446,7 @@ unregistering unregistration untracked upsert +upstreams upvote URIs url diff --git a/.github/workflows/api-breaking-changes.yml b/.github/workflows/api-breaking-changes.yml index 5f456a9b0b..d44e524ccb 100644 --- a/.github/workflows/api-breaking-changes.yml +++ b/.github/workflows/api-breaking-changes.yml @@ -18,7 +18,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: # Fetch the commit that's merged into the base rather than the target ref # This will let us diff only the contents of the PR, without fetching more history @@ -39,20 +39,18 @@ jobs: - name: breaking changes check run: | - yarn backstage-repo-tools repo schema openapi check --since origin/${{ github.base_ref }} > comment.md + yarn backstage-repo-tools repo schema openapi diff --since origin/${{ github.base_ref }} > comment.md - - name: Upload Rendered Comment as Artifact + - name: clone artifacts to current directory + run: | + cat ${{ github.event_path }} > event.json + + - name: Upload Artifacts uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4 with: name: preview-spec - path: comment.md - retention-days: 2 - overwrite: true - - - name: Upload PR Event as Artifact - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 - with: - name: preview-spec - path: ${{ github.event_path }} + path: | + comment.md + event.json retention-days: 2 overwrite: true diff --git a/.github/workflows/automate_changeset_feedback.yml b/.github/workflows/automate_changeset_feedback.yml index 6ac327bcd6..95b0a808a1 100644 --- a/.github/workflows/automate_changeset_feedback.yml +++ b/.github/workflows/automate_changeset_feedback.yml @@ -27,7 +27,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: # Fetch the commit that's merged into the base rather than the target ref # This will let us diff only the contents of the PR, without fetching more history diff --git a/.github/workflows/automate_merge_message.yml b/.github/workflows/automate_merge_message.yml index 56ab3283ac..a53369c884 100644 --- a/.github/workflows/automate_merge_message.yml +++ b/.github/workflows/automate_merge_message.yml @@ -28,7 +28,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: ref: '${{ github.event.pull_request.merge_commit_sha }}' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45d2363a36..00181adb5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 @@ -68,7 +68,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 @@ -188,7 +188,7 @@ jobs: ports: - 3306/tcp redis: - image: redis + image: redis:7 options: >- --health-cmd "redis-cli ping" --health-interval 10s @@ -206,7 +206,7 @@ jobs: INTEGRATION_TEST_AZURE_TOKEN: ${{ secrets.INTEGRATION_TEST_AZURE_TOKEN }} steps: - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: fetch master branch run: git fetch origin master @@ -240,7 +240,7 @@ jobs: BACKSTAGE_TEST_DATABASE_POSTGRES16_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres16.ports[5432] }} BACKSTAGE_TEST_DATABASE_POSTGRES12_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres12.ports[5432] }} BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING: mysql://root:root@localhost:${{ job.services.mysql8.ports[3306] }}/ignored - BACKSTAGE_TEST_CACHE_REDIS_CONNECTION_STRING: redis://localhost:${{ job.services.redis.ports[6379] }} + BACKSTAGE_TEST_CACHE_REDIS7_CONNECTION_STRING: redis://localhost:${{ job.services.redis.ports[6379] }} # We run the test cases before verifying the specs to prevent any failing tests from causing errors. - name: verify openapi specs against test cases diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml index 19c58a8a60..ddf01fa49c 100644 --- a/.github/workflows/deploy_docker-image.yml +++ b/.github/workflows/deploy_docker-image.yml @@ -25,7 +25,7 @@ jobs: egress-policy: audit - name: checkout - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: path: backstage ref: ${{ github.event.client_payload.version && env.RELEASE_VERSION || github.ref }} diff --git a/.github/workflows/deploy_microsite.yml b/.github/workflows/deploy_microsite.yml index 2554e3eeb8..fd7766e643 100644 --- a/.github/workflows/deploy_microsite.yml +++ b/.github/workflows/deploy_microsite.yml @@ -28,7 +28,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: use node.js 18.x uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 @@ -66,7 +66,7 @@ jobs: run: ls microsite/build && ls microsite/build/storybook - name: Deploy both microsite and storybook to gh-pages - uses: JamesIves/github-pages-deploy-action@ec9c88baef04b842ca6f0a132fd61c762aa6c1b0 # v4.6.0 + uses: JamesIves/github-pages-deploy-action@5c6e9e9f3672ce8fd37b9856193d2a537941e66c # v4.6.1 with: branch: gh-pages folder: microsite/build diff --git a/.github/workflows/deploy_nightly.yml b/.github/workflows/deploy_nightly.yml index 6ec5b03d56..02a1c65681 100644 --- a/.github/workflows/deploy_nightly.yml +++ b/.github/workflows/deploy_nightly.yml @@ -19,7 +19,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: use node.js 18.x uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index 444f3f825d..04badd65b0 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -55,6 +55,15 @@ jobs: --health-retries 5 ports: - 3306/tcp + redis: + image: redis:7 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379/tcp env: CI: true @@ -65,7 +74,7 @@ jobs: INTEGRATION_TEST_AZURE_TOKEN: ${{ secrets.INTEGRATION_TEST_AZURE_TOKEN }} steps: - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 @@ -115,6 +124,7 @@ jobs: BACKSTAGE_TEST_DATABASE_POSTGRES16_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres16.ports[5432] }} BACKSTAGE_TEST_DATABASE_POSTGRES12_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres12.ports[5432] }} BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING: mysql://root:root@localhost:${{ job.services.mysql8.ports[3306] }}/ignored + BACKSTAGE_TEST_CACHE_REDIS7_CONNECTION_STRING: redis://localhost:${{ job.services.redis.ports[6379] }} - name: Discord notification if: ${{ failure() }} @@ -148,7 +158,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 0b883dd75a..8d5b2522af 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -34,12 +34,12 @@ jobs: egress-policy: audit - name: 'Checkout code' - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: persist-credentials: false - name: 'Run analysis' - uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3 with: results_file: results.sarif results_format: sarif diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml index dce7217201..bb2e00ffa6 100644 --- a/.github/workflows/sync_code-formatting.yml +++ b/.github/workflows/sync_code-formatting.yml @@ -14,7 +14,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: # Fetch changes to previous commit - required for 'only_changed' in Prettier action fetch-depth: 0 diff --git a/.github/workflows/sync_dependabot-changesets.yml b/.github/workflows/sync_dependabot-changesets.yml index ddd60750d1..0102cf316a 100644 --- a/.github/workflows/sync_dependabot-changesets.yml +++ b/.github/workflows/sync_dependabot-changesets.yml @@ -16,7 +16,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: fetch-depth: 2 ref: ${{ github.head_ref }} diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index c971be522f..1f58969202 100644 --- a/.github/workflows/sync_release-manifest.yml +++ b/.github/workflows/sync_release-manifest.yml @@ -21,7 +21,7 @@ jobs: run: npm install semver@7.3.5 fs-extra@10.0.0 @manypkg/get-packages@1.1.1 - name: Checkout - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: path: backstage # 'v' prefix is added here for the tag, we keep it out of the manifest logic @@ -29,7 +29,7 @@ jobs: # Checkout backstage/versions into /backstage/versions, which is where store the output - name: Checkout versions - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: repository: backstage/versions path: backstage/versions diff --git a/.github/workflows/sync_renovate-changesets.yml b/.github/workflows/sync_renovate-changesets.yml index 9d031812ea..01e6ee6dfc 100644 --- a/.github/workflows/sync_renovate-changesets.yml +++ b/.github/workflows/sync_renovate-changesets.yml @@ -16,7 +16,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: fetch-depth: 2 ref: ${{ github.head_ref }} diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index 2ea3fcd5a1..cb5eaa1737 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -16,7 +16,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: use node.js 18.x uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 diff --git a/.github/workflows/sync_snyk-monitor.yml b/.github/workflows/sync_snyk-monitor.yml index e6b274c395..4678c36907 100644 --- a/.github/workflows/sync_snyk-monitor.yml +++ b/.github/workflows/sync_snyk-monitor.yml @@ -29,7 +29,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Monitor and Synchronize Snyk Policies uses: snyk/actions/node@8349f9043a8b7f0f3ee8885bf28f0b388d2446e8 # master with: diff --git a/.github/workflows/sync_version-packages.yml b/.github/workflows/sync_version-packages.yml index 884bcf3d95..a7f8d3674e 100644 --- a/.github/workflows/sync_version-packages.yml +++ b/.github/workflows/sync_version-packages.yml @@ -18,7 +18,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: fetch-depth: 20000 fetch-tags: true diff --git a/.github/workflows/uffizzi-build.yml b/.github/workflows/uffizzi-build.yml deleted file mode 100644 index 5ab208590a..0000000000 --- a/.github/workflows/uffizzi-build.yml +++ /dev/null @@ -1,132 +0,0 @@ -name: Preview (build) -on: - pull_request: - types: [opened, synchronize, reopened, closed] - paths-ignore: - - '.changeset/**' - - 'contrib/**' - - 'docs/**' - - 'microsite/**' - - 'beps/**' - - 'scripts/**' - - 'storybook/**' - - '**/*.test.*' - - '**/package.json' - - '*.md' - -jobs: - build-backstage: - env: - NODE_OPTIONS: --max-old-space-size=4096 - UFFIZZI_URL: https://app.uffizzi.com - name: Build PR image - runs-on: ubuntu-latest - if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }} - outputs: - tags: ${{ steps.meta.outputs.tags }} - steps: - - name: Harden Runner - uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 - with: - egress-policy: audit - - - name: checkout - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 - - - name: setup-node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - with: - node-version: 18.x - registry-url: https://registry.npmjs.org/ - - - name: yarn install - uses: backstage/actions/yarn-install@af61233abb88019335b07ab855873d991f43d25a # v0.6.7 - with: - cache-prefix: linux-v18 - - - name: Use Uffizzi's backstage app config - run: | - cp -f ./.github/uffizzi/uffizzi.production.app-config.yaml ./app-config.yaml - - - name: backstage build - run: | - yarn workspace example-backend build - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 - - - name: Generate UUID image name - id: uuid - run: echo "UUID_TAG_APP=backstage-$(uuidgen --time)" >> $GITHUB_ENV - - - name: Docker metadata - id: meta - uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 - with: - images: registry.uffizzi.com/${{ env.UUID_TAG_APP }} - tags: type=raw,value=60d - - - name: Build Image - uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 - with: - context: . - file: packages/backend/Dockerfile - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - push: true - cache-from: type=gha - cache-to: type=gha,mode=max - - render-kustomize: - name: Render Kustomize Manifests - runs-on: ubuntu-latest - needs: - - build-backstage - steps: - - name: Harden Runner - uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 - with: - egress-policy: audit - - - name: Checkout git repo - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 - - name: Render Compose File - run: | - # update image after the build above - cd ./.github/uffizzi/k8s/manifests - kustomize edit set image backstage=${{ needs.build-backstage.outputs.tags }} - kustomize build . > manifests.rendered.yml - cat manifests.rendered.yml - - name: Upload Rendered Manifests File as Artifact - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4 - with: - name: preview-spec - path: ./.github/uffizzi/k8s/manifests/manifests.rendered.yml - retention-days: 2 - overwrite: true - - name: Upload PR Event as Artifact - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 - with: - name: preview-spec - path: ${{ github.event_path }} - retention-days: 2 - overwrite: true - - delete-preview: - name: Call for Preview Deletion - runs-on: ubuntu-latest - if: ${{ github.event.action == 'closed' }} - steps: - - name: Harden Runner - uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 - with: - egress-policy: audit - - # If this PR is closing, we will not render a compose file nor pass it to the next workflow. - - name: Upload PR Event as Artifact - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 - with: - name: preview-spec - path: ${{ github.event_path }} - retention-days: 2 - overwrite: true diff --git a/.github/workflows/uffizzi-preview.yaml b/.github/workflows/uffizzi-preview.yaml deleted file mode 100644 index 8d0f558b1f..0000000000 --- a/.github/workflows/uffizzi-preview.yaml +++ /dev/null @@ -1,238 +0,0 @@ -name: Preview (deploy) - -on: - workflow_run: - workflows: - - 'Preview (build)' - types: - - completed - -jobs: - cache-manifests-file: - name: Cache Manifests File - runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' }} - permissions: - # "If you specify the access for any of these scopes, all of those that are not specified are set to none." - # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions - actions: read # Access cache - outputs: - manifests-cache-key: ${{ steps.hash.outputs.MANIFESTS_FILE_HASH }} - git-ref: ${{ steps.event.outputs.GIT_REF }} - pr-number: ${{ steps.event.outputs.PR_NUMBER }} - action: ${{ steps.event.outputs.ACTION }} - steps: - - name: Harden Runner - uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 - with: - disable-sudo: true - egress-policy: block - allowed-endpoints: > - api.github.com:443 - - - name: 'Download artifacts' - # Fetch output (zip archive) from the workflow run that triggered this workflow. - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - script: | - let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.payload.workflow_run.id, - }); - let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { - return artifact.name == "preview-spec" - })[0]; - if (matchArtifact === undefined) { - throw TypeError('Build Artifact not found!'); - } - let download = await github.rest.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - }); - let fs = require('fs'); - fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data)); - - - name: 'Accept event from first stage' - run: unzip preview-spec.zip event.json - - - name: Read Event into ENV - id: event - run: | - echo PR_NUMBER=$(jq '.number | tonumber' < event.json) >> $GITHUB_OUTPUT - echo ACTION=$(jq --raw-output '.action | tostring | [scan("\\w+")][0]' < event.json) >> $GITHUB_OUTPUT - echo GIT_REF=$(jq --raw-output '.pull_request.head.sha | tostring | [scan("\\w+")][0]' < event.json) >> $GITHUB_OUTPUT - - - name: Hash Rendered Manifests File - id: hash - # If the previous workflow was triggered by a PR close event, we will not have a manifests file artifact. - if: ${{ steps.event.outputs.ACTION != 'closed' }} - run: | - unzip preview-spec.zip manifests.rendered.yml - ls - echo "MANIFESTS_FILE_HASH=$(md5sum manifests.rendered.yml | awk '{ print $1 }')" >> $GITHUB_OUTPUT - - - name: Cache Manifests File - if: ${{ steps.event.outputs.ACTION != 'closed' }} - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 - with: - path: manifests.rendered.yml - key: ${{ steps.hash.outputs.MANIFESTS_FILE_HASH }} - - - name: DEBUG - Print Job Outputs - if: ${{ runner.debug }} - run: | - echo "PR number: ${{ steps.event.outputs.PR_NUMBER }}" - echo "Git Ref: ${{ steps.event.outputs.GIT_REF }}" - echo "Action: ${{ steps.event.outputs.ACTION }}" - echo "Manifests file hash: ${{ steps.hash.outputs.MANIFESTS_FILE_HASH }}" - cat event.json - - deploy-uffizzi-preview: - name: Deploy to Uffizzi Virtual Cluster - needs: - - cache-manifests-file - if: ${{ github.event.workflow_run.conclusion == 'success' && needs.cache-manifests-file.outputs.action != 'closed' }} - permissions: - contents: read - pull-requests: write - id-token: write - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - # Identify comment to be updated - - name: Find comment for Ephemeral Environment - uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3 - id: find-comment - with: - issue-number: ${{ needs.cache-manifests-file.outputs.pr-number }} - comment-author: 'github-actions[bot]' - body-includes: pr-${{ needs.cache-manifests-file.outputs.pr-number }} - direction: last - - # Create/Update comment with action deployment status - - name: Create or Update Comment with Deployment Notification - id: notification - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4 - with: - comment-id: ${{ steps.find-comment.outputs.comment-id }} - issue-number: ${{ needs.cache-manifests-file.outputs.pr-number }} - body: | - ## Uffizzi Ephemeral Environment - Virtual Cluster - - :cloud: deploying cluster `pr-${{ needs.cache-manifests-file.outputs.pr-number }}` - - :gear: Updating now by workflow run [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}). - - Download the Uffizzi CLI to interact with the upcoming virtual cluster - https://docs.uffizzi.com/install - edit-mode: replace - - - name: Connect to Virtual Cluster - uses: UffizziCloud/cluster-action@main - with: - cluster-name: pr-${{ needs.cache-manifests-file.outputs.pr-number }} - server: https://app.uffizzi.com - - - name: Fetch cached Manifests File - id: cache - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4 - with: - path: manifests.rendered.yml - key: ${{ needs.cache-manifests-file.outputs.manifests-cache-key }} - - - name: Kustomize and Apply Manifests - id: prev - run: | - # Apply kustomized manifests to virtual cluster. - export KUBECONFIG=`pwd`/kubeconfig - kubectl apply -f manifests.rendered.yml --kubeconfig ./kubeconfig - # Allow uffizzi to sync the resources - sleep 10 - # Get the hostnames assigned by uffizzi - export BACKSTAGE_HOST=$(kubectl get ingress backstage --kubeconfig kubeconfig -o json | jq '.spec.rules[0].host' | tr -d '"') - export UFFIZZI_CLUSTER_APISERVER=$(kubectl config view --minify | grep server | cut -f 2- -d ":" | tr -d " ") - # Patch backstage deployment to use UFFIZZI_URL - kubectl patch deployment backstage --kubeconfig kubeconfig -p '{"spec": {"template": {"spec": {"containers": [{"name": "backstage", "args":["-c", "APP_CONFIG_app_baseUrl='https://${BACKSTAGE_HOST}' APP_CONFIG_backend_baseUrl='https://${BACKSTAGE_HOST}' APP_CONFIG_auth_environment='production' node packages/backend --config app-config.yaml"], "env": [{"name": "UFFIZZI_URL", "value": "'https://${BACKSTAGE_HOST}'"}, {"name": "UFFIZZI_CLUSTER_APISERVER", "value": "'${UFFIZZI_CLUSTER_APISERVER}'"}, {"name": "GITHUB_SHA", "value": "'${GITHUB_SHA}'"}, {"name": "REF_NAME", "value": "'${{ needs.cache-manifests-file.outputs.git-ref }}'"}]}]}}}}' - if [[ ${RUNNER_DEBUG} == 1 ]]; then - kubectl get all --kubeconfig ./kubeconfig - fi - echo "backstage_url=${BACKSTAGE_HOST}" >> $GITHUB_OUTPUT - echo "Access the \`backstage\` endpoint at [\`${BACKSTAGE_HOST}\`](http://${BACKSTAGE_HOST})" >> $GITHUB_STEP_SUMMARY - - - name: Create or Update Comment with Deployment URL - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4 - with: - comment-id: ${{ steps.notification.outputs.comment-id }} - issue-number: ${{ github.event.pull_request.number }} - body: | - ## Uffizzi Ephemeral Environment - Virtual Cluster - - Your cluster `pr-${{ needs.cache-manifests-file.outputs.pr-number }}` was successfully created. Learn more about [Uffizzi virtual clusters](https://docs.uffizzi.com/topics/virtual-clusters) - To connect to this cluster, follow these steps: - - 1. Download and install the Uffizzi CLI from https://docs.uffizzi.com/install - 2. Login to Uffizzi, then select the `backstage` account and project: - ``` - uffizzi login - ``` - - ``` - Select an account: - ‣ ${{ github.event.repository.name }} - jdoe - - Select a project or create a new project: - ‣ ${{ github.event.repository.name }}-6783521 - ``` - 3. Update your kubeconfig: `uffizzi cluster update-kubeconfig pr-${{ needs.cache-manifests-file.outputs.pr-number }} --kubeconfig=[PATH_TO_KUBECONFIG]` - After updating your kubeconfig, you can manage your cluster with `kubectl`, `kustomize`, `helm`, and other tools that use kubeconfig files: `kubectl get namespace --kubeconfig [PATH_TO_KUBECONFIG]` - - - Access the `backstage` endpoint at [`https://${{ steps.prev.outputs.backstage_url }}`](https://${{ steps.prev.outputs.backstage_url }}) - - edit-mode: replace - - delete-uffizzi-preview: - permissions: - contents: read - pull-requests: write - id-token: write - name: Delete the Uffizzi Virtual Cluster - needs: - - cache-manifests-file - if: ${{ needs.cache-manifests-file.outputs.action == 'closed' }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - - name: Delete Virtual Cluster - uses: UffizziCloud/cluster-action@main - with: - cluster-name: pr-${{ needs.cache-manifests-file.outputs.pr-number }} - server: https://app.uffizzi.com - action: delete - - # Identify comment to be updated - - name: Find comment for Ephemeral Environment - uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3 - id: find-comment - with: - issue-number: ${{ needs.cache-manifests-file.outputs.pr-number }} - comment-author: 'github-actions[bot]' - body-includes: pr-${{ needs.cache-manifests-file.outputs.pr-number }} - direction: last - - - name: Update Comment with Deletion - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4 - with: - comment-id: ${{ steps.find-comment.outputs.comment-id }} - issue-number: ${{ needs.cache-manifests-file.outputs.pr-number }} - body: | - Uffizzi Cluster `pr-${{ needs.cache-manifests-file.outputs.pr-number }}` was deleted. - edit-mode: replace diff --git a/.github/workflows/verify_accessibility.yml b/.github/workflows/verify_accessibility.yml index 75415f7961..996153f8d3 100644 --- a/.github/workflows/verify_accessibility.yml +++ b/.github/workflows/verify_accessibility.yml @@ -24,7 +24,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Use Node.js 18.x uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: diff --git a/.github/workflows/verify_codeql.yml b/.github/workflows/verify_codeql.yml index 0c30218d72..bcac7e947c 100644 --- a/.github/workflows/verify_codeql.yml +++ b/.github/workflows/verify_codeql.yml @@ -47,7 +47,7 @@ jobs: egress-policy: audit - name: Checkout repository - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/.github/workflows/verify_docs-quality.yml b/.github/workflows/verify_docs-quality.yml index f784608493..5b213f2c14 100644 --- a/.github/workflows/verify_docs-quality.yml +++ b/.github/workflows/verify_docs-quality.yml @@ -16,7 +16,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 # Vale does not support file excludes, so we use the script to generate a list of files instead # The action also does not allow args or a local config file to be passed in, so the files array diff --git a/.github/workflows/verify_e2e-kubernetes.yml b/.github/workflows/verify_e2e-kubernetes.yml index 9763cbd085..77d2c3bdc1 100644 --- a/.github/workflows/verify_e2e-kubernetes.yml +++ b/.github/workflows/verify_e2e-kubernetes.yml @@ -26,7 +26,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index b9229263ae..f8583ac178 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -45,7 +45,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Configure Git run: | diff --git a/.github/workflows/verify_e2e-techdocs.yml b/.github/workflows/verify_e2e-techdocs.yml index 31d9d8b06a..2ed566e36b 100644 --- a/.github/workflows/verify_e2e-techdocs.yml +++ b/.github/workflows/verify_e2e-techdocs.yml @@ -34,7 +34,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 with: python-version: '3.9' diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index 064baf700e..e2a789d666 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -42,7 +42,7 @@ jobs: git config --global core.autocrlf false git config --global core.eol lf - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Configure Git run: | diff --git a/.github/workflows/verify_fossa.yml b/.github/workflows/verify_fossa.yml index 152fe9f185..ba5d177cc4 100644 --- a/.github/workflows/verify_fossa.yml +++ b/.github/workflows/verify_fossa.yml @@ -19,7 +19,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Install Fossa run: "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash" diff --git a/.github/workflows/verify_microsite.yml b/.github/workflows/verify_microsite.yml index 48d7da218d..256b00b160 100644 --- a/.github/workflows/verify_microsite.yml +++ b/.github/workflows/verify_microsite.yml @@ -28,7 +28,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: use node.js 18.x uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 diff --git a/.github/workflows/verify_microsite_accessibility.yml b/.github/workflows/verify_microsite_accessibility.yml index 9728bf57c6..9986ae051e 100644 --- a/.github/workflows/verify_microsite_accessibility.yml +++ b/.github/workflows/verify_microsite_accessibility.yml @@ -19,7 +19,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Use Node.js 18.x uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index 9a59b7b071..1859c0b8bc 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -32,7 +32,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: fetch-depth: 0 # Required to retrieve git history diff --git a/.github/workflows/verify_windows.yml b/.github/workflows/verify_windows.yml index cd7a9b62ae..5c4bed2d11 100644 --- a/.github/workflows/verify_windows.yml +++ b/.github/workflows/verify_windows.yml @@ -33,7 +33,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 diff --git a/ADOPTERS.md b/ADOPTERS.md index 2bb76a199e..5fe359bf1e 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -270,3 +270,4 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [OP Financial Group](https://www.op.fi/op-financial-group) | [Heikki Hellgren](https://github.com/drodil), [Jyrki Koistinen](https://github.com/snyvision) | We are using Backstage as a gateway into our internal development platform offering to simplify complexity. | | [Scania](https://www.scania.com) | [Santosh Rangavajjula](https://linkedin.com/in/rsbth) | We are implementing backstage at Scania to consolidate operational information from Gitlab, Jira, Confluence, Artifactory, Servicenow and other tools into one place. | | [Senora.dev](https://senora.dev) | [Shaked Braimok Yosef](https://github.com/ShakedBraimok) | We are using Backstage as a service catalog for our costumers. | +| [Covestro](https://www.covestro.com) | [Julien Gedeon](https://github.com/cvvcv1) | We use Backstage as our Internal developer portal for the Digital R&D department. | diff --git a/OWNERS.md b/OWNERS.md index f2e2cb7cf1..bf97c91685 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -22,14 +22,15 @@ Team: @backstage/catalog-maintainers Scope: The catalog plugin and catalog model -| Name | Organization | Team | GitHub | Discord | -| --------------- | ------------ | --------- | ----------------------------------------- | ------------------- | -| Rickard Dybeck | Spotify | Chipmunks | [alde](https://github.com/alde) | `rdybeck#8083` | -| Mike Blockley | Spotify | Chipmunks | [mikeyhc](https://github.com/mikeyhc) | `mikey-spot#5363` | -| Elon Jefferson | Spotify | Chipmunks | [Edje-C](https://github.com/Edje-C) | `elon-spotty#6086 ` | -| Nurit Izrailov | Spotify | Chipmunks | [nuritizra](https://github.com/nuritizra) | - | -| Hunter Dougless | Spotify | Chipmunks | [hntrdglss](https://github.com/hntrdglss) | `hntrdglss#1849` | -| Seve Kim | Spotify | Chipmunks | [sevedkim](https://github.com/sevedkim) | `seve#9951` | +| Name | Organization | Team | GitHub | Discord | +| -------------------- | ------------ | ------------- | ----------------------------------------------- | --------------- | +| Ben Lambert | Spotify | Cubic Belugas | [benjdlambert](https://github.com/benjdlambert) | `blam#2159` | +| Camila Loiola | Spotify | Cubic Belugas | [camilaibs](http://github.com/camilaibs) | `camilal#0226` | +| Fredrik Adelöw | Spotify | Cubic Belugas | [freben](https://github.com/freben) | `freben#3926` | +| Johan Haals | Spotify | Cubic Belugas | [jhaals](https://github.com/jhaals) | `Johan#0679` | +| Mihai Tabara | Spotify | Cubic Belugas | [MihaiTabara](http://github.com/MihaiTabara) | `mihait#3107` | +| Patrik Oldsberg | Spotify | Cubic Belugas | [Rugvip](https://github.com/Rugvip) | `Rugvip#0019` | +| Vincenzo Scamporlino | Spotify | Cubic Belugas | [vinzscam](http://github.com/vinzscam) | `vinzscam#6944` | ### Discoverability @@ -73,14 +74,15 @@ Team: @backstage/permission-maintainers Scope: The Permission Framework and plugins integrating with the permission framework -| Name | Organization | Team | GitHub | Discord | -| -------------------- | ------------ | --------------- | ------------------------------------------ | ------------- | -| Ainhoa Larumbe | Spotify | Imaginary Goats | [ainhoaL](http://github.com/ainhoaL) | ainhoa#8085 | -| Eric Peterson | Spotify | Imaginary Goats | [iamEAP](http://github.com/iamEAP) | iamEAP#3058 | -| Harry Hogg | Spotify | Imaginary Goats | [HHogg](http://github.com/HHogg) | simplex#3451 | -| Joon Park | Spotify | Imaginary Goats | [Joonpark13](http://github.com/Joonpark13) | Sixpool#5060 | -| Mike Lewis | Spotify | Imaginary Goats | [mtlewis](http://github.com/mtlewis) | mtlewis#3658 | -| Vincenzo Scamporlino | Spotify | Imaginary Goats | [vinzscam](http://github.com/vinzscam) | vinzscam#6944 | +| Name | Organization | Team | GitHub | Discord | +| -------------------- | ------------ | ------------- | ----------------------------------------------- | --------------- | +| Ben Lambert | Spotify | Cubic Belugas | [benjdlambert](https://github.com/benjdlambert) | `blam#2159` | +| Camila Loiola | Spotify | Cubic Belugas | [camilaibs](http://github.com/camilaibs) | `camilal#0226` | +| Fredrik Adelöw | Spotify | Cubic Belugas | [freben](https://github.com/freben) | `freben#3926` | +| Johan Haals | Spotify | Cubic Belugas | [jhaals](https://github.com/jhaals) | `Johan#0679` | +| Mihai Tabara | Spotify | Cubic Belugas | [MihaiTabara](http://github.com/MihaiTabara) | `mihait#3107` | +| Patrik Oldsberg | Spotify | Cubic Belugas | [Rugvip](https://github.com/Rugvip) | `Rugvip#0019` | +| Vincenzo Scamporlino | Spotify | Cubic Belugas | [vinzscam](http://github.com/vinzscam) | `vinzscam#6944` | ### TechDocs diff --git a/README-ko_kr.md b/README-ko_kr.md index e5f50bd2b8..b2bba41567 100644 --- a/README-ko_kr.md +++ b/README-ko_kr.md @@ -10,7 +10,6 @@ ![Code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg) [![Codecov](https://img.shields.io/codecov/c/github/backstage/backstage)](https://codecov.io/gh/backstage/backstage) [![](https://img.shields.io/github/v/release/backstage/backstage)](https://github.com/backstage/backstage/releases) -[![Uffizzi](https://img.shields.io/endpoint?url=https%3A%2F%2Fapp.uffizzi.com%2Fapi%2Fv1%2Fpublic%2Fshields%2Fgithub.com%2Fbackstage%2Fbackstage)](https://app.uffizzi.com/ephemeral-environments/backstage/backstage) [![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/7678/badge)](https://bestpractices.coreinfrastructure.org/projects/7678) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/backstage/backstage/badge)](https://securityscorecards.dev/viewer/?uri=github.com/backstage/backstage) diff --git a/README-zh_Hans.md b/README-zh_Hans.md index 3e6cea4e95..2813ee0793 100644 --- a/README-zh_Hans.md +++ b/README-zh_Hans.md @@ -10,7 +10,6 @@ ![Code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg) [![Codecov](https://img.shields.io/codecov/c/github/backstage/backstage)](https://codecov.io/gh/backstage/backstage) [![](https://img.shields.io/github/v/release/backstage/backstage)](https://github.com/backstage/backstage/releases) -[![Uffizzi](https://img.shields.io/endpoint?url=https%3A%2F%2Fapp.uffizzi.com%2Fapi%2Fv1%2Fpublic%2Fshields%2Fgithub.com%2Fbackstage%2Fbackstage)](https://app.uffizzi.com/ephemeral-environments/backstage/backstage) [![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/7678/badge)](https://bestpractices.coreinfrastructure.org/projects/7678) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/backstage/backstage/badge)](https://securityscorecards.dev/viewer/?uri=github.com/backstage/backstage) diff --git a/README.md b/README.md index 6cea4235e0..47ae539b6b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ English \| [한국어](README-ko_kr.md) \| [中文版](README-zh_Hans.md) ![Code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg) [![Codecov](https://img.shields.io/codecov/c/github/backstage/backstage)](https://codecov.io/gh/backstage/backstage) [![](https://img.shields.io/github/v/release/backstage/backstage)](https://github.com/backstage/backstage/releases) -[![Uffizzi](https://img.shields.io/endpoint?url=https%3A%2F%2Fapp.uffizzi.com%2Fapi%2Fv1%2Fpublic%2Fshields%2Fgithub.com%2Fbackstage%2Fbackstage)](https://app.uffizzi.com/ephemeral-environments/backstage/backstage) [![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/7678/badge)](https://bestpractices.coreinfrastructure.org/projects/7678) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/backstage/backstage/badge)](https://securityscorecards.dev/viewer/?uri=github.com/backstage/backstage) diff --git a/beps/0001-notifications-system/README.md b/beps/0001-notifications-system/README.md index 150abec9d6..80751179dd 100644 --- a/beps/0001-notifications-system/README.md +++ b/beps/0001-notifications-system/README.md @@ -92,7 +92,7 @@ If the notification status is updated, the signal service shall emit a signal wi The role of the notifications plugin is to manage the lifecycle of notifications. The backend plugin provides an API for other backends to send notifications, as well as an accompanying [backend service](https://backstage.io/docs/backend-system/architecture/services). It also provides a separate API for the frontend plugin to read notifications for an individual user and manage the read status of notifications. -The notification backend stores notification using the [database service](https://backstage.io/docs/backend-system/core-services/index#database). In particular it needs to store the following information for each notification: +The notification backend stores notification using the [database service](https://backstage.io/docs/backend-system/core-services/index#database). In particular, it needs to store the following information for each notification: - ID - Recipients @@ -110,8 +110,9 @@ The notification backend stores notification using the [database service](https: - Topic (optional) - Scope (optional) - Icon (optional) + - Metadata (optional) -The recipients is **not** a list of users, but rather a filter that describes who should receive the notification. It must be possible to evaluate this filter in a database query, so that we can efficiently fetch all notifications for a given user. The same filter will also be used by the signal backend to determine which users should receive a signal. +The recipients are **not** a list of users, but rather a filter that describes who should receive the notification. It must be possible to evaluate this filter in a database query, so that we can efficiently fetch all notifications for a given user. The same filter will also be used by the signal backend to determine which users should receive a signal. The read date is a timestamp of marking the notifications as read by the user. If missing, the notification is still unread. @@ -147,6 +148,8 @@ The link is a relative or absolute URL. As an example, it can be used: - by an external system to request an action within an asynchronous task - by a BE plugin to provide link to other part of the Backstage UI (i.e. to the Catalog) +The metadata is an opaque JSON field, where an additional payload can be stored. The format of this data is owned by the notification sender and is tied to the notification topic, i.e. notifications sent from the source on the same topic should use a compatible metadata format. The primary purpose of this field is to allow for custom processing and rendering based on the additional metadata. + The additional links are an array of title-URL pairs. They can represent immediate actions on the notification (i.e. yes-no) or lead the user to additional details. The `notification-backend` does not provide any new permissions, since creating notifications can only be done by other backend plugins, while reading notifications can only be done by the authenticated user. It is possible that we want to add a permissions for reading notifications, in particular for admin and impersonation use cases, but that is not part of this proposal or the initial implementation. @@ -206,12 +209,16 @@ export type NotificationSeverity = 'critical' | 'high' | 'normal' | 'low'; export type NotificationPayload = { title: string; description?: string; - link: string; + link?: string; additionalLinks?: string[]; - severity: NotificationSeverity; + severity?: NotificationSeverity; topic?: string; scope?: string; icon?: string; + metadata?: Array<{ + type: string; + value: JsonValue; + }>; }; export type Notification = { @@ -236,7 +243,8 @@ interface NotificationService { } ``` -Each notification contains a human readable `title`, `origin` and optionally `link` for additional details. The `created`, `id`, `read` and `saved` properties are handled by the backend based and cannot be passed during the notification creation. +Each notification contains a human-readable `title`, `origin` and optionally `link` for additional details. The `created`, `id`, `read` and `saved` properties are handled by the backend based and cannot be passed during the notification creation. +Any optional additional details could be stored in `metadata`. We advise to provide the name to the type which contains the information about the context and the version, for example: 'core.icon.v1'. Calling `sendNotification` should never throw an error so that it doesn't block the current processing. Notifications should be considered as second-level citizens that are not critical if not delivered. diff --git a/beps/0002-dynamic-frontend-plugins/README.md b/beps/0002-dynamic-frontend-plugins/README.md index 6581457e06..f26a3b488d 100644 --- a/beps/0002-dynamic-frontend-plugins/README.md +++ b/beps/0002-dynamic-frontend-plugins/README.md @@ -67,7 +67,7 @@ The overarching goal of this proposal is to outline the full path of how fronten - **Distribution**: How these artifacts are deployed or published and made available for apps to load. - **Loading**: How an app is able to load these artifacts from remote or local sources at runtime. -Each of these may have multiple possible solutions, in particular the loading and distribution steps. This proposal should aim to provide the minimum solutions that avoids the need for each adopter to have to re-bundle open source plugins, while still making it simple to use dynamic installation for their own internal plugins. +Each of these may have multiple possible solutions, in particular the loading and distribution steps. This proposal should aim to provide a solution that makes it possible to re-bundle 3rd party plugins to make it possible to install them dynamically, as well as a simple solution to deploy internal plugins for dynamic installation into an app. There are a couple of sub-goals that are important for this to work: @@ -84,6 +84,8 @@ and make progress. The **integration** of installed plugins and features is not in scope for this proposal, that is the responsibility of the new frontend system. +This proposal does not aim to provide a solution for re-building packages on the fly. Plugins must be pre-built for it to be possible to dynamically install them into an app. This means that a solution where you can directly install existing packages from NPM is not in scope. + This proposal does not contain any form of visual interface for managing dynamically installed plugins. The scope of this proposal only includes configuration of dynamic plugins through static configuration and TypeScript interfaces. This proposal does not aim to make it possible to add or remove plugins into an already running frontend app instance as created by `createApp` from the Backstage core APIs. The page must be reloaded for any updates to take effect. @@ -217,6 +219,12 @@ This can be checked by debugging network traffic and shared scopes: ![notifications system architecture diagram](./scope-sharing.png) +#### Limitations + +A limitation of module sharing is that each module name can only appear once in each context. This means that if you're trying to build a bundle of shared modules with a deeper dependency tree, you may end up needing to resolve conflicts by renaming some modules that appear multiple times with different versions. This will in turn limit sharing of those modules with other bundles. + +There is also a conflict with the chunk splitting currently used in the Backstage CLI that means that some singleton modules need to be configured to be eagerly loaded. More broadly we may also want to re-evaluate the chunk splitting strategy in the Backstage CLI when adding support for module federation. + ### Plugin manifest Each plugin should have a manifest file with important metadata. This metadata is used to load the remote assets to browser. The plugin manifest should be part of a build output. @@ -496,6 +504,8 @@ Theoretically plugins can be hosted on some "public CDN" which is detached from Assets can be also be hosted in the same way as they have always been. +It's important to consider old deployments as part of this storage, to make sure that dynamic chunks are still available to users who have not yet refreshed the page to load the latest version of the app. + ### Plugin discovery How to notify/send data to browser @@ -555,8 +565,16 @@ List any dependencies that this work has on other BEPs or features. ## Alternatives - +### Runtime packaging of NPM packages + +A solution that has been considered is to allow for the installation of NPM packages directly into the app, with them being repackaged for dynamic installation on the fly. This allows for a more direct way of installing plugins, without the need to change how plugins are published to NPM. The downside of this approach is that it leaves validation and dependency management to the runtime and operator of the Backstage instance. + +The NPM ecosystem is built around using version ranges along with lockfiles for dependency management. It is generally up to the maintainers of a repo to make sure that the specific dependency versions installed are all compatible with each other and do not introduce security vulnerabilities. This can be a fairly complex problem, and pushing it to the runtime installation of each plugin ends up being a significant risk. + +We consider it to be a better approach to push this responsibility down one step from Backstage operators, either to plugin maintainers, or to a service that is able to bundle and validate plugin bundles before they are installed into a Backstage instance. Building and running such as service as part of the Backstage open source project is out of scope for this proposal, but is something that could be considered in the future. + +### Publish dynamic bundle to NPM + +We could modify the plugin build process to also output a module federation bundle for dynamic installation. This would also allow for direct installation of plugins directly from NPM, but puts a responsibility on each plugin maintainer to keep publishing new versions of the plugin to NPM simply to update transitive dependencies. This significantly increases the effort of maintaining a Backstage plugin. + +The dynamic installation bundle can also be quite large, depending on the dependency tree of each plugin. There is a risk that the size of `node_modules` in a Backstage increases significantly, potentially hurting the development experience by increasing installation time both locally and in CI. diff --git a/beps/0008-docs-personas-framework-portal/README.md b/beps/0008-docs-personas-framework-portal/README.md new file mode 100644 index 0000000000..07468b7c4a --- /dev/null +++ b/beps/0008-docs-personas-framework-portal/README.md @@ -0,0 +1,347 @@ +--- +title: Improved Backstage Documentation with Personas +status: provisional +authors: + - '@waldirmontoya25' + - '@aramissennyeyd' +owners: +project-areas: + - core +creation-date: 2024-03-18 +--- + +# BEP: Enhancing Backstage Documentation: Personas, Framework, and Developer Portal + +[**Discussion Issue**](https://github.com/backstage/backstage/issues/23689) + +- [BEP: Enhancing Backstage Documentation: Personas, Framework, and Developer Portal](#bep-enhancing-backstage-documentation-personas-framework-and-developer-portal) + - [Summary](#summary) + - [Motivation](#motivation) + - [Goals](#goals) + - [Non-Goals](#non-goals) + - [Proposal](#proposal) + - [Design Details](#design-details) + - [Personas](#personas) + - [User](#user) + - [Documentation Style](#documentation-style) + - [Administrator](#administrator) + - [Documentation Style](#documentation-style-1) + - [Integrator](#integrator) + - [Documentation Style](#documentation-style-2) + - [Contributor](#contributor) + - [Documentation Style](#documentation-style-3) + - [Business Stakeholder](#business-stakeholder) + - [Documentation Style](#documentation-style-4) + - [Release Plan](#release-plan) + - [Dependencies](#dependencies) + - [Example Table of Contents](#example-table-of-contents) + +## Summary + +This BEP proposes restructuring the Backstage documentation to emphasize the dual nature of Backstage as both a framework for building developer portals and a fully functional developer portal out of the box, as demonstrated by the demo site. The documentation will be divided into two main sections: One focusing on the developer portal that users get with the core plugins, and another on the framework that allows integrators and builders to create their own developer portal. The goal is to improve clarity, navigation, and adoption of Backstage by positioning it as both a ready-to-use developer portal and a framework for building custom developer portals. + +## Motivation + +The current Backstage documentation has been reported to be difficult to navigate, making it challenging for different personas across the DevEx ecosystem to extract value. The CNCF [**assessment**](https://github.com/cncf/techdocs/tree/main/assessments/0008-backstage/) and the resulting [**issues**](https://github.com/backstage/backstage/issues/21893) highlight the need for improvement in the documentation structure. + +### Goals + +- Divide the documentation into two section: Framework and Developer Portal +- Define the personas Backstage is targeting +- Structure the documentation to cater the different personas +- Move the existing content to the appropriate section + +### Non-Goals + +- Rewrite the entire documentation from scratch +- Write additional content beyond the scope of the existing documentation + +## Proposal + +The proposed restructuring of the Backstage documentation revolves around two core ideas: + +1. Positioning Backstage as both a framework to build developer portals and a developer portal itself, and splitting the documentation into two main sections: + + - Developer Portal: Focusing on the features, configuration, and usage of the developer portal that users get out of the box with the core plugins. + - Framework: Covering the aspects of Backstage as a framework, including guides for integrators and builders who want to create their own developer portal using Backstage. + +2. Defining the personas participating in Backstage adoption journeys to improve documentation navigation. The identified personas are: + + - **End User**: A person who uses Backstage to find information, use plugins, and consume the developer portal. + - **Administrator/Operator**: A person who configures, secures, and deploys the developer portal, manages plugins, and oversees the general administration of the developer portal. + - **Integrator/Builder**: A person who builds plugins, customizes the code and design, and creates custom-built developer portals based on the Backstage framework. This includes developers and designers and anyone adding new functionality to their own Backstage instance. + - **Product Manager/Business stakeholders**: A person who defines the strategy for adopting Backstage, identifies use cases, communicates the value proposition for adopting Backstage and connects the developer portal to the business strategy. + - **Contributor**: A person who contributes to the Backstage upstream ecosystem. + +The adoption strategy would be as follows: + +- Create a dedicated page describing the personas Backstage is targeting and the documentation sections that cater to each persona. +- Restructure the documentation into the two new sections (Framework and Developer Portal) and redistribute the existing content accordingly. +- Publish a blog post announcing the changes, highlighting Backstage's positioning as both a framework and a developer portal, and explaining the benefits of the restructured documentation. + +The benefits of restructuring the documentation according to these ideas include: + +1. Easier navigation and discoverability of information for different personas and use cases. +2. Clear separation of runtime and development documentation. +3. Simplified process for contributors to determine the appropriate location for new documentation. +4. Streamlined Backstage adoption process for new adopters. + +## Design Details + +- The Docs section of the microsite will be divided into two top-level sections: Framework and Developer Portal. +- The structure of the Table of Contents will align with the outline proposed in https://github.com/backstage/backstage/issues/21946. + +### Personas + +#### User + +Users navigate the developer portal to access tools, information, and plugins essential for their daily tasks. They rely on Backstage to effortlessly find resources, utilize integrations, and connect with other tools and services within their ecosystem. Their interaction is predominantly with the frontend of the portal, where ease of use, accessibility, and relevant content discovery are critical. + +##### Documentation Style + +Documentation for this persona should be about usability of the portal once it is running. For example: + +- Understanding the mechanics of the Software Catalog +- Registering components +- Deleting components +- How the source of truth is the external tool linked through the plugins +- Understand dependencies relations and the overall schema of the catalog +- Using available scaffolder actions +- Customizing new workflows with available actions +- Searching +- Using available plugins +- Step by step tutorials + +#### Administrator + +Administrators are responsible for the behind-the-scenes technical setup and maintenance of the Backstage portal. This includes deploying the portal, configuring plugins, managing user access, and ensuring the security and performance of the system. They interact with both the frontend and backend, often using command-line tools, administrative dashboards, and configuration files to perform their tasks. + +##### Documentation Style + +Documentation written for this persona should be DevOps technical, assuming a strong DevOps background. The goal with administrator documentation is to give administrators a strong understanding of how to deploy and manage a Backstage Developer Portal, best practices. For example: + +- Installing and upgrading +- Configuring + - Authentication + - Plugins +- Ingesting data (users/groups/components, etc) +- Installing plugins +- Implement Git Flows for the Developer portal +- Creating Pipelines for Docs generation +- Troubleshooting + +#### Integrator + +Integrators actively work on extending and customizing Backstage. This includes developing new plugins, customizing the UI/UX, and integrating external services or data sources. Their work is deeply technical, involving coding, and engaging with the Backstage community for support and collaboration. They need a deep understanding of the Backstage architecture and APIs, working closely with both the framework's backend and frontend aspects. + +##### Documentation Style + +Documentation written for this persona should be software technical, assuming a strong software background. While we can assume an overall technical knowledge, where possible we should link out to useful guides for the technologies we use, ex: Node.js, express.js, React, etc. The goal with documentation written for integrators is to give them a strong understanding of how to use the Backstage framework to build/evolve a company's Backstage Developer Portal, orient them to get support from the open source community, and prepare them for continuing to deliver value for their Backstage Developer Portal. For example: + +- API references +- Frontend and Backend systems +- Package architecture +- Extending the Software Catalog +- Creating custom themes +- Integrating new react components +- Building custom authentication providers/strategies +- Accessibility + +#### Contributor + +Contributors are involved in the development of the Backstage framework itself. They contribute to the core codebase, develop new features, fix bugs, create documentation and maintain the overall health of the project. They are deeply involved in the open-source community, collaborating with maintainers and other contributors to improve the framework and its ecosystem. + +##### Documentation Style + +The goal with documentation written for contributors is to give them a strong understanding of how to contribute to the Backstage framework, orient them to get support from the open source community, and prepare them for continuing to deliver value for the Backstage framework. For example: + +- Contributing to the Backstage framework +- Setting up a development environment +- Writing tests +- Writing documentation + +#### Business Stakeholder + +Business stakeholders use Backstage to align technical capabilities with business goals, monitoring how features and plugins support operational efficiency, developer satisfaction, and strategic objectives. They are involved in defining the strategy and measuring the impact of the developer portal on the organization. They need to navigate through dashboards, reports, and analytics within Backstage to gather insights and make informed decisions. + +##### Documentation Style + +Documentation written for this persona should be strategic, assuming a strong background in business development and strategy. The goal for business documentation is to give a strong understanding of what Backstage Developer Portal can do for their company, how to deliver value quickly and continuously and guides for pitching or driving Backstage adoption. For example: + +- Adoption use cases +- Adoption strategies +- Measuring success +- Case studies + +## Release Plan + +- Release the BEP by 03/24/2024. +- Discuss the changes with the community and gather feedback by 04/24/2024. +- Implement the changes by 04/30/2024. + +## Dependencies + +None + +## Example Table of Contents + +- Overview + - "The overview should introduce users to the concept of Backstage, what an IDP is, how to deliver value, why you should care about DevEx, etc." + - What is Backstage? + - Roadmap + - Vision + - Release and Versioning Policy + - Backstage Threat Model + - Logo assets + - Support and community +- Framework + - Architecture Overview + - "The arch overview should explain how the framework is structured, where plugins and instances fit in and how to understand the current design of Backstage." + - Getting Started + - Integrator/Builder Guides + - Local Development + - "Prepare users for how to develop locally, debug problems, run tests, etc." + - CLI + - Linking in local packages + - Debugging Backstage + - Backstage core framework + - "Internal documentation." + - Systems + - Frontend + - Old + - New + - Backend + - Old + - New + - API Reference + - "Internal documentation" + - Building plugins + - "How to build a plugin, how to integrate it with other plugins, how to deploy and monitor it, and how to iterate on plugin development." + - Intro to plugins + - Existing plugins + - Creating a new plugin + - Plugin development + - Structuring a plugin + - Integrating with other systems + - Integrating with the Catalog + - Integrating Search + - Composability system + - Internationalization + - Plugin analytics + - Feature flags + - OpenAPI + - Backends and APIs + - Testing + - Publishing + - Core Plugins + - "How to leverage the existing plugins for your new plugin or customization options." + - Home Page + - Customizing the home page + - Software Catalog + - Extending the model + - External integrations + - Catalog Customization + - API + - Software Templates + - Writing custom actions + - Writing tests for actions + - Writing custom field extensions + - Writing custom step layouts + - Authorizing parameters, steps and actions + - Migrating to react-jsonschema-form@v5 + - Migrating to v1beta3 templates + - Search + - Overview + - Getting Started with search + - Search concepts + - Search architecture + - Search Engines + - How to Guides + - TechDocs + - Customizing TechDocs + - TechDocs add-ons + - Kubernetes + - Customizing the kubernetes plugin + - Authentication + - Proxy + - Permissions + - Overview + - Concepts + - Getting Started + - Writing a permission policy + - Frontend integration + - Defining custom permission rules + - Using permissions in plugins + - Designing for Backstage + - ADRs + - Accessibility + - References + - Contributor Guides + - "How to get started contributing to OSS." + - Contributing to Backstage + - Reference +- Developer Portal + - Architecture Overview + - Getting Started + - Administrator Guides + - Developer Portal + - "How do I deploy, monitor, configure and verify my Backstage Developer Portal?" + - Installing and Configuring + - Database + - Authentication + - Installing plugins + - Customize the design + - Securing + - Deploying in Production + - Integrating with other systems + - Managing + - Monitoring + - Troubleshooting + - Upgrading + - Keeping backstage up to date + - Customizing + - Core Plugins + - "How do I install and configure Backstage Developer Portal with plugins." + - Home Page + - Installing and Configuring + - Software Catalog + - Overview + - The life of an Entity + - Catalog Configuration + - System Model + - YAML file format + - Entity Reference + - Well Known annotations + - Well known relations + - Well known statuses + - Creating the catalog graph + - Software Templates + - Overview + - Configuring + - Adding a new template + - Writing a template + - Built in actions + - TechDocs + - Overview + - Getting Started + - Architecture + - Installing and configuring + - Using Cloud Storage for TechDocs generated files + - Configuring CI/CD to generate and publish TechDocs sites + - TechDocs CLI + - Troubleshooting + - Kubernetes + - Installing and Configuring + - Authentication + - Troubleshooting + - Search + - Product Manager Guides + - "How do I present Backstage to leadership, what are the benefits, why should I care, etc." + - Strategies for adopting + - Use cases + - User Guides + - "How do I use the default OSS Backstage" + - Logging in + - Registering a component + - Creating a new component + - Reference diff --git a/beps/0009-plugin-metadata/README.md b/beps/0009-plugin-metadata/README.md new file mode 100644 index 0000000000..a23a2d7ce4 --- /dev/null +++ b/beps/0009-plugin-metadata/README.md @@ -0,0 +1,215 @@ +--- +title: Plugin Metadata +status: provisional +authors: + - '@Rugvip' +owners: + - '@backstage/maintainers' +project-areas: + - core +creation-date: yyyy-mm-dd +--- + + + +# BEP: Plugin Metadata + + + +[**Discussion Issue**](https://github.com/backstage/backstage/issues/25076) + +- [Summary](#summary) +- [Motivation](#motivation) + - [Goals](#goals) + - [Non-Goals](#non-goals) +- [Proposal](#proposal) +- [Design Details](#design-details) +- [Release Plan](#release-plan) +- [Dependencies](#dependencies) +- [Alternatives](#alternatives) + +## Summary + +The goal of this BEP is to define a strategy for how we define metadata for Backstage packages, as well as define a new set of metadata for how to associate packages with each other. The proposed solution is to store all plugin metadata in `package.json` under the `backstage` field, unless there is an existing field that can be used instead. Each piece of metadata is documented in the Backstage documentation, and as much of the metadata as possible is generated and validated by the Backstage CLI. The validation is structured in such a way that it does not interfere with internal Backstage setups, and only applies when publishing packages to NPM. + +## Motivation + +All Backstage adopters benefit from it being easy to find, install, configure, and validate the setup of plugins and modules. That's regardless of whether one is only using a minimal set of core plugins, or installing a large number of 3rd party features from the Backstage ecosystem. This proposal aims to build upon our [existing package metadata](https://backstage.io/docs/tooling/cli/build-system#package-roles) to introduce a new set of standardized fields that plugins can use to provide additional information about themselves. + +This metadata will be used to improve tooling in and around Backstage, such as providing better validation and more powerful utilities in the Backstage CLI, and more information to be surfaced in association with plugins for better discoverability. + +### Goals + +We want to provide a new set of standardized fields that makes it possible to build tooling that improves the following interactions with the Backstage ecosystem: + +- Browsing packages related to a plugin, i.e. listing all available library packages and frontend/backend variants of a specific plugin. +- Discoverability of plugin modules. Given a plugin you should be able to identify all of its modules, and given a module you should be able to discovery its plugin. + +Furthermore this proposal should also lay the foundation for how we define additional metadata fields in the future. How they are validated, documented, and surfaced in the Backstage ecosystem. + +In relation to the above, this proposal will aim to define what existing `package.json` fields are known in the Backstage ecosystem, and what their purpose is. This will help avoid duplication of fields, and aim to reduce metadata fragmentation. + +Where appropriate, this new metadata will be made required for all packages that are published using Backstage's tooling. This will ensure that packages across the ecosystem are consistent in how they provide metadata. This requirement should not be unnecessarily strict, ideally only requiring fields that either have sensible defaults or that can be inferred from workspace content. + +These goals must all be reached in a way that does not risk intentional or unintentional disruption of the metadata. For example it should not be possible for an unrelated package to claim to be part of a group of plugin packages that it is not part of. + +### Non-Goals + +The surfacing of extensive implementation metadata is not in scope for this proposal. This includes for example the extension point and extensions exported by a plugin, what route references a frontend plugin has, or what API endpoints a backend plugin exposes. + +This proposal will not aim to standardize any other fields that are not included in the above [goals](#goals). It will provide the foundation for how to define such fields, but not the fields themselves. This includes any informational fields such as package lifecycle or usage, but also fields that provide functional requirements, such as plugin dependencies or environment restrictions. + +This proposal does not aim to provide any direct solution for plugin support metadata as proposed in [#10256](https://github.com/backstage/backstage/pull/10256). Many of the fields defined as followups to this proposal may cover the support metadata use-case, but what is also important for such a solution is to be able to provide local overrides for the metadata, which this proposal does not cover. We do however want to make sure that this use-case is taken into consideration when defining new metadata fields. + +## Proposal + +The proposal is split into separate sections, each contributing to the overall solution in different ways. + +### Validation & Automation + +The purpose of this section is to define how we make it as simple as possible to populate the metadata fields, and how we ensure that the metadata is present and correct. + +Validation and automation is split into two separate phases. The first is phase is manual tooling that is run by the developer and typically validated in CI, for example the `backstage-cli fix` command. The second phase is validation at the time of publishing the package, which is done by the `yarn prepack` command. By splitting the tooling into these two phases we can ensure that a minimal set of metadata is present for all packages published using the Backstage tooling, but at the same time avoid adding unnecessary friction to the development process. + +An important aspect to take into consideration is that most Backstage packages are never published to NPM, as they're part of an internal monorepo and get built directly into an internal Backstage application. Defining the full set of metadata for these packages is likely to be wasteful, which means we should avoid building too many requirements for that path. + +### Package Metadata and Documentation + +This section defines our strategy for defining new metadata fields, and how they are documented. + +To the furthest extent possible we will rely on established standards for `package.json` fields, but we will do so within the bounds of the intended usage of these fields. This means that we will only use existing fields for their intended purpose, and instead define new fields if no existing field is suitable. An index of existing fields is found in the [NPM documentation](https://docs.npmjs.com/cli/v10/configuring-npm/package-json), but we are not limited to this list. Any fields that we can find within the NPM ecosystem may be used, as long as they are well established, for example if there could be a use for the `prettier` field [defined by prettier](https://prettier.io/docs/en/configuration.html#sharing-configurations). + +In the case of new fields we always define them to be part of the top-level `"backstage"` field. The assumption is that any field that we define will always be tied to the Backstage ecosystem, even in the case of general fields such as "lifecycle". The only exception to this is if we believe that a field is generic enough that it could genuinely become part of the broader NPM ecosystem. + +All fields must be documented in a new section of the "Tooling" documentation on backstage.io. This documentation should include a description of the field, what it is used for, and what the expected values are. The documentation must also mention whether the field is expected to be filled in manually, or if it will be generated by tooling. Changes to this documentation must be approved by the Backstage core maintainers, it is not owned by individual project area maintainers. + +### Package Relationships + +This section defines how we associate packages from the same plugin with each other. + +Packages that are part of the same plugin should always be managed within the same monorepo and workspace. This does not apply to modules for a plugin, which may be hosted separately, but may still need to refer to the plugin that it is a module for. + +Each plugin package must define a `backstage.pluginId` field, which is the same identifier as is used in the implementation of the plugin. This field is inferred from the package name by the `backstage-cli repo fix` command if it is not present. It should only be defined for plugin package, for example `@backstage/errors` should not define a plugin ID. The `backstage.pluginId` field is required when publishing a package with a plugin or module role, or a library role with "plugin" in its name. + +The package relationships are defined in the `backstage.pluginPackages` field. The value of the field is an object where each key is the role of the package as defined by the `backstage.role` field, and the value is the package name. For example: + +```json +{ + "name": "@backstage/plugin-catalog", + "backstage": { + "role": "frontend-plugin", + "pluginId": "catalog", + "pluginPackages": { + "frontend-plugin": "@backstage/plugin-catalog", + "backend-plugin": "@backstage/plugin-catalog-backend", + "web-library": "@backstage/plugin-catalog-react", + "node-library": "@backstage/plugin-catalog-node", + "common-library": "@backstage/plugin-catalog-common" + } + } +} +``` + +The `backstage.pluginPackages` field is generated and updated by the `backstage-cli repo fix` command based on the packages that are present in the workspace and their `backstage.pluginId` and `backstage.role` fields. There can only be a single package of each role with a given plugin ID. The `backstage.pluginPackages` field is required when publishing a package with a `backstage.pluginId` field that is not using a module role. + +Module packages define their target plugin both via the `backstage.pluginId` field, as well as via `backstage.pluginPackage`. For example: + +```json +{ + "name": "@backstage/plugin-catalog-backend-module-github", + "backstage": { + "role": "backend-plugin-module", + "pluginId": "catalog", + "pluginPackage": "@backstage/plugin-catalog-backend" + } +} +``` + +The `backstage.pluginPackage` field is required when publishing a package with a module role. + +## Design Details + +### Package Relationships Conflict Resolution + +Conflicts in the defined metadata are handled as follows: + +- The union of all `pluginPackages` is always used. If there are multiple packages defined for the same role, conflicts are resolved using the `backstage.moved` field if possible, but otherwise all packages are rejected. +- All plugin packages with references to each other must have the same `pluginId`, or they are all rejected. +- A module must have the same `pluginId` as the plugin it is a module for, or it is rejected. +- If the role of a package does not match the definition in the `pluginPackages` field, the package is rejected. + +When metadata is "rejected" the consumer should either throw an error, or treat the conflicted fields as if they do not exist. + +### References & Prior Art + +The following resources have been used to inform this proposal: + +- [package.json documentation](https://docs.npmjs.com/cli/v10/configuring-npm/package-json) +- [VSCode Extensions](https://code.visualstudio.com/api/references/extension-manifest) + +## Release Plan + +The new metadata fields and any generation and validation of fields will initially be rolled out as part of the `repo fix` command in the Backstage CLI. In addition we will also add validation to the `yarn prepack` command in such a way that it does not interfere with backend package bundling, but does apply when packing packages for publishing. These changes will all be rolled out in a single release of the Backstage CLI, meaning that anyone publishing Backstage packages using the Backstage CLI will be required to provide the new metadata. + +## Dependencies + +None + +## Alternatives + +### Simplified Library Relationships + +Rather than listing libraries for each role, we could simply have an array of libraries that are available for the plugin: + +```json +{ + "name": "@backstage/plugin-catalog", + "backstage": { + "role": "frontend-plugin", + "pluginId": "catalog", + "pluginPackages": { + "frontend-plugin": "@backstage/plugin-catalog", + "backend-plugin": "@backstage/plugin-catalog-backend", + "libraries": [ + "@backstage/plugin-catalog-react", + "@backstage/plugin-catalog-node", + "@backstage/plugin-catalog-common" + ] + } + } +} +``` + +A benefit of this approach is that it keeps the metadata simpler and reduces the need for conflict resolution logic since we can use the union of all listed library packages. + +It also allows for the use-case of having multiple libraries of the same role, in case that would provide a benefit. Examples of this are the `@backstage/catalog-client` and `@backstage/catalog-model` packages. + +A downside of this approach could be that it may encourage a larger number of library packages, which is not necessarily what we want. Another downside is that the role of each package is not immediately available. + +### Separate Metadata File + +This proposal suggests that we keep metadata in the `package.json` file. An alternative would be to define a separate metadata file, for example `backstage.json` or `backstage.yaml`. + +A benefit of this approach is that we do not pollute the `package.json` file with additional metadata, which can help keep overall metadata easier to browse and understand. It also allows us to use our own format, such as allowing comments in JSON, or using YAML for easier readability. + +A downside of this approach is that it introduces a bit more complexity and friction in the tooling and package publishing, because we need to make sure that the metadata file is always included in the published package. We also end up with additional logic for finding and parsing this file, whereas resolving and parsing `package.json` is already a solved problem. + +One of the larger benefits of keeping package metadata `package.json` is that it makes the data immediately available in registry APIs. It avoids the need to download and parse additional files to get the metadata for a package. For this reason we believe that we should stick to using `package.json` for metadata for the time being. If we for some reason in the future find that we need to use a separate file, for example because the metadata becomes too large, we can always evolve the `backstage` field in a couple of different ways: + +```json +{ + // Moving all data to a separate file + "backstage": "backstage.json" +} +{ + // Moving some data to a separate file + "backstage": { + "role": "frontend-plugin", + "metadata": "backstage.json" + } +} +``` diff --git a/docs/accessibility/index.md b/docs/accessibility/index.md index b4d63fd4e4..da10d60dd2 100644 --- a/docs/accessibility/index.md +++ b/docs/accessibility/index.md @@ -47,7 +47,10 @@ If you want to use the Lighthouse CLI and run the checks based on the config you yarn dlx @lhci/cli@0.11.x autorun ``` -> Note: running this command will use the [Lighthouse config](https://github.com/backstage/backstage/blob/39ba2284d73885b7ca8290cb38e2b1e4d983c8d6/lighthouserc.js#L19-L34) so make sure to adjust it to your needs if needed. +:::note Note +Running this command will use the [Lighthouse config](https://github.com/backstage/backstage/blob/39ba2284d73885b7ca8290cb38e2b1e4d983c8d6/lighthouserc.js#L19-L34) so make sure to adjust it to your needs if needed. + +::: ### Use Lighthouse Github Action on your own repo diff --git a/docs/auth/add-auth-provider.md b/docs/auth/add-auth-provider.md index a43e2307e6..d1dad0b183 100644 --- a/docs/auth/add-auth-provider.md +++ b/docs/auth/add-auth-provider.md @@ -4,10 +4,14 @@ title: Contributing New Providers description: Documentation on adding new authentication providers --- -> NOTE: The primary audience for this documentation are contributors to the main -> Backstage project that want to add support for new authentication providers. -> While you can follow it to implement your own custom providers it is much -> more advanced than using our built-in providers. +:::note Note + +The primary audience for this documentation are contributors to the main +Backstage project that want to add support for new authentication providers. +While you can follow it to implement your own custom providers it is much +more advanced than using our built-in providers. + +::: ## How Does Authentication Work? diff --git a/docs/auth/oauth2-proxy/provider.md b/docs/auth/oauth2-proxy/provider.md index 8745f32297..dca074cc7d 100644 --- a/docs/auth/oauth2-proxy/provider.md +++ b/docs/auth/oauth2-proxy/provider.md @@ -23,13 +23,12 @@ auth: environment: development providers: oauth2Proxy: - development: - signIn: - resolvers: - # typically you would pick one of these - - resolver: emailMatchingUserEntityProfileEmail - - resolver: emailLocalPartMatchingUserEntityName - - resolver: forwardedUserMatchingUserEntityName + signIn: + resolvers: + # typically you would pick one of these + - resolver: emailMatchingUserEntityProfileEmail + - resolver: emailLocalPartMatchingUserEntityName + - resolver: forwardedUserMatchingUserEntityName ``` ### Resolvers diff --git a/docs/auth/service-to-service-auth.md b/docs/auth/service-to-service-auth.md index c08411aca5..8c9130173c 100644 --- a/docs/auth/service-to-service-auth.md +++ b/docs/auth/service-to-service-auth.md @@ -27,6 +27,45 @@ any configuration. They generate self-signed tokens automatically for making requests to other Backstage backend plugins, and the receivers use the caller's public key set endpoint to be able to perform verification. +A backend plugin wishing to make a request to another backend plugin acquires +the required token as follows, where `auth` and `httpAuth` are assumed to be +injected from `coreServices.auth` and `coreServices.httpAuth`, respectively: + +```ts +const credentials = await httpAuth.credentials(req); +const { token } = await auth.getPluginRequestToken({ + onBehalfOf: credentials, + targetPluginId: '', // e.g. 'catalog' +}); +``` + +In this example we are assuming that we are in an Express request handler, and +we extract the caller credentials (typically a user or a service) out of its +`req` and make the upstream request on-behalf-of that principal. Prefer to use +this pattern wherever there's an incoming set of credentials to refer to. + +If you want to initiate a request entirely as your own service, not on behalf of +anybody else, you can do so as follows: + +```ts +const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await auth.getOwnServiceCredentials(), + targetPluginId: '', // e.g. 'catalog' +}); +``` + +Callers pass along the tokens verbatim with requests in the `Authorization` +header: + +```yaml +Authorization: Bearer eyJhbG... +``` + +You may occasionally also see some code, e.g. clients to other systems, that +accept a `credentials` argument directly instead of a token. For those, just +pass in the credentials as acquired above, instead of making a token. The client +code will know what to do with those credentials internally. + This flow has only one configuration option to set in your app-config: `backend.auth.dangerouslyDisableDefaultAuthPolicy`, which can be set to `true` if you for some reason need to completely disable both the issuing and @@ -55,6 +94,9 @@ backend: options: token: ${CICD_TOKEN} subject: cicd-system-completion-events + # Restrictions are optional; see below + accessRestrictions: + - plugin: events - type: static options: token: ${ADMIN_CURL_TOKEN} @@ -73,13 +115,65 @@ The subjects must be strings without whitespace. They are used for identifying each caller, and become part of the credentials object that request recipient plugins get. -Callers pass along the tokens verbatim with requests in the `Authorization` -header: +Callers must pass along tokens verbatim with requests in the `Authorization` +header when calling Backstage plugins: ```yaml Authorization: Bearer eZv5o+fW3KnR3kVabMW4ZcDNLPl8nmMW ``` +## JWKS Token Auth + +This access method allows for external caller token authentication using configured +JSON Web Key Sets (JWKS). This is useful for callers that are authenticating to our +instance of Backstage with third-party tools, such as Auth0. + +You can configure this access method by adding one or more entries of type `jwks` +to the `backend.auth.externalAccess` app-config key: + +```yaml title="in e.g. app-config.production.yaml" +backend: + auth: + externalAccess: + - type: jwks + options: + url: https://example.com/.well-known/jwks.json + issuer: https://example.com + algorithm: RS256 + audience: example, other-example + subjectPrefix: custom-prefix + - type: jwks + options: + url: https://another-example.com/.well-known/jwks.json + issuer: https://example.com +``` + +The URL should point at an unauthenticated endpoint that returns the JWKS. + +`issuer` specifies the issuer(s) of the JWT that the authenticating app will accept. +Passed JWTs must have an `iss` claim which matches one of the specified issuers. + +`algorithm` specifies the algorithm(s) that are used to verify the JWT. The passed JWTs +must have been signed using one of the listed algorithms. + +`audience` specifies the intended audience(s) of the JWT. The passed JWTs must have an "aud" +claim that matches one of the audiences specified, or have no audience specified. + +For additional details regarding the JWKS configuration, please consult your authentication +provider's documentation. + +The subject returned from the token verification will become part of the +credentials object that the request recipient plugins get. All subjects will have the prefix +`external:`, but you can also provide a custom subjectPrefix which will get appended before the +subject returned from your JWKS service (ex. `external:custom-prefix:sub`). + +Callers must pass along tokens with requests in the `Authorization` header when +calling Backstage plugins: + +```yaml +Authorization: Bearer eyJhbG... +``` + ## Legacy Tokens Plugins and backends that are _not_ on the new backend system use a legacy token @@ -156,8 +250,12 @@ payload: - `sub`: the exact string "backstage-server" - `exp`: one hour from the time it was generated, in epoch seconds -> NOTE: The JWT must encode the `alg` header as a protected header, such as with -> [setProtectedHeader](https://github.com/panva/jose/blob/main/docs/classes/jwt_sign.SignJWT.md#setprotectedheader). +:::note Note + +The JWT must encode the `alg` header as a protected header, such as with +[setProtectedHeader](https://github.com/panva/jose/blob/main/docs/classes/jwt_sign.SignJWT.md#setprotectedheader). + +::: The caller then passes along the JWT token with requests in the `Authorization` header: @@ -165,3 +263,97 @@ header: ```yaml Authorization: Bearer eZv5o+fW3KnR3kVabMW4ZcDNLPl8nmMW ``` + +## Access Restrictions + +Each `externalAccess` entry may optionally have an `accessRestrictions` key, +which limits what that particular access method can do. Let's look at an +example: + +```yaml title="in e.g. app-config.production.yaml" +backend: + auth: + externalAccess: + - type: static + options: + token: ${CICD_TOKEN} + subject: cicd-system-completion-events + accessRestrictions: + - plugin: events +``` + +In this short example there's only one entry. It says that for anyone trying to +make access with the CICD token, they will be rejected if they try to contact +anything but the `events` backend plugin. You could add additional entries to +the array that allow targeting more plugins if that's what you want. + +:::note Note + +If no `accessRestrictions` are added, the access method has unlimited access to +all functionality of all plugins. It is recommended that you try to specify +access restrictions whenever possible, to reduce risk. + +::: + +Each entry has one or more of the following fields: + +- **`plugin`**: Required. A plugin ID as a string, for example `'catalog'`. Permits + access to make requests to this plugin. Can be further refined by setting + additional fields as per below. + + Example: + + ```yaml + accessRestrictions: + # access to any other plugin will be rejected + - plugin: my-plugin + ``` + +- **`permission`**: Optional. A collection (comma/space separated string or + string array) of permission names. If given, this method is limited to only + performing actions with these named permissions in the plugin with the ID + given above. + + Note that this only applies where permissions checks are enabled in the first + place. Endpoints that are not protected by the permissions system at all, are + not affected by this setting. + + Example: + + ```yaml + accessRestrictions: + - plugin: my-plugin + # Any other permission check will be rejected. + permission: + - my-plugin.add-item + - my-plugin.remove-item + # Also supports the shorthand form: + # permission: my-plugin.add-item, my-plugin.remove-item + ``` + +- **`permissionAttribute`**: Optional. A key-value object of permission attributes + where each value is a collection (comma/space separated string or string + array) of allowed such values. If given, this method is limited to only + performing actions whose permissions have these attributes. + + Note that this only applies where permissions checks are + enabled in the first place. Endpoints that are not protected by + the permissions system at all, are not affected by this + setting. + + In practice, this is typically used to limit by the `action` attribute, for + `'create'`, `'read'`, `'update'`, or `'delete'` values. + + Example: + + ```yaml + accessRestrictions: + - plugin: my-plugin + permissionAttribute: + # Updates and deletes will be rejected. + action: + - create + - read + # Also supports the shorthand form: + # action: create, read + ``` diff --git a/docs/backend-system/architecture/01-index.md b/docs/backend-system/architecture/01-index.md index 681d0fada2..968ff2f001 100644 --- a/docs/backend-system/architecture/01-index.md +++ b/docs/backend-system/architecture/01-index.md @@ -19,7 +19,11 @@ The diagram below provides an overview of the different building blocks, and the ![backend system building blocks diagram](../../assets/backend-system/architecture-building-blocks.drawio.svg) -> NOTE: These are all concepts that existed in our old backend system in one way or another, but they have now all been lifted up to be first class concerns. +:::note Note + +These are all concepts that existed in our old backend system in one way or another, but they have now all been lifted up to be first class concerns. + +::: ### Backend diff --git a/docs/backend-system/architecture/03-services.md b/docs/backend-system/architecture/03-services.md index 573f6612c2..777eddd3c4 100644 --- a/docs/backend-system/architecture/03-services.md +++ b/docs/backend-system/architecture/03-services.md @@ -111,7 +111,7 @@ There are only two possible scopes for services, `'plugin'` and `'root'`. ## Root Scoped Services -If a service is defined as a root scoped service, the implementation created by the factory will be shared across all plugins and services. One other differentiating factory for root scoped services is that they are always initialized, regardless of whether any plugins depend on them or not. This makes them suitable for implementing backend-wide concerns that are not specific to any individual plugin. +If a service is defined as a root scoped service, the implementation created by the factory will be shared across all plugins and services. One other differentiating factor for root scoped services is that they are always initialized, regardless of whether any plugins depend on them or not. This makes them suitable for implementing backend-wide concerns that are not specific to any individual plugin. There is a limitation in the usage of root scoped services, which is that their implementation can only depend on other root scoped services. Plugin scoped services on the other hand can depend on both root and plugin scoped services. Because of this limitation, one of the main reasons to define a root scoped services is to make it possible for other root scoped services to depend on it. @@ -157,7 +157,7 @@ export const fooServiceFactory = createServiceFactory({ }); ``` -Whatever value is returned by the `createRootContext` function will shared and passed as the second argument to each invocation of the `factory` function. That way you can create a shared context that is used in the creation of each plugin instance. Unlike the `factory` function, the `createRootContext` function will only receive root scoped services as its dependencies, but just like the `factory` function, it can also be `async`. +Whatever value is returned by the `createRootContext` function will be shared and passed as the second argument to each invocation of the `factory` function. That way you can create a shared context that is used in the creation of each plugin instance. Unlike the `factory` function, the `createRootContext` function will only receive root scoped services as its dependencies, but just like the `factory` function, it can also be `async`. ## Default Service Factories @@ -192,7 +192,11 @@ When defining a default factory for a service, it is possible for it to end up w ## Service Factory Options -> NOTE: This pattern is discouraged, only use it when necessary. If possible you should prefer to make services configurable via static configuration instead. +:::note Note + +This pattern is discouraged, only use it when necessary. If possible you should prefer to make services configurable via static configuration instead. + +::: When declaring a service factory it's possible to include an options callback. This allows you to customize the factory through code when installing it in the backend. For example, this is how you install an explicit factory instance in the backend without any options: diff --git a/docs/backend-system/architecture/04-plugins.md b/docs/backend-system/architecture/04-plugins.md index 195c32b049..af59c2a48a 100644 --- a/docs/backend-system/architecture/04-plugins.md +++ b/docs/backend-system/architecture/04-plugins.md @@ -78,4 +78,4 @@ Plugins must always be designed to be horizontally scalable. This means that you ### Isolated -Plugins must never communicate with each other directly through code, they may only communicate over the network. Plugins that wish to expose an external interface for other plugins and modules to use are recommended to do so through a [node-library](../../local-dev/cli-build-system.md#package-roles) package. The library should export an API client service to make calls to your plugin, or similar construct. +Plugins must never communicate with each other directly through code, they may only communicate over the network. Plugins that wish to expose an external interface for other plugins and modules to use are recommended to do so through a [node-library](../../tooling/cli/02-build-system.md#package-roles) package. The library should export an API client service to make calls to your plugin, or similar construct. diff --git a/docs/backend-system/architecture/07-naming-patterns.md b/docs/backend-system/architecture/07-naming-patterns.md index f45355acf0..32ed1d9035 100644 --- a/docs/backend-system/architecture/07-naming-patterns.md +++ b/docs/backend-system/architecture/07-naming-patterns.md @@ -91,6 +91,6 @@ export const catalogClientServiceFactory = createServiceFactory({ }) ``` -An exception to the above service reference naming pattern has been made for the all of the core services in the core API. The `@backstage/backend-plugin-api` makes all core service references available via a single `coreServices` collection. Likewise, the `@backstage/backend-test-utils` exports all mock service implementations via a single `mockServices` collection. This means that the table above is slightly misleading, since `loggerServiceRef` and `databaseServiceRef` are instead available as `coreServices.logger` and `coreService.database`. We recommend that plugins avoid this patterns unless they have a very large number of services that they need to export. +An exception to the above service reference naming pattern has been made for all of the core services in the core API. The `@backstage/backend-plugin-api` makes all core service references available via a single `coreServices` collection. Likewise, the `@backstage/backend-test-utils` exports all mock service implementations via a single `mockServices` collection. This means that the table above is slightly misleading, since `loggerServiceRef` and `databaseServiceRef` are instead available as `coreServices.logger` and `coreService.database`. We recommend that plugins avoid this patterns unless they have a very large number of services that they need to export. While it is often preferred to prefix root scoped services with `Root`, it is not required. For example, `RootHttpRouterService` and `RootLifecycleService` follow this pattern, but `ConfigService` doesn't and it is a root scoped service. diff --git a/docs/backend-system/building-backends/01-index.md b/docs/backend-system/building-backends/01-index.md index 44aa0c7e28..2fb89e45af 100644 --- a/docs/backend-system/building-backends/01-index.md +++ b/docs/backend-system/building-backends/01-index.md @@ -46,7 +46,7 @@ Apart from installing existing plugins and modules in the backend, there are a c ### Configuration -Perhaps the most accessible way is though static configuration, which you can read more about in the documentation for how to [write configuration](../../conf/writing.md). Many different aspects of the backend can be configured, including both the behavior of the backend itself, as well as many plugins or modules. You'll need to refer to the documentation of each plugin or module to see what configuration is available. Also be sure to check out the documentation of the [core services](../core-services/01-index.md), as that also covers how to configure those. +Perhaps the most accessible way is through static configuration, which you can read more about in the documentation for how to [write configuration](../../conf/writing.md). Many different aspects of the backend can be configured, including both the behavior of the backend itself, as well as many plugins or modules. You'll need to refer to the documentation of each plugin or module to see what configuration is available. Also be sure to check out the documentation of the [core services](../core-services/01-index.md), as that also covers how to configure those. ### Services @@ -107,7 +107,7 @@ This example touches on the fact that services can have different scopes, being A more advanced way to deploy Backstage is to split the backend plugins into multiple different backend deployments. Both the [deployment documentation](../../deployment/scaling.md) and [Threat Model](../../overview/threat-model.md#trust-model) explain the benefits of this, so here we'll focus on how to do it. -To create a separate backend we need to create an additional backend package. This package will be built and deployed separately from your existing backend. There is currently no template to create a backend via `yarn new`, so the quickest way is to copy the new package and modify. The naming is up to you and it depends on how you are splitting things and up. For this example we'll just use a simple suffix. You might end up with a directory structure like this: +To create a separate backend we need to create an additional backend package. This package will be built and deployed separately from your existing backend. There is currently no template to create a backend via `yarn new`, so the quickest way is to copy the new package and modify. The naming is up to you and it depends on how you are splitting things up. For this example we'll just use a simple suffix. You might end up with a directory structure like this: ```text packages/ diff --git a/docs/backend-system/building-backends/08-migrating.md b/docs/backend-system/building-backends/08-migrating.md index ae41193b11..d38acb695e 100644 --- a/docs/backend-system/building-backends/08-migrating.md +++ b/docs/backend-system/building-backends/08-migrating.md @@ -1369,7 +1369,7 @@ The vast majority of the backend plugins that currently live in the Backstage Re | @backstage/plugin-catalog-backend-module-github-org | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-github-org/README.md) | | @backstage/plugin-catalog-backend-module-gitlab | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-gitlab/README.md) | | @backstage/plugin-catalog-backend-module-incremental-ingestion | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-incremental-ingestion/README.md) | -| @backstage/plugin-catalog-backend-module-ldap | backend-plugin-module | | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-ldap/README.md) | +| @backstage/plugin-catalog-backend-module-ldap | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-ldap/README.md) | | @backstage/plugin-catalog-backend-module-msgraph | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-msgraph/README.md) | | @backstage/plugin-catalog-backend-module-openapi | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-openapi/README.md) | | @backstage/plugin-catalog-backend-module-puppetdb | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-puppetdb/README.md) | diff --git a/docs/backend-system/building-plugins-and-modules/01-index.md b/docs/backend-system/building-plugins-and-modules/01-index.md index 1aa8b140ed..379ce19587 100644 --- a/docs/backend-system/building-plugins-and-modules/01-index.md +++ b/docs/backend-system/building-plugins-and-modules/01-index.md @@ -6,8 +6,12 @@ sidebar_label: Overview description: Building backend plugins and modules using the new backend system --- -> NOTE: If you have an existing backend and/or backend plugins that are not yet -> using the new backend system, see [migrating](./08-migrating.md). +:::note Note + +If you have an existing backend and/or backend plugins that are not yet +using the new backend system, see [migrating](./08-migrating.md). + +::: This section covers how to build your own backend [plugins](../architecture/04-plugins.md) and [modules](../architecture/06-modules.md). They are sometimes collectively referred to as diff --git a/docs/backend-system/building-plugins-and-modules/08-migrating.md b/docs/backend-system/building-plugins-and-modules/08-migrating.md index 8a31251ee8..699852c494 100644 --- a/docs/backend-system/building-plugins-and-modules/08-migrating.md +++ b/docs/backend-system/building-plugins-and-modules/08-migrating.md @@ -39,7 +39,7 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { catalogServiceRef } from '@backstage/plugin-catalog-node'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; import { Router } from 'express'; import { KubernetesBuilder } from './KubernetesBuilder'; @@ -214,3 +214,44 @@ The above module can then be installed by the integrator alongside the kubernete backend.add(import('@backstage/plugin-kubernetes-backend')); backend.add(import('@internal/gke-cluster-supplier')); ``` + +### Dev Server + +Follow the steps below to run your migrated plugin on a local development server: + +1. First, delete the `src/run.ts` and `src/service/standaloneServer.ts` files in case they exist (the `backstage-cli` previously used these files to run legacy backend plugins locally, but they are no longer required). + +2. Next, create a new development backend in the `dev/index.ts` file. The dev server is a lite version of a backend app that is mainly used to run your plugin locally, so a simple `kubernetes` backend local development server would look like this: + +```ts title="in dev/index.js" +// This package should be installed as a `dev` dependency +import { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); +// Path to the file where the plugin is export as default +backend.add(import('../src')); +backend.start(); +``` + +The development server created above will be automatically configured with the default dependency factories, but if you need to mock some of the services your plugin relies on, such as the `rootConfig` service, you can use one of the `mockServices` factories: + +```ts title="in dev/index.js" +//... +// This package should be installed as `devDependecies` +import { mockServices } from '@backstage/backend-test-utils'; + +const backend = createBackend(); +// ... +backend.add( + mockServices.rootConfig.factory({ + data: { + // your config mocked values goes here + }, + }), +); +// ... +``` + +Checkout the [custom service implementations](https://backstage.io/docs/backend-system/building-backends/index#custom-service-implementations) documentation and also the [core service configurations](https://backstage.io/docs/backend-system/core-services/index) page in case you'd like to create your own custom mock factory for one or more services. + +3. Now you can finally start your plugin locally by running `yarn start` from the root folder of your plugin. diff --git a/docs/backend-system/core-services/auth.md b/docs/backend-system/core-services/auth.md index 4f1229a9d7..f1243f11fe 100644 --- a/docs/backend-system/core-services/auth.md +++ b/docs/backend-system/core-services/auth.md @@ -5,4 +5,136 @@ sidebar_label: Auth description: Documentation for the Auth service --- -TODO +This service deals with the generation and verification of tokens and their +associated representations as credentials objects. You can use it for validating +incoming tokens, and generating tokens for calling other services. + +If you want to deal with credentials specifically in the HTTP request/response +flow, see also [the `httpAuth` service](./http-auth.md). If you want to extract +more details about authenticated users such as their ownership entity refs, use +[the `userInfo` service](./user-info.md). + +## Using the Service + +In the following code examples, the `auth` and `httpAuth` variables are assumed +to be dependency-injected instances of the `coreServices.auth` and +`coreServices.httpAuth` service, respectively. For a backend plugin, it might +look like this: + +```ts +export default createBackendPlugin({ + pluginId: 'my-plugin', + register(env) { + env.registerInit({ + deps: { + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, + httpRouter: coreServices.httpRouter, + }, + async init({ auth, httpAuth, httpRouter }) { + // Your code goes here + }, + }); + }, +}); +``` + +### Creating Request Tokens + +If you need to create a token that can be used for making a request to another +backend plugin: + +```ts +const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', +}); +``` + +:::note Note + +Never store and reuse tokens. Always call `getPluginRequestToken` immediately +before making a request. Otherwise you run the risk of running into permission +problems when expired tokens are being used for requests. + +::: + +This example is suitable when you need to make the request "as your own plugin", +i.e. when your code is the original initiator of the call. An example of this +could be periodic batch processes that index content in another service. + +In situations where you are making a call on-behalf-of someone else, for example +when making upstream requests inside a request handler, please always instead +use the extracted credentials from the request. + +```ts +router.get('/makes-calls', async (req, res) => { + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await httpAuth.credentials(req), + targetPluginId: 'catalog', + }); + // make a call using the token +``` + +This ensures that the original caller and their associated permissions are +properly carried along with the request chain. See [the `httpAuth` service docs](./http-auth.md) +for more details. + +The [service to service auth docs](../../auth/service-to-service-auth.md) +contain more details about how to properly use tokens in your HTTP request +paths. + +### Authorizing Tokens + +Most plugins should not deal with incoming request tokens directly at all, but +rather use [`httpAuth.credentials`](./http-auth.md) instead as part of their +request handlers. But in the rare cases where you are holding an incoming token +and want to validate it and turn it into a credentials object, you can do so: + +```ts +const credentials = await auth.authenticate(token); +``` + +There is an optional second parameter that you can set to `{ allowLimitedAccess: +true }` if you specifically built a plugin that deals with cookie based access, +which is rare. + +### Inspecting Credentials + +The `auth` service also contains facilities for working with credentials +objects. For example checking what type of principal (caller type - e.g. user or +service) they represent. For example: + +```ts +if (auth.isPrincipal(credentials, 'user)) { + // In here, the TypeScript type of the credentials object has been properly + // narrowed to `BackstageCredentials` so you can + // access its specific properties such as `credentials.principal.userEntityRef`. +} +``` + +## Configuring the service + +:::note Note + +The `auth` service is not suitable for having its implementation replaced +entirely in your private repo. If you desire additional service auth related +features, don't hesitate to [file an issue](https://github.com/backstage/backstage/issues/new/choose) +or [contribute](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) to the open source features. + +::: + +For configuring service-to-service access methods, see [the auth docs](../../auth/service-to-service-auth.md). + +The default auth policy requires all requests to be authenticated with either +user or service credentials. This can be disabled by setting the +`backend.auth.dangerouslyDisableDefaultAuthPolicy` app-config flag to `true`. +Disabling this check means that the backend will no longer block unauthenticated +requests, but instead allow them to pass through to plugins. Do not do this in +production unless absolutely necessary. + +If permissions are enabled, unauthenticated requests will be treated exactly as +such, leaving it to the permission policy to determine what permissions should +be allowed for an unauthenticated identity. Note that this will also apply to +service-to-service calls between plugins unless you configure credentials for +service calls. diff --git a/docs/backend-system/core-services/http-auth.md b/docs/backend-system/core-services/http-auth.md index 7af30c3251..05a26fc075 100644 --- a/docs/backend-system/core-services/http-auth.md +++ b/docs/backend-system/core-services/http-auth.md @@ -5,4 +5,102 @@ sidebar_label: Http Auth description: Documentation for the Http Auth service --- -TODO +The `httpAuth` service deals with submitting and receiving credentials on Express +HTTP request/response objects. This service is frequently used in plugins that +have REST interfaces. + +If you want to deal with raw tokens and do low level credentials handling, see +also [the `auth` service](./auth.md). If you want to extract more details about +authenticated users such as their ownership entity refs, use [the `userInfo` service](./user-info.md). + +## Using the Service + +In the following code examples, the `auth` and `httpAuth` variables are assumed +to be dependency-injected instances of the `coreServices.auth` and +`coreServices.httpAuth` service, respectively. For a backend plugin, it might +look like this: + +```ts +export default createBackendPlugin({ + pluginId: 'my-plugin', + register(env) { + env.registerInit({ + deps: { + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, + httpRouter: coreServices.httpRouter, + }, + async init({ auth, httpAuth, httpRouter }) { + // Your code goes here + }, + }); + }, +}); +``` + +### Getting Request Credentials + +If you need to extract the validated credentials out of an incoming request, you +can do so like this: + +```ts +router.get('/some-request', async (req, res) => { + const credentials = await httpAuth.credentials(req, { allow: ['user'] }); + // Do something with the credentials here +``` + +The second argument is optional, but in this example we specified that we only +want to allow user based requests. The credentials returned will then have a +narrowed TypeScript type that reflects that the principal is known to be of the +user type. This second argument can also specify `allowLimitedAccess: true` if +you specifically built a plugin that deals with cookie based access, which is +rare. + +The default is to accept both service and user credentials (excluding limited +access), but in the example above, any attempt to call this endpoint with +service credentials will result in an Unauthorized error being thrown. + +:::note Note + +You don't need to call `httpAuth.credentials` _just_ to ensure that incoming +credentials are valid in the first place; only use this method if you actually +need to act upon the credentials somehow. The Backstage backend framework will have +ensured the actual validity of any incoming token before your backend code is +reached. The policy for these upfront framework level rules is controlled using +[the `httpRouter` service](./http-router.md) when you register your routes. + +::: + +If you want to further work with the credentials object, [the `auth` service](./auth.md) +has helper methods for that. + +### Issuing Cookies + +For some rare use cases, plugins may want to issue cookies with _limited access_ +user credentials. This is mostly relevant when browsers need to be able to +request static resources, such as in the TechDocs plugin. + +Plugins should almost never interact with the cookie functionality of the +`httpAuth` service directly. The framework has builtin handling of cookie +creation/deletion requests on a dedicated well-known endpoint. All you normally +have to do to accept limited user access is to inform [the `httpRouter` service](./http-router.md) +when creating your route that you want to permit +cookie based access for a specific route, and then setting `allowLimitedAccess` +to `true` when extracting credentials. + +Due to the above, we do not document the `httpAuth.issueUserCookie` method here. + +## Configuring the service + +:::note Note + +The `httpAuth` service is not suitable for having its implementation replaced +entirely in your private repo. If you desire additional service auth related +features, don't hesitate to [file an issue](https://github.com/backstage/backstage/issues/new/choose) +or [contribute](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) to the open source features. + +::: + +This service has no configuration options, but it abides by the policies you +have set up using [the `httpRouter` service](./http-router.md) for your routes, +if any. diff --git a/docs/backend-system/core-services/http-router.md b/docs/backend-system/core-services/http-router.md index 4dfa7d80e1..6ab8218c83 100644 --- a/docs/backend-system/core-services/http-router.md +++ b/docs/backend-system/core-services/http-router.md @@ -5,7 +5,8 @@ sidebar_label: Http Router description: Documentation for the Http Router service --- -One of the most common services is the HTTP router service which is used to expose HTTP endpoints for other plugins to consume. +One of the most common services is the HTTP router service which is used to +expose HTTP endpoints for other plugins to consume. ## Using the service @@ -37,6 +38,36 @@ createBackendPlugin({ }); ``` +This service is also responsible for keeping track of the auth policies that +apply to your routes. The default policy is to require that auth is present with +every incoming request, and to accept both service and user credentials +(excluding limited access tokens). You can override this while registering your +routes. This dangerously allows unauthenticated access on a specific route: + +```ts +http.addAuthPolicy({ + path: '/static/:id', + allow: 'unauthenticated', +}); +``` + +Note that the path is exactly the same format as what you used in your routes, +including placeholders. + +If your plugin uses cookie based access (which is rare), you need to allow that +as follows: + +```ts +http.addAuthPolicy({ + path: '/static/:id', + allow: 'user-cookie', +}); +``` + +For those routes you will also have to specify `allowLimitedAccess: true` when +using the [`auth`](./auth.md) and [`httpAuth`](./http-auth.md) services to +access the incoming credentials. + ## Configuring the service This service does not have any configuration options. diff --git a/docs/backend-system/core-services/permissions.md b/docs/backend-system/core-services/permissions.md index d979932c2f..06e966bbb2 100644 --- a/docs/backend-system/core-services/permissions.md +++ b/docs/backend-system/core-services/permissions.md @@ -16,36 +16,42 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { Router } from 'express'; +import { NotAllowedError } from '@backstage/errors'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; +import Router from 'express-promise-router'; -createBackendPlugin({ +export default createBackendPlugin({ pluginId: 'example', register(env) { env.registerInit({ deps: { permissions: coreServices.permissions, - http: coreServices.httpRouter, + httpRouter: coreServices.httpRouter, + httpAuth: coreServices.httpAuth, }, - async init({ permissions, http }) { - const router = Router(); - router.get('/test-me', (request, response) => { - // use the identity service to pull out the token from request headers - const { token } = await identity.getIdentity({ - request, - }); - - // ask the permissions framework what the decision is for the permission + async init({ permissions, httpRouter, httpAuth }) { + const endpoints = Router(); + endpoints.get('/test-me', (request, response) => { + // Ask the permissions framework what the decision is for the given + // permission, for the principal that made the original request. The + // `httpAuth` service helps us extract those credentials. We authorize + // a single permission here, so the result will be an array with one + // element accordingly. const permissionResponse = await permissions.authorize( - [ - { - permission: myCustomPermission, - }, - ], - { token }, + [{ permission: myCustomPermission }], + { credentials: await httpAuth.credentials(request) }, ); + + if (permissionResponse[0].result !== AuthorizeResult.ALLOW) { + throw new NotAllowedError( + 'You are not permitted to perform this action', + ); + } + + // TODO: Actual code goes here }); - http.use(router); + httpRouter.use(endpoints); }, }); }, diff --git a/docs/backend-system/core-services/plugin-metadata.md b/docs/backend-system/core-services/plugin-metadata.md index f045ed7f19..4038a40b6c 100644 --- a/docs/backend-system/core-services/plugin-metadata.md +++ b/docs/backend-system/core-services/plugin-metadata.md @@ -5,4 +5,28 @@ sidebar_label: Plugin Metadata description: Documentation for the Plugin Metadata service --- -TODO +This service allows you to query for metadata about the current plugin. In particular, this service is used by other plugin-scoped services, if they need to know what the ID is of the plugin that they are being instantiated for. + +## Using the service + +The following example shows a fake plugin-scoped service which wants to know what plugin it "belongs" to. + +```ts +import { + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; + +export const myServiceFactory = createServiceFactory({ + service: myServiceRef, + deps: { + logger: coreServices.logger, + plugin: coreServices.pluginMetadata, + }, + async factory({ logger, plugin }) { + const pluginId = plugin.getId(); + logger.info(`Creating an instance of my service for plugin '${id}'`); + return ...; // TODO + }, +}); +``` diff --git a/docs/backend-system/core-services/user-info.md b/docs/backend-system/core-services/user-info.md index 5ca717ddd5..e9d17f1dd7 100644 --- a/docs/backend-system/core-services/user-info.md +++ b/docs/backend-system/core-services/user-info.md @@ -5,4 +5,63 @@ sidebar_label: User Info description: Documentation for the User Info service --- -TODO +This service lets you extract more information about a set of user credentials. +Specifically, it can be used to extract the ownership entity refs for a user +principal. + +See also the [`auth`](./auth.md) and [`httpAuth`](./http-auth.md) services for +general credentials handling. + +## Using the Service + +In the following code examples, the `userInfo`, `auth`, and `httpAuth` variables are assumed +to be dependency-injected instances of the `coreServices.userInfo` and +`coreServices.httpAuth` service, respectively. For a backend plugin, it might +look like this: + +```ts +export default createBackendPlugin({ + pluginId: 'my-plugin', + register(env) { + env.registerInit({ + deps: { + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, + httpRouter: coreServices.httpRouter, + userInfo: coreServices.userInfo, + }, + async init({ auth, httpAuth, httpRouter, userInfo }) { + // Your code goes here + }, + }); + }, +}); +``` + +### Getting User Info + +This example extracts some user credentials out of a request and fetches +additional information about that principal. + +```ts +router.get('/some-request', async (req, res) => { + const credentials = await httpAuth.credentials(req, { allow: ['user'] }); + const info = await userInfo.getUserInfo(credentials); +``` + +The `userInfo` service only deals with credentials that contain user principals, +it won't accept requests for service principals. In our example code the initial +credentials extraction limits it to user credentials upfront. If you have an +endpoint that allows both user and service credentials, you may want to wrap +your user info extraction in a principal type check: + +```ts +router.get('/some-request', async (req, res) => { + const credentials = await httpAuth.credentials(req); + if (auth.isPrincipal(credentials, 'user')) { + const info = await userInfo.getUserInfo(credentials); + // ... +``` + +The user info contains data that was extracted during sign-in for the given +user. diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md index f16a2ad13e..154f68c7d1 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -101,7 +101,7 @@ CMD ["node", "packages/backend", "--config", "app-config.yaml"] For more details on how the `backend:bundle` command and the `skeleton.tar.gz` file works, see the -[`backend:bundle` command docs](../local-dev/cli-commands.md#backendbundle). +[`backend:bundle` command docs](../tooling/cli/03-commands.md#backendbundle). The `Dockerfile` is located at `packages/backend/Dockerfile`, but needs to be executed with the root of the repo as the build context, in order to get access diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 0002e51160..7c14c40283 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -627,7 +627,7 @@ annotations: #### Adding the namespace annotation Entities can have the `backstage.io/kubernetes-namespace` annotation, this will cause the entity's Kubernetes resources -to by looked up via that namespace. +to be looked up via that namespace. ```yaml annotations: diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md index 42ceaaa016..b3d03cc0b4 100644 --- a/docs/features/kubernetes/installation.md +++ b/docs/features/kubernetes/installation.md @@ -236,7 +236,11 @@ backend.add(kubernetesModuleCustomClusterDiscovery); backend.start(); ``` -> Note: this example assumes the `CustomClustersSupplier` class is the same from the [previous example](#custom-cluster-discovery) +:::note Note + +This example assumes the `CustomClustersSupplier` class is the same from the [previous example](#custom-cluster-discovery) + +::: ## Configuration diff --git a/docs/features/search/search-engines.md b/docs/features/search/search-engines.md index d06e401847..86b6244fb6 100644 --- a/docs/features/search/search-engines.md +++ b/docs/features/search/search-engines.md @@ -34,10 +34,14 @@ const searchEngine = new LunrSearchEngine({ logger: env.logger }); const indexBuilder = new IndexBuilder({ logger: env.logger, searchEngine }); ``` -> Note: Lunr is appropriate as a zero-config search engine when developing -> other parts of Backstage locally, however its use is highly discouraged when -> running Backstage in production. When deploying Backstage, use one of the -> other search engines instead. +:::note Note + +Lunr is appropriate as a zero-config search engine when developing +other parts of Backstage locally, however its use is highly discouraged when +running Backstage in production. When deploying Backstage, use one of the +other search engines instead. + +::: ## Postgres diff --git a/docs/features/software-catalog/catalog-customization.md b/docs/features/software-catalog/catalog-customization.md index 3dc47a610a..0c8eb7bcda 100644 --- a/docs/features/software-catalog/catalog-customization.md +++ b/docs/features/software-catalog/catalog-customization.md @@ -95,7 +95,11 @@ const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => { } /> ``` -> Note: the above example has been simplified and you will most likely have more code then just this in your `App.tsx` file. +:::note Note + +The above example has been simplified and you will most likely have more code then just this in your `App.tsx` file. + +::: ## Customize Actions @@ -162,7 +166,11 @@ const customActions: TableProps['actions'] = [ } /> ``` -> Note: the above example has been simplified and you will most likely have more code then just this in your `App.tsx` file. +:::note Note + +The above example has been simplified and you will most likely have more code then just this in your `App.tsx` file. + +::: The above customization will override the existing actions. Currently the only way to keep them and add your own is to also include the existing actions in your array by copying them from the [`defaultActions`](https://github.com/backstage/backstage/blob/57397e7d6d2d725712c439f4ab93f2ac6aa27bf8/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx#L113-L168). @@ -400,7 +408,11 @@ export const CustomCatalogPage = () => { The above is a very basic version of a fully custom `CatalogIndexPage`, you'll want to explore the various props to see what you can all do with them. This was built off the building blocks seen in the [`DefaultCatalogPage`](https://github.com/backstage/backstage/blob/master/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx) -> Note: The catalog index page is designed to have a minimal code footprint to support easy customization, but creating a replica does introduce a possibility of drifting out of date over time. Be sure to check the catalog [CHANGELOG](https://github.com/backstage/backstage/blob/master/plugins/catalog/CHANGELOG.md) periodically. +:::note Note + +The catalog index page is designed to have a minimal code footprint to support easy customization, but creating a replica does introduce a possibility of drifting out of date over time. Be sure to check the catalog [CHANGELOG](https://github.com/backstage/backstage/blob/master/plugins/catalog/CHANGELOG.md) periodically. + +::: To use this custom `CatalogIndexPage` which we called `CustomCatalogPage`, you'll need to make the following change: diff --git a/docs/features/software-catalog/extending-the-model--old.md b/docs/features/software-catalog/extending-the-model--old.md new file mode 100644 index 0000000000..7368a4a339 --- /dev/null +++ b/docs/features/software-catalog/extending-the-model--old.md @@ -0,0 +1,604 @@ +--- +id: extending-the-model--old +title: Extending the model +# prettier-ignore +description: Documentation on extending the catalog model +--- + +The Backstage catalog [entity data model](descriptor-format.md) is based on the +[Kubernetes objects format](https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/), +and borrows a lot of its semantics as well. This page describes those semantics +at a higher level and how to extend them to fit your organization. + +Backstage comes with a number of catalog concepts out of the box: + +- There are a number of builtin versioned _kinds_, such as `Component`, `User` + etc. These encapsulate the high level concept of an entity, and define the + schema for its entity definition data. +- An entity has both a _metadata_ object and a _spec_ object at the root. +- Each kind may or may not have a _type_. For example, there are several well + known types of component, such as `service` and `website`. These clarify the + more detailed nature of the entity, and may affect what features are exposed + in the interface. +- Entities may have a number of _[annotations](well-known-annotations.md)_ on + them. These can be added either by humans into the descriptor files, or added + by automated processes when the entity is ingested into the catalog. +- Entities may have a number of _labels_ on them. +- Entities may have a number of _relations_, expressing how they relate to each + other in different ways. + +We'll list different possibilities for extending this below. + +## Adding a New apiVersion of an Existing Kind + +Example intents: + +> "I want to evolve this core kind, tweaking the semantics a bit so I will bump +> the apiVersion a step" + +> "This core kind is a decent fit but we want to evolve it at will so we'll move +> it to our own company's apiVersion space and use that instead of +> `backstage.io`." + +The `backstage.io` apiVersion space is reserved for use by the Backstage +maintainers. Please do not change or add versions within that space. + +If you add an [apiVersion](descriptor-format.md#apiversion-and-kind-required) +space of your own, you are effectively branching out from the underlying kind +and making your own. An entity kind is identified by the apiVersion + kind pair, +so even though the resulting entity may be similar to the core one, there will +be no guarantees that plugins will be able to parse or understand its data. See +below about adding a new kind. + +## Adding a New Kind + +Example intents: + +> "The kinds that come with the package are lacking. I want to model this other +> thing that is a poor fit for either of the builtins." + +> "This core kind is a decent fit but we want to evolve it at will so we'll move +> it to our own company's apiVersion space and use that instead of +> `backstage.io`." + +A [kind](descriptor-format.md#apiversion-and-kind-required) is an overarching +family, or an idea if you will, of entities that also share a schema. Backstage +comes with a number of builtin ones that we believe are useful for a large +variety of needs that one may want to model in Backstage. The primary ambition +is to map things to these kinds, but sometimes you may want or need to extend +beyond them. + +Introducing a new apiVersion is basically the same as adding a new kind. Bear in +mind that most plugins will be compiled against the builtin +`@backstage/catalog-model` package and have expectations that kinds align with +that. + +The catalog backend itself, from a storage and API standpoint, does not care +about the kind of entities it stores. Extending with new kinds is mainly a +matter of permitting them to pass validation when building the backend catalog +using the `CatalogBuilder`, and then to make plugins be able to understand the +new kind. + +For the consuming side, it's a different story. Adding a kind has a very large +impact. The very foundation of Backstage is to attach behavior and views and +functionality to entities that we ascribe some meaning to. There will be many +places where code checks `if (kind === 'X')` for some hard coded `X`, and casts +it to a concrete type that it imported from a package such as +`@backstage/catalog-model`. + +If you want to model something that doesn't feel like a fit for either of the +builtin kinds, feel free to reach out to the Backstage maintainers to discuss +how to best proceed. + +If you end up adding that new kind, you must namespace its `apiVersion` +accordingly with a prefix that makes sense, typically based on your organization +name - e.g. `my-company.net/v1`. Also do pick a new `kind` identifier that does +not collide with the builtin kinds. + +## Adding a New Type of an Existing Kind + +Example intents: + +> "This is clearly a component, but it's of a type that doesn't quite fit with +> the ones I've seen before." + +> "We don't call our teams "team", can't we put "flock" as the group type?" + +Some entity kinds have a `type` field in its spec. This is where an organization +are free to express the variety of entities within a kind. This field is +expected to follow some taxonomy that makes sense for yourself. The chosen value +may affect what operations and views are enabled in Backstage for that entity. +Inside Spotify our model has grown significantly over the years, and our +component types now include ML models, apps, data pipelines and many more. + +It might be tempting to put software that doesn't fit into any of the existing +types into an Other catch-all type. There are a few reasons why we advise +against this; firstly, we have found that it is preferred to match the +conceptual model that your engineers have when describing your software. +Secondly, Backstage helps your engineers manage their software by integrating +the infrastructure tooling through plugins. Different plugins are used for +managing different types of components. + +For example, the +[Lighthouse plugin](https://github.com/backstage/community-plugins/tree/main/workspaces/lighthouse/plugins/lighthouse) +only makes sense for Websites. The more specific you can be in how you model +your software, the easier it is to provide plugins that are contextual. + +Adding a new type takes relatively little effort and carries little risk. Any +type value is accepted by the catalog backend, but plugins may have to be +updated if you want particular behaviors attached to that new type. + +## Changing the Validation Rules for The Entity Envelope or Metadata Fields + +Example intents: + +> "We want to import our old catalog but the default set of allowed characters +> for a metadata.name are too strict." + +> "I want to change the rules for annotations so that I'm allowed to store any +> data in annotation values, not just strings." + +After pieces of raw entity data have been read from a location, they are passed +through a field format validation step. This ensures that the types and syntax +of the base envelope and metadata make sense - in short, things that aren't +entity-kind-specific. Some or all of these validators can be replaced when +building the backend using the catalog's dedicated `catalogModelExtensionPoint` +(or directly on the `CatalogBuilder` if you are still using the old backend +system). + +The risk and impact of this type of extension varies, based on what it is that +you want to do. For example, extending the valid character set for kinds, +namespaces and names can be fairly harmless, with a few notable exceptions - +there is code that expects these to never ever contain a colon or slash, for +example, and introducing URL-unsafe characters risks breaking plugins that +aren't careful about encoding arguments. Supporting non-strings in annotations +may be possible but has not yet been tried out in the real world - there is +likely to be some level of plugin breakage that can be hard to predict. + +You must also be careful about not making the rules _more strict_ than they used +to be after populating the catalog with data. This risks making previously valid +entities start having processing errors and fail to update. + +Before making this kind of extension, we recommend that you contact the +Backstage maintainers or a support partner to discuss your use case. + +This is an example of relaxing the format rules of the `metadata.name` field: + +```ts +import { createBackend } from '@backstage/backend-defaults'; +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { catalogModelExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; + +const myCatalogCustomizations = createBackendModule({ + pluginId: 'catalog', + moduleId: 'catalog-customization', + register(reg) { + reg.registerInit({ + deps: { + catalogModel: catalogModelExtensionPoint, + }, + async init({ catalogModel }) { + catalogModel.setFieldValidators({ + // This is only one of many methods that you can pass into + // setFieldValidators; your editor of choice should help you + // find the others. The length checks and regexp inside are + // just examples and can be adjusted as needed, but take care + // to test your changes thoroughly to ensure that you get + // them right. + isValidEntityName(value) { + return ( + typeof value === 'string' && + value.length >= 1 && + value.length <= 63 && + /^[A-Za-z0-9@+_.-]+$/.test(value) + ); + }, + }); + }, + }); + }, +}); + +const backend = createBackend(); +// ... add other backend features and the catalog backend itself here ... +backend.add(myCatalogCustomizations); +backend.start(); +``` + +## Changing the Validation Rules for Core Entity Fields + +Example intent: + +> "I don't like that the owner is mandatory. I'd like it to be optional." + +After reading and policy-checked entity data from a location, it is sent through +the processor chain looking for processors that implement the +`validateEntityKind` step, to see that the data is of a known kind and abides by +its schema. There is a builtin processor that implements this for all known core +kinds and matches the data against their fixed validation schema. This processor +can be replaced when building the backend catalog using the `CatalogBuilder`, +with a processor of your own that validates the data differently. +This replacement processor must have a name that matches the builtin processor, `BuiltinKindsEntityProcessor`. + +This type of extension is high risk, and may have high impact across the +ecosystem depending on the type of change that is made. It is therefore not +recommended in normal cases. There will be a large number of plugins and +processors - and even the core itself - that make assumptions about the shape of +the data and import the typescript data type from the `@backstage/catalog-model` +package. + +## Adding New Fields to the Metadata Object + +Example intent: + +> "Our entities have this auxiliary property that I would like to express for +> several entity kinds and it doesn't really fit as a spec field." + +The metadata object is currently left open for extension. Any unknown fields +found in the metadata will just be stored verbatim in the catalog. However we +want to caution against extending the metadata excessively. Firstly, you run the +risk of colliding with future extensions to the model. Secondly, it is common +that this type of extension lives more comfortably elsewhere - primarily in the +metadata labels or annotations, but sometimes you even may want to make a new +component type or similar instead. + +There are some situations where metadata can be the right place. If you feel +that you have run into such a case and that it would apply to others, do feel +free to contact the Backstage maintainers or a support partner to discuss your +use case. Maybe we can extend the core model to benefit both you and others. + +## Adding New Fields to the Spec Object of an Existing Kind + +Example intent: + +> "The builtin Component kind is fine but we want to add an additional field to +> the spec for describing whether it's in prod or staging." + +A kind's schema validation typically doesn't forbid "unknown" fields in an +entity `spec`, and the catalog will happily store whatever is in it. So doing +this will usually work from the catalog's point of view. + +Adding fields like this is subject to the same risks as mentioned about metadata +extensions above. Firstly, you run the risk of colliding with future extensions +to the model. Secondly, it is common that this type of extension lives more +comfortably elsewhere - primarily in the metadata labels or annotations, but +sometimes you even may want to make a new component type or similar instead. + +There are some situations where the spec can be the right place. If you feel +that you have run into such a case and that it would apply to others, do feel +free to contact the Backstage maintainers or a support partner to discuss your +use case. Maybe we can extend the core model to benefit both you and others. + +## Adding a New Annotation + +Example intents: + +> "Our custom made build system has the concept of a named pipeline-set, and we +> want to associate individual components with their corresponding pipeline-sets +> so we can show their build status." + +> "We have an alerting system that automatically monitors service health, and +> there's this integration key that binds the service to an alerts pool. We want +> to be able to show the ongoing alerts for our services in Backstage so it'd be +> nice to attach that integration key to the entity somehow." + +Annotations are mainly intended to be consumed by plugins, for feature detection +or linking into external systems. Sometimes they are added by humans, but often +they are automatically generated at ingestion time by processors. There is a set +of [well-known annotations](well-known-annotations.md), but you are free to add +additional ones. This carries no risk or impact to other systems as long as you +abide by the following naming rules. + +- The `backstage.io` annotation prefix is reserved for use by the Backstage + maintainers. Reach out to us if you feel that you would like to make an + addition to that prefix. +- Annotations that pertain to a well known third party system should ideally be + prefixed with a domain, in a way that makes sense to a reader and connects it + clearly to the system (or the maker of the system). For example, you might use + a `pagerduty.com` prefix for pagerduty related annotations, but maybe not + `ldap.com` for LDAP annotations since it's not directly affiliated with or + owned by an LDAP foundation/company/similar. +- Annotations that have no prefix at all, are considered local to your Backstage + instance and can be used freely as such, but you should not make use of them + outside of your organization. For example, if you were to open source a plugin + that generates or consumes annotations, then those annotations must be + properly prefixed with your company domain or a domain that pertains to the + annotation at hand. + +## Adding a New Label + +Example intents: + +> "Our process reaping system wants to periodically scrape for components that +> have a certain property." + +> "It'd be nice if our service owners could just tag their components somehow to +> let the CD system know to automatically generate SRV records or not for that +> service." + +Labels are mainly intended to be used for filtering of entities, by external +systems that want to find entities that have some certain property. This is +sometimes used for feature detection / selection. An example could be to add a +label `deployments.my-company.net/register-srv: "true"`. + +At the time of writing this, the use of labels is very limited and we are still +settling together with the community on how to best use them. If you feel that +your use case fits the labels best, we would appreciate if you let the Backstage +maintainers know. + +You are free to add labels. This carries no risk or impact to other systems as +long as you abide by the following naming rules. + +- The `backstage.io` label prefix is reserved for use by the Backstage + maintainers. Reach out to us if you feel that you would like to make an + addition to that prefix. +- Labels that pertain to a well known third party system should ideally be + prefixed with a domain, in a way that makes sense to a reader and connects it + clearly to the system (or the maker of the system). For example, you might use + a `pagerduty.com` prefix for pagerduty related labels, but maybe not + `ldap.com` for LDAP labels since it's not directly affiliated with or owned by + an LDAP foundation/company/similar. +- Labels that have no prefix at all, are considered local to your Backstage + instance and can be used freely as such, but you should not make use of them + outside of your organization. For example, if you were to open source a plugin + that generates or consumes labels, then those labels must be properly prefixed + with your company domain or a domain that pertains to the label at hand. + +## Adding a New Relation Type + +Example intents: + +> "We have this concept of service maintainership, separate from ownership, that +> we would like to make relations to individual users for." + +> "We feel that we want to explicitly model the team-to-global-department +> mapping as a relation, because it is core to our org setup and we frequently +> query for it." + +Any processor can emit relations for entities as they are being processed, and +new processors can be added when building the backend catalog using the +`CatalogBuilder`. They can emit relations based on the entity data itself, or +based on information gathered from elsewhere. Relations are directed and go from +a source entity to a target entity. They are also tied to the entity that +originated them - the one that was subject to processing when the relation was +emitted. Relations may be dangling (referencing something that does not actually +exist by that name in the catalog), and callers need to be aware of that. + +There is a set of [well-known relations](well-known-relations.md), but you are +free to emit your own as well. You cannot change the fact that they are directed +and have a source and target that have to be an +[entity reference](references.md), but you can invent your own types. You do not +have to make any changes to the catalog backend in order to accept new relation +types. + +At the time of writing this, we do not have any namespacing/prefixing scheme for +relation types. The type is also not validated to contain only some particular +set of characters. Until rules for this are settled, you should stick to using +only letters, dashes and digits, and to avoid collisions with future core +relation types, you may want to prefix the type somehow. For example: +`myCompany-maintainerOf` + `myCompany-maintainedBy`. + +If you have a suggestion for a relation type to be elevated to the core +offering, reach out to the Backstage maintainers or a support partner. + +## Using a Well-Known Relation Type for a New Purpose + +Example intents: + +> "The ownerOf/ownedBy relation types sound like a good fit for expressing how +> users are technical owners of our company specific ServiceAccount kind, and we +> want to reuse those relation types for that." + +At the time of writing, this is uncharted territory. If the documented use of a +relation states that one end of the relation commonly is a User or a Group, for +example, then consumers are likely to have conditional statements on the form +`if (x.kind === 'User') {} else {}`, which get confused when an unexpected kind +appears. + +If you want to extend the use of an established relation type in a way that has +an effect outside of your organization, reach out to the Backstage maintainers +or a support partner to discuss risk/impact. It may even be that one end of the +relation could be considered for addition to the core. + +## Adding a New Status field + +Example intent: + +> "We would like to convey entity statuses through the catalog in a generic way, +> as an integration layer. Our monitoring and alerting system has a plugin with +> Backstage, and it would be useful if the entity's status field contained the +> current alert state close to the actual entity data for anyone to consume. We +> find the `status.items` semantics a poor fit, so we would prefer to make our +> own custom field under `status` for these purposes." + +We have not yet ventured to define any generic semantics for the `status` +object. We recommend sticking with the `status.items` mechanism where possible +(see below), since third party consumers will not be able to consume your status +information otherwise. Please reach out to the maintainers on Discord or by +making a GitHub issue describing your use case if you are interested in this +topic. + +## Adding a New Status Item Type + +Example intent: + +> "The semantics of the entity `status.items` field are fine for our needs, but +> we want to contribute our own type of status into that array instead of the +> catalog specific one." + +This is a simple, low risk way of adding your own status information to +entities. Consumers will be able to easily track and display the status together +with other types / sources. + +We recommend that any status type that are not strictly private within the +organization be namespaced to avoid collisions. Statuses emitted by Backstage +core processes will for example be prefixed with `backstage.io/`, your +organization may prefix with `my-org.net/`, and `pagerduty.com/active-alerts` +could be a sensible complete status item type for that particular external +system. + +The mechanics for how to emit custom statuses is not in place yet, so if this is +of interest to you, you might consider contacting the maintainers on Discord or +my making a GitHub issue describing your use case. +[This issue](https://github.com/backstage/backstage/issues/2292) also contains +more context. + +## Referencing different environments with the model + +Example intent: + +> "I have multiple versions of my API deployed in different environments so I +> want to have `mytool-dev` and `mytool-prod` as different entities." + +While it's possible to have different versions of the same thing represented as +separate entities, it's something we generally recommend against. We believe +that a developer should be able to just find for example one `Component` +representing a service, and to be able to see the different code versions that +are deployed throughout your stack within its view. This reasoning works +similarly for other kinds as well, such as `API`. + +That being said - sometimes the differences between versions are so large, that +they represent what is for all intents and purposes an entirely new entity as +seen from the consumer's point of view. This can happen for example for +different _significant_ major versions of an API, and in particular if the two +major versions coexist in the ecosystem for some time. In those cases, it can be +motivated to have one `my-api-v2` and one `my-api-v3` named entity. This matches +the end user's expectations when searching for the API, and matches the desire +to maybe have separate documentation for the two and similar. But use this +sparingly - only do it if the extra modelling burden is outweighed by any +potential better clarity for users. + +When writing your custom plugins, we encourage designing them such that they can +show all the different variations through environments etc under one canonical +reference to your software in the catalog. For example for a continuous +deployment plugin, a user is likely to be greatly helped by being able to see +the entity's versions deployed in all different environments next to each other +in one view. That is also where they might be offered the ability to promote +from one environment to the other, do rollbacks, see their relative performance +metrics, and similar. This coherency and collection of tooling in one place is +where something like Backstage can offer the most value and effectiveness of +use. Splitting your entities apart into small islands makes this harder. + +## Implementing custom model extensions + +This section walks you through the steps involved extending the catalog model +with a new Entity type. + +### Creating a custom entity definition + +The first step of introducing a custom entity is to define what shape and schema +it has. We do this using a TypeScript type, as well as a JSONSchema schema. + +Most of the time you will want to have at least the TypeScript type of your +extension available in both frontend and backend code, which means you likely +want to have an isomorphic package that houses these types. Within the Backstage +main repo the package naming pattern of `-common` is used for isomorphic +packages, and you may choose to adopt this pattern as well. + +You can generate an isomorphic plugin package by running:`yarn new --select plugin-common` +or you can run `yarn new` and then select "plugin-common" from the list of options + +There's at this point no existing templates for generating isomorphic plugins +using the `@backstage/cli`. Perhaps the simplest way to get started right now is +to copy the contents of one of the existing packages in the main repository, +such as `plugins/scaffolder-common`, and rename the folder and file contents to +the desired name. This example uses _foobar_ as the plugin name so the plugin +will be named _foobar-common_. + +Once you have a common package in place you can start adding your own entity +definitions. For the exact details on how to do that we defer to getting +inspired by the existing +[scaffolder-common](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-common/src/index.ts) +package. But in short you will need to declare a TypeScript type and a +JSONSchema for the new entity kind. + +### Building a custom processor for the entity + +The next step is to create a custom processor for your new entity kind. This +will be used within the catalog to make sure that it's able to ingest and +validate entities of our new kind. Just like with the definition package, you +can find inspiration in for example the existing +[ScaffolderEntitiesProcessor](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-scaffolder-entity-model/src/processor/ScaffolderEntitiesProcessor.ts). +We also provide a high-level example of what a catalog process for a custom +entity might look like: + +```ts +import { CatalogProcessor, CatalogProcessorEmit, processingResult } from '@backstage/plugin-catalog-node'; +import { LocationSpec } from '@backstage/plugin-catalog-common' +import { Entity, entityKindSchemaValidator } from '@backstage/catalog-model'; + +// For an example of the JSONSchema format and how to use $ref markers to the +// base definitions, see: +// https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema/kinds/Component.v1alpha1.schema.json +import { foobarEntityV1alpha1Schema } from '@internal/catalog-model'; + +export class FoobarEntitiesProcessor implements CatalogProcessor { + // You often end up wanting to support multiple versions of your kind as you + // iterate on the definition, so we keep each version inside this array as a + // convenient pattern. + private readonly validators = [ + // This is where we use the JSONSchema that we export from our isomorphic + // package + entityKindSchemaValidator(foobarEntityV1alpha1Schema), + ]; + + // Return processor name + getProcessorName(): string { + return 'FoobarEntitiesProcessor' + } + + // validateEntityKind is responsible for signaling to the catalog processing + // engine that this entity is valid and should therefore be submitted for + // further processing. + async validateEntityKind(entity: Entity): Promise { + for (const validator of this.validators) { + // If the validator throws an exception, the entity will be marked as + // invalid. + if (validator(entity)) { + return true; + } + } + + // Returning false signals that we don't know what this is, passing the + // responsibility to other processors to try to validate it instead. + return false; + } + + async postProcessEntity( + entity: Entity, + _location: LocationSpec, + emit: CatalogProcessorEmit, + ): Promise { + if ( + entity.apiVersion === 'example.com/v1alpha1' && + entity.kind === 'Foobar' + ) { + const foobarEntity = entity as FoobarEntityV1alpha1; + + // Typically you will want to emit any relations associated with the + // entity here. + emit(processingResult.relation({ ... })) + } + + return entity; + } +} +``` + +Once the processor is created it can be wired up to the catalog via the +`CatalogBuilder` in `packages/backend/src/plugins/catalog.ts`: + +```ts title="packages/backend/src/plugins/catalog.ts" +/* highlight-add-next-line */ +import { FoobarEntitiesProcessor } from '@internal/plugin-foobar-backend'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const builder = await CatalogBuilder.create(env); + /* highlight-add-next-line */ + builder.addProcessor(new FoobarEntitiesProcessor()); + const { processingEngine, router } = await builder.build(); + // .. +} +``` diff --git a/docs/features/software-catalog/extending-the-model.md b/docs/features/software-catalog/extending-the-model.md index b3d7de3a65..51acd44c1d 100644 --- a/docs/features/software-catalog/extending-the-model.md +++ b/docs/features/software-catalog/extending-the-model.md @@ -519,6 +519,9 @@ will be used within the catalog to make sure that it's able to ingest and validate entities of our new kind. Just like with the definition package, you can find inspiration in for example the existing [ScaffolderEntitiesProcessor](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-scaffolder-entity-model/src/processor/ScaffolderEntitiesProcessor.ts). + +The custom processor should be created as a separate module for the catalog plugin. For information on how to set that up, see the [plugin docs](../../plugins/backend-plugin.md#creating-a-backend-plugin). Use `yarn new --select backend-module` instead to create a module. For our case, the module ID will be `foobar` and the plugin ID will be `catalog`. + We also provide a high-level example of what a catalog process for a custom entity might look like: @@ -585,20 +588,43 @@ export class FoobarEntitiesProcessor implements CatalogProcessor { } ``` -Once the processor is created it can be wired up to the catalog via the -`CatalogBuilder` in `packages/backend/src/plugins/catalog.ts`: +#### New Backend -```ts title="packages/backend/src/plugins/catalog.ts" +To use your custom processor, you'll need to add the module to your backend as well as integrate your module with the catalog plugin. + +```ts title="plugins/catalog-backend-module-foobar/src/index.ts" +import { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { catalogModelExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; /* highlight-add-next-line */ -import { FoobarEntitiesProcessor } from '@internal/plugin-foobar-backend'; +import { FoobarEntitiesProcessor } from './providers'; -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - /* highlight-add-next-line */ - builder.addProcessor(new FoobarEntitiesProcessor()); - const { processingEngine, router } = await builder.build(); - // .. -} +export const catalogModuleFoobarEntitiesProcessor = createBackendModule({ + pluginId: 'catalog', + moduleId: 'foobar', + register(env) { + env.registerInit({ + deps: { + catalog: catalogProcessingExtensionPoint, + }, + async init({ catalog }) { + catalog.addProcessor(new FoobarEntitiesProcessor()); + }, + }); + }, +}); + +export default catalogModuleFoobarEntitiesProcessor; ``` + +This module can then be installed to your backend like so, + +```ts +backend.add(import('@internal/plugin-catalog-backend-module-foobar')); +``` + +#### Legacy Backend + +Look through the [legacy documentation](./extending-the-model--old.md). diff --git a/docs/features/software-catalog/life-of-an-entity.md b/docs/features/software-catalog/life-of-an-entity.md index d1c01141e4..13fe496fc1 100644 --- a/docs/features/software-catalog/life-of-an-entity.md +++ b/docs/features/software-catalog/life-of-an-entity.md @@ -162,9 +162,13 @@ steps and merging them into the final object which is what is visible from the catalog API. As the final entity itself gets updated, the stitcher makes sure that the search table gets refreshed accordingly as well. -> Note: The search table mentioned here is not related to the core Search -> feature of Backstage. It's rather the table that backs the ability to filter -> catalog API query results. +:::note Note + +The search table mentioned here is not related to the core Search +feature of Backstage. It's rather the table that backs the ability to filter +catalog API query results. + +::: ![Stitching overview](../../assets/features/catalog/life-of-an-entity_stitching.svg) diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md index 6396d7530a..083516192c 100644 --- a/docs/features/software-templates/adding-templates.md +++ b/docs/features/software-templates/adding-templates.md @@ -86,12 +86,17 @@ contains more information about the required fields. Once we have a `template.yaml` ready, we can then add it to the software catalog for use by the scaffolder. -> Note: When you add or modify a template, you will need to refresh the location entity. -> Otherwise, Backstage won't display the template in the available templates, -> or it will keep showing the old template. You can refresh the location instance by -> going into `Catalog` web page, choosing `Locations` instead of `Components`, and selecting the correct location entity. -> From there, you can click on the refresh icon representing "Scheduled entity refresh" action. -> Afterwards, you should see your template updated. +:::note Note + +When you add or modify a template, you will need to refresh the location entity. +Otherwise, Backstage won't display the template in the available templates, +or it will keep showing the old template. You can refresh the location instance by +going into `Catalog` web page, choosing `Locations` instead of `Components`, and selecting the correct +location entity. +From there, you can click on the refresh icon representing "Scheduled entity refresh" action. +Afterwards, you should see your template updated. + +::: You can add the template files to the catalog through [static location configuration](../software-catalog/configuration.md#static-location-configuration), diff --git a/docs/features/software-templates/authorizing-parameters-steps-and-actions.md b/docs/features/software-templates/authorizing-scaffolder-template-details.md similarity index 74% rename from docs/features/software-templates/authorizing-parameters-steps-and-actions.md rename to docs/features/software-templates/authorizing-scaffolder-template-details.md index 072f750a14..127677757c 100644 --- a/docs/features/software-templates/authorizing-parameters-steps-and-actions.md +++ b/docs/features/software-templates/authorizing-scaffolder-template-details.md @@ -1,10 +1,10 @@ --- -id: authorizing-parameters-steps-and-actions -title: 'Authorizing parameters, steps and actions' -description: How to authorize part of a template +id: authorizing-scaffolder-template-details +title: 'Authorizing scaffolder tasks, parameters, steps, and actions' +description: How to authorize parts of a template and authorize scaffolder task access --- -The scaffolder plugin integrates with the Backstage [permission framework](../../permissions/overview.md), which allows you to control access to certain parameters and steps in your templates based on the user executing the template. +The scaffolder plugin integrates with the Backstage [permission framework](../../permissions/overview.md), which allows you to control access to certain parameters and steps in your templates based on the user executing the template. It also allows you to control access to scaffolder tasks. ### Authorizing parameters and steps @@ -174,7 +174,64 @@ class ExamplePermissionPolicy implements PermissionPolicy { } ``` -Although the rules exported by the scaffolder are simple, combining them can help you achieve more complex cases. +### Authorizing scaffolder tasks + +The scaffolder plugin also exposes permissions that can restrict access to tasks, task logs, task creation, and task cancellation. This can be useful if you want to control who has access to these areas of the scaffolder. + +```ts title="packages/src/backend/plugins/permissions.ts" +/* highlight-add-start */ +import { + taskCancelPermission, + taskCreatePermission, + taskReadPermission, +} from '@backstage/plugin-scaffolder-common/alpha'; +/* highlight-add-end */ + +class ExamplePermissionPolicy implements PermissionPolicy { + async handle( + request: PolicyQuery, + user?: BackstageIdentityResponse, + ): Promise { + /* highlight-add-start */ + if (isPermission(request.permission, taskCreatePermission)) { + if (user?.identity.userEntityRef === 'user:default/spiderman') { + return { + result: AuthorizeResult.ALLOW, + }; + } + } + if (isPermission(request.permission, taskCancelPermission)) { + if (user?.identity.userEntityRef === 'user:default/spiderman') { + return { + result: AuthorizeResult.ALLOW, + }; + } + } + if (isPermission(request.permission, taskReadPermission)) { + if (user?.identity.userEntityRef === 'user:default/spiderman') { + return { + result: AuthorizeResult.ALLOW, + }; + } + } + /* highlight-add-end */ + + return { + result: AuthorizeResult.DENY, + }; + } +} +``` + +In the provided example permission policy, we only grant the `spiderman` user permissions to perform/access the following actions/resources: + +- Read all scaffolder tasks and their associated events/logs. +- Cancel any ongoing scaffolder tasks. +- Trigger software templates, which effectively creates new scaffolder tasks. + +Any other user would be denied access to these actions/resources. + +Although the rules exported by the scaffolder are simple, combining them can help you achieve more complex use cases. ### Authorizing in the New Backend System @@ -229,4 +286,8 @@ backend.add(customPermissionBackendModule); /* highlight-add-end */ ``` -> Note: the `ExamplePermissionPolicy` here could be the one from the [Authorizing parameters and steps](#authorizing-parameters-and-steps) example or from the [Authorizing actions](#authorizing-actions) example. It would work the same way for both of them. +:::note Note + +The `ExamplePermissionPolicy` here could be the one from the [Authorizing parameters and steps](#authorizing-parameters-and-steps) example or from the [Authorizing actions](#authorizing-actions) example. It would work the same way for both of them. + +::: diff --git a/docs/features/software-templates/builtin-actions.md b/docs/features/software-templates/builtin-actions.md index 94b2d2298b..5b1e4347da 100644 --- a/docs/features/software-templates/builtin-actions.md +++ b/docs/features/software-templates/builtin-actions.md @@ -57,7 +57,11 @@ backend.add(import('@backstage/plugin-scaffolder-backend-module-github')); backend.start(); ``` -> Note: This is a simplified example of what your backend may look like, you may have more code in here then this. +:::note Note + +This is a simplified example of what your backend may look like, you may have more code in here then this. + +::: ## Listing Actions diff --git a/docs/features/software-templates/configuration.md b/docs/features/software-templates/configuration.md index 24b8c530b8..571f1150d0 100644 --- a/docs/features/software-templates/configuration.md +++ b/docs/features/software-templates/configuration.md @@ -12,7 +12,11 @@ This is done in your `app-config.yaml` by adding [Backstage integrations](https://backstage.io/docs/integrations/) for the appropriate source code repository for your organization. -> Note: Integrations may already be set up as part of your `app-config.yaml`. +:::note Note + +Integrations may already be set up as part of your `app-config.yaml`. + +::: The next step is to [add templates](http://backstage.io/docs/features/software-templates/adding-templates) to your Backstage app. diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md index 9642214ede..d4fa6ac971 100644 --- a/docs/features/software-templates/index.md +++ b/docs/features/software-templates/index.md @@ -20,10 +20,14 @@ locations like GitHub or GitLab. > Be sure to have covered > [Getting Started with Backstage](../../getting-started) before proceeding. -> Note: if you're running Backstage with Node 20 or later, you'll need to pass the flag `--no-node-snapshot` to Node in order to -> use the templates feature. -> One way to do this is to specify the `NODE_OPTIONS` environment variable before starting Backstage: -> `export NODE_OPTIONS=--no-node-snapshot` +:::note Note + +If you're running Backstage with Node 20 or later, you'll need to pass the flag `--no-node-snapshot` to Node in order to +use the templates feature. +One way to do this is to specify the `NODE_OPTIONS` environment variable before starting Backstage: +`export NODE_OPTIONS=--no-node-snapshot` + +::: The Software Templates are available under `/create`. For local development you should be able to reach them at `http://localhost:3000/create`. diff --git a/docs/features/software-templates/migrating-to-rjsf-v5.md b/docs/features/software-templates/migrating-to-rjsf-v5.md index 829aaf8410..856841bdd4 100644 --- a/docs/features/software-templates/migrating-to-rjsf-v5.md +++ b/docs/features/software-templates/migrating-to-rjsf-v5.md @@ -5,7 +5,11 @@ title: 'Migrating to react-jsonschema-form@v5' description: Docs on migrating to `react-jsonschema-form`@v5 and the new designs --- -> Note: If you were previously using the `/alpha` imports to test out the `scaffolder/next` work, those imports have been promoted to the default exports from the respective packages. You should just have to remove the `/alpha` from the import path, and remove the `Next` from the import name. `NextScaffolderPage` -> `ScaffolderPage`, `createNextScaffolderFieldExtension` -> `createScaffolderFieldExtension` etc. +:::note Note + +If you were previously using the `/alpha` imports to test out the `scaffolder/next` work, those imports have been promoted to the default exports from the respective packages. You should just have to remove the `/alpha` from the import path, and remove the `Next` from the import name. `NextScaffolderPage` -> `ScaffolderPage`, `createNextScaffolderFieldExtension` -> `createScaffolderFieldExtension` etc. + +::: ## What's `react-jsonschema-form`? diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index 789b8a8122..d937d9ea54 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -8,10 +8,14 @@ If you want to extend the functionality of the Scaffolder, you can do so by writing custom actions which can be used alongside our [built-in actions](./builtin-actions.md). -> Note: When adding custom actions, the actions array will **replace the -> built-in actions too**. Meaning, you will no longer be able to use them. -> If you want to continue using the builtin actions, include them in the actions -> array when registering your custom actions, as seen below. +:::note Note + +When adding custom actions, the actions array will **replace the +built-in actions too**. Meaning, you will no longer be able to use them. +If you want to continue using the builtin actions, include them in the actions +array when registering your custom actions, as seen below. + +::: ## Streamlining Custom Action Creation with Backstage CLI @@ -226,7 +230,7 @@ const scaffolderModuleCustomExtensions = createBackendModule({ async init({ scaffolder /* ..., other dependencies */ }) { // Here you have the opportunity to interact with the extension // point before the plugin itself gets instantiated - scaffolder.addActions(new createNewFileAction()); // just an example + scaffolder.addActions(createNewFileAction()); // just an example }, }); }, diff --git a/docs/features/techdocs/README.md b/docs/features/techdocs/README.md index bee03ab45e..a696b850a8 100644 --- a/docs/features/techdocs/README.md +++ b/docs/features/techdocs/README.md @@ -70,6 +70,7 @@ See [TechDocs Architecture](architecture.md) to get an overview of where the bel | GitLab Enterprise | Yes ✅ | | Gitea | Yes ✅ | | AWS CodeCommit | Yes ✅ | +| Harness Code | Yes ✅ | ### File storage providers diff --git a/docs/features/techdocs/architecture.md b/docs/features/techdocs/architecture.md index 2cb8543ccd..8c9c4401cc 100644 --- a/docs/features/techdocs/architecture.md +++ b/docs/features/techdocs/architecture.md @@ -13,9 +13,14 @@ out-of-the box experience. ![TechDocs Architecture diagram](../../assets/techdocs/architecture-basic.drawio.svg) -> Note: See below for our recommended deployment architecture which takes care -> of stability, scalability and speed. Also look at the -> [HOW TO migrate guide](how-to-guides.md#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach). +:::note Note + +See below for our recommended deployment architecture which takes care +of stability, scalability and speed. Also look at the +[HOW TO migrate guide](how-to-guides +md#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach). + +::: When you open a TechDocs site in Backstage, the [TechDocs Reader](./concepts.md#techdocs-reader) makes a request to diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index bf55c90af9..6cbbc83614 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -219,7 +219,11 @@ backend.add(import('@backstage/plugin-techdocs-backend/alpha')); backend.start(); ``` -> Note: The above is a very simplified example, you may have more content then this in your version. +:::note Note + +The above is a very simplified example, you may have more content then this in your version. + +::: ## Setting the configuration diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index b25c2a3928..3a7cfd97bb 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -499,8 +499,12 @@ Start writing your documentation by adding more markdown (.md) files to this folder (/docs) or replace the content in this file. ``` -> Note: The values of `site_name`, `component_id` and `site_description` depends -> on how you have configured your `template.yaml` +:::note Note + +The values of `site_name`, `component_id` and `site_description` depends +on how you have configured your `template.yaml`. + +::: Done! You now have support for TechDocs in your own software template! @@ -514,7 +518,11 @@ theme: font: false ``` -> Note: The addition `name: material` is necessary. Otherwise it will not work +:::note Note + +The addition `name: material` is necessary. Otherwise it will not work + +::: ## How to enable iframes in TechDocs @@ -623,12 +631,16 @@ plugins: - kroki ``` -> Note: you will very likely want to set a `kroki` `ServerURL` configuration in your -> `mkdocs.yml` as well. The default value is the publicly hosted `kroki.io`. If -> you have sensitive information in your organization's diagrams, you should set -> up a [server of your own](https://docs.kroki.io/kroki/setup/install/) and use it -> instead. Check out [mkdocs-kroki-plugin config](https://github.com/AVATEAM-IT-SYSTEMHAUS/mkdocs-kroki-plugin#config) -> for more plugin configuration details. +:::note Note + +You will very likely want to set a `kroki` `ServerURL` configuration in your +`mkdocs.yml` as well. The default value is the publicly hosted `kroki.io`. If +you have sensitive information in your organization's diagrams, you should set +up a [server of your own](https://docs.kroki.io/kroki/setup/install/) and use it +instead. Check out [mkdocs-kroki-plugin config](https://github.com/AVATEAM-IT-SYSTEMHAUS/mkdocs-kroki-plugin#config) +for more plugin configuration details. + +::: 4. **Add mermaid code into TechDocs:** @@ -766,7 +778,11 @@ backend.add(techdocsCustomBuildStrategy()); backend.start(); ``` -> Note: You may need to add the `@backstage/plugin-techdocs-node` package to your backend `package.json` if it's not been imported already. +:::note Note + +You may need to add the `@backstage/plugin-techdocs-node` package to your backend `package.json` if it's not been imported already. + +::: ## How to use other mkdocs plugins? diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index aaf2758241..6ba826ea62 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -169,17 +169,21 @@ permissions to: - `s3:ListBucket` - To retrieve bucket metadata - `s3:GetObject` - To retrieve files from the bucket -> Note: If you need to migrate documentation objects from an older-style path -> format including case-sensitive entity metadata, you will need to add some -> additional permissions to be able to perform the migration, including: -> -> - `s3:PutBucketAcl` (for copying files, -> [more info here](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectAcl.html)) -> - `s3:DeleteObject` and `s3:DeleteObjectVersion` (for deleting migrated files, -> [more info here](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html)) -> -> ...And you will need to ensure the permissions apply to the bucket itself, as -> well as all resources under the bucket. See the example policy below. +:::note Note + +If you need to migrate documentation objects from an older-style path +format including case-sensitive entity metadata, you will need to add some +additional permissions to be able to perform the migration, including: + +- `s3:PutBucketAcl` (for copying files, + [more info here](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectAcl.html)) +- `s3:DeleteObject` and `s3:DeleteObjectVersion` (for deleting migrated files, + [more info here](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html)) + +...And you will need to ensure the permissions apply to the bucket itself, as +well as all resources under the bucket. See the example policy below. + +::: ```json { diff --git a/docs/frontend-system/architecture/02-app.md b/docs/frontend-system/architecture/02-app.md index 1ffae71981..3b839d4232 100644 --- a/docs/frontend-system/architecture/02-app.md +++ b/docs/frontend-system/architecture/02-app.md @@ -10,7 +10,7 @@ description: App instances ## The App Instance -The app instance is main entry point for creating a frontend app. It doesn't do much on its own, but is instead responsible for wiring things together that have been provided as features from other parts of the system. +The app instance is the main entry point for creating a frontend app. It doesn't do much on its own, but is instead responsible for wiring things together that have been provided as features from other parts of the system. Below is a simple example of how to create and render an app instance: @@ -35,7 +35,7 @@ ReactDOM.createRoot(rootEl).render(app); We call `createApp` to create a new app instance, which is responsible for wiring together all of the features that we provide to the app. It also provides a set of built-in [Extensions](./03-extensions.md) that help build out the foundations of the app, as well as defaults for many other systems such as [Utility API](./06-utility-apis.md) implementations, components, icons, themes, and how to load configuration. No real work is done at the point of creating the app though, it's all deferred to the rendering of the element returned from `app.createRoot()`. -It is possible to explicitly install features when creating the app, although typically these will instead be discovered automatically which we'll explore later on. Nevertheless these features are what build out the actual functionality of the app by providing [Extensions](./03-extensions.md). These extensions are wired together by the app into a tree structure known as the app extension tree. Each node in this tree receives data from its child nodes, and pass along data to its parent. The following diagram illustrates the shape of a small app extension tree. +It is possible to explicitly install features when creating the app, although typically these will instead be discovered automatically which we'll explore later on. Nevertheless these features are what build out the actual functionality of the app by providing [Extensions](./03-extensions.md). These extensions are wired together by the app into a tree structure known as the app extension tree. Each node in this tree receives data from its child nodes, and passes along data to its parent. The following diagram illustrates the shape of a small app extension tree. ![frontend system app structure diagram](../../assets/frontend-system/architecture-app.drawio.svg) diff --git a/docs/frontend-system/architecture/03-extensions.md b/docs/frontend-system/architecture/03-extensions.md index 061c695abb..19345e0b26 100644 --- a/docs/frontend-system/architecture/03-extensions.md +++ b/docs/frontend-system/architecture/03-extensions.md @@ -22,7 +22,7 @@ Each extensions has a number of different properties that define how it behaves Update this to be 3 different sections: name, kind and namespace --> -The ID of an extension is used to uniquely identity it, and it should ideally by unique across the entire Backstage ecosystem. For each frontend app instance there can only be a single extension for any given ID. Installing multiple extensions with the same ID will either result in an error or one of the extensions will override the others. The ID is also used to reference the extensions from other extensions, in configuration, and in other places such as developer tools and analytics. +The ID of an extension is used to uniquely identity it, and it should ideally be unique across the entire Backstage ecosystem. For each frontend app instance there can only be a single extension for any given ID. Installing multiple extensions with the same ID will either result in an error or one of the extensions will override the others. The ID is also used to reference the extensions from other extensions, in configuration, and in other places such as developer tools and analytics. ### Output @@ -183,7 +183,7 @@ const navigationExtension = createExtension({ }); ``` -The input (see [1] above) is an object that we create using `createExtensionInput`. The first argument is the set of extension data that we accept via this input, and works just like the `output` option. The second argument is optional, and it allows us to put constraints on the extensions that are attached to our input. If the `singleton: true` option is set, only a single extension can attached at a time, and unless the `optional: true` option is set it will also be required that there is exactly on attached extension. +The input (see [1] above) is an object that we create using `createExtensionInput`. The first argument is the set of extension data that we accept via this input, and works just like the `output` option. The second argument is optional, and it allows us to put constraints on the extensions that are attached to our input. If the `singleton: true` option is set, only a single extension can be attached at a time, and unless the `optional: true` option is set it will also be required that there is exactly one attached extension. So how can we now attach the output to the parent extension's input? If we think about a navigation component, like the Sidebar in Backstage, there might be plugins that want to attach a link to their plugin to this navigation component. In this case the plugin only needs to know the extension `id` and the name of the extension `input` to attach the extension `output` returned by the `factory` to the specified extension: diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 7fb053aa56..99cfbae937 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -274,6 +274,21 @@ Note that we are not importing and using the `RouteRef`s directly in the app, an Another thing to note is that this indirection in the routing is particularly useful for open source plugins that need to provide flexibility in how they are integrated. For plugins that you build internally for your own Backstage application, you can choose to use direct imports or even concrete route path strings directly. Although there can be some benefits to using the full routing system even in internal plugins: it can help you structure your routes, and as you will see further down it also helps you manage route parameters. +### Default Targets for External Route References + +It is possible to define a default target for an external route reference, potentially removing the need to bind the route in the app. This reduces the need for configuration when installing new plugins through providing a sensible default. It is of course still possible to override the route binding in the app. + +The default target uses the same syntax as the route binding configuration, and will only be used if the target plugin and route exist. For example, this is how the catalog can define a default target for the create component external route in a way that removes the need for the binding in the previous example: + +```tsx title="plugins/catalog/src/routes.ts" +import { createExternalRouteRef } from '@backstage/frontend-plugin-api'; + +export const createComponentExternalRouteRef = createExternalRouteRef({ + // highlight-next-line + defaultTarget: 'scaffolder.createComponent', +}); +``` + ### Optional External Route References It is possible to define an `ExternalRouteRef` as optional, so it is not required to bind it in the app. diff --git a/docs/frontend-system/building-plugins/02-testing.md b/docs/frontend-system/building-plugins/02-testing.md index fa29918311..26c9503c6d 100644 --- a/docs/frontend-system/building-plugins/02-testing.md +++ b/docs/frontend-system/building-plugins/02-testing.md @@ -10,7 +10,11 @@ description: Testing plugins in the frontend system # Testing Frontend Plugins -> NOTE: The new frontend system is in alpha, and some plugins do not yet fully implement it. +:::note Note + +The new frontend system is in alpha, and some plugins do not yet fully implement it. + +::: Utilities for testing frontend features and components are available in `@backstage/frontend-test-utils`. diff --git a/docs/getting-started/keeping-backstage-updated.md b/docs/getting-started/keeping-backstage-updated.md index e7324f4603..1de8874d56 100644 --- a/docs/getting-started/keeping-backstage-updated.md +++ b/docs/getting-started/keeping-backstage-updated.md @@ -13,7 +13,7 @@ starting point that's meant to be evolved. The Backstage CLI has a command to bump all `@backstage` packages and dependencies you're using to the latest versions: -[versions:bump](https://backstage.io/docs/local-dev/cli-commands#versionsbump). +[versions:bump](https://backstage.io/docs/tooling/cli/03-commands#versionsbump). ```bash yarn backstage-cli versions:bump @@ -70,7 +70,7 @@ example, depends on global referential equality. This can cause problems in Backstage with API lookup, or config loading. To help resolve these situations, the Backstage CLI has -[versions:check](https://backstage.io/docs/local-dev/cli-commands#versionscheck). This +[versions:check](https://backstage.io/docs/tooling/cli/03-commands#versionscheck). This will validate versions of `@backstage` packages in your app to check for duplicate definitions: diff --git a/docs/integrations/azure/locations.md b/docs/integrations/azure/locations.md index b59c4149e0..ddc95b65eb 100644 --- a/docs/integrations/azure/locations.md +++ b/docs/integrations/azure/locations.md @@ -68,11 +68,15 @@ integrations: If you do not specify the `organizations` field the credential will be used for all organizations for which no other credential is configured. -> Note: An Azure DevOps provider is added automatically at startup for -> convenience, so you only need to list it if you want to supply a -> [personalAccessToken](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate), -> a [service principal](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity), -> or a [managed identity](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity) +:::note Note + +An Azure DevOps provider is added automatically at startup for +convenience, so you only need to list it if you want to supply a +[personalAccessToken](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate), +a [service principal](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity), +or a [managed identity](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity) + +::: The configuration is a structure with these elements: @@ -86,9 +90,11 @@ The `credentials` element is a structure with these elements: - `tenantId`: The tenant ID of the service principal (required for service principal) - `personalAccessToken`: The personal access token (required for personal access token) -> Note: -> -> - You cannot use a service principal or managed identity for Azure DevOps Server (on-premises) organizations -> - You can only use a service principal or managed identity for Microsoft Entra ID (formerly Azure Active Directory) backed Azure DevOps organizations -> - You can only specify one credential per host without any organizations specified -> - The personal access token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API +:::note Note + +- You cannot use a service principal or managed identity for Azure DevOps Server (on-premises) organizations +- You can only use a service principal or managed identity for Microsoft Entra ID (formerly Azure Active Directory) backed Azure DevOps organizations +- You can only specify one credential per host without any organizations specified +- The personal access token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API + +::: diff --git a/docs/integrations/bitbucketCloud/locations.md b/docs/integrations/bitbucketCloud/locations.md index e7666efb54..9b08a79c63 100644 --- a/docs/integrations/bitbucketCloud/locations.md +++ b/docs/integrations/bitbucketCloud/locations.md @@ -22,10 +22,18 @@ integrations: appPassword: ${BITBUCKET_CLOUD_PASSWORD} ``` -> Note: A public Bitbucket Cloud provider is added automatically at startup for -> convenience, so you only need to list it if you want to supply credentials. +:::note Note -> Note: The credential used for this is type [App Password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/). An Atlassian Account API key will not work +A public Bitbucket Cloud provider is added automatically at startup for +convenience, so you only need to list it if you want to supply credentials. + +::: + +:::note Note + +The credential used for this is type [App Password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/). An Atlassian Account API key will not work. + +::: Directly under the `bitbucketCloud` key is a list of provider configurations, where you can list the Bitbucket Cloud providers you want to fetch data from. diff --git a/docs/integrations/github/discovery--old.md b/docs/integrations/github/discovery--old.md index b94cb256c4..138e54a538 100644 --- a/docs/integrations/github/discovery--old.md +++ b/docs/integrations/github/discovery--old.md @@ -200,6 +200,7 @@ This provider supports multiple organizations via unique provider IDs. - **`filters`** _(optional)_: - **`branch`** _(optional)_: String used to filter results based on the branch name. + Defaults to the default Branch of the repository. - **`repository`** _(optional)_: Regular expression used to filter results based on the repository name. - **`topic`** _(optional)_: diff --git a/docs/integrations/github/discovery.md b/docs/integrations/github/discovery.md index f16d1c17b2..5c3a33416b 100644 --- a/docs/integrations/github/discovery.md +++ b/docs/integrations/github/discovery.md @@ -130,8 +130,12 @@ catalog: This provider supports multiple organizations via unique provider IDs. -> **Note:** It is possible but certainly not recommended to skip the provider ID level. -> If you do so, `default` will be used as provider ID. +:::note Note + +It is possible but certainly not recommended to skip the provider ID level. +If you do so, `default` will be used as provider ID. + +::: - **`catalogPath`** _(optional)_: Default: `/catalog-info.yaml`. @@ -141,6 +145,7 @@ This provider supports multiple organizations via unique provider IDs. - **`filters`** _(optional)_: - **`branch`** _(optional)_: String used to filter results based on the branch name. + Defaults to the default Branch of the repository. - **`repository`** _(optional)_: Regular expression used to filter results based on the repository name. - **`topic`** _(optional)_: diff --git a/docs/integrations/github/github-apps.md b/docs/integrations/github/github-apps.md index 6e3d028e6d..b5e252be74 100644 --- a/docs/integrations/github/github-apps.md +++ b/docs/integrations/github/github-apps.md @@ -42,7 +42,7 @@ yarn backstage-cli create-github-app ``` You can read more about the -[`backstage-cli create-github-app`](../../local-dev/cli-commands.md#create-github-app) command. +[`backstage-cli create-github-app`](../../tooling/cli/03-commands.md#create-github-app) command. Once you've gone through the CLI command, it should produce a YAML file in the root of the project which you can then use as an `include` in your diff --git a/docs/integrations/github/org.md b/docs/integrations/github/org.md index 06b4e07daa..f94ffe6c7a 100644 --- a/docs/integrations/github/org.md +++ b/docs/integrations/github/org.md @@ -17,9 +17,13 @@ is a hierarchy of [`Group`](../../features/software-catalog/descriptor-format.md#kind-group) kind entities that mirror your org setup. -> Note: This adds `User` and `Group` entities to the catalog, but does not -> provide authentication. See the -> [GitHub auth provider](../../auth/github/provider.md) for that. +:::note Note + +This adds `User` and `Group` entities to the catalog, but does not +provide authentication. See the +[GitHub auth provider](../../auth/github/provider.md) for that. + +::: ## Permissions diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index 91efe05fee..b16f6db839 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -136,7 +136,11 @@ To use the discovery provider, you'll need a GitLab integration [set up](locations.md) with a `token`. Then you can add a provider config per group to the catalog configuration. -> > NOTE: if you are using the New Backend System, the `schedule` has to be setup in the config, as shown below. +:::note Note + +If you are using the New Backend System, the `schedule` has to be setup in the config, as shown below. + +::: ```yaml title="app-config.yaml" catalog: diff --git a/docs/integrations/gitlab/org.md b/docs/integrations/gitlab/org.md index 14587461e5..2cc0dcfb82 100644 --- a/docs/integrations/gitlab/org.md +++ b/docs/integrations/gitlab/org.md @@ -158,7 +158,11 @@ amount of data, this can take significant time and resources. The token used must have the `read_api` scope, and the Users and Groups fetched will be those visible to the account which provisioned the token. -> > NOTE: if you are using the New Backend System, the `schedule` has to be setup in the config, as shown below. +:::note Note + +If you are using the New Backend System, the `schedule` has to be setup in the config, as shown below. + +::: ```yaml catalog: diff --git a/docs/integrations/ldap/org--old.md b/docs/integrations/ldap/org--old.md new file mode 100644 index 0000000000..13fbcb9408 --- /dev/null +++ b/docs/integrations/ldap/org--old.md @@ -0,0 +1,417 @@ +--- +id: org--old +title: LDAP Organizational Data +sidebar_label: Org Data +# prettier-ignore +description: Setting up ingestion of organizational data from LDAP +--- + +The Backstage catalog can be set up to ingest organizational data - users and +groups - directly from an LDAP compatible service. The result is a hierarchy of +[`User`](../../features/software-catalog/descriptor-format.md#kind-user) and +[`Group`](../../features/software-catalog/descriptor-format.md#kind-group) kind +entities that mirror your org setup. + +## Supported vendors + +Backstage in general supports OpenLDAP compatible vendors, as well as Active Directory and FreeIPA. If you are using a vendor that does not seem to be supported, please [file an issue](https://github.com/backstage/backstage/issues/new?assignees=&labels=enhancement&template=feature_template.md). + +## Installation + +This guide will use the Entity Provider method. If you for some reason prefer +the Processor method (not recommended), it is described separately below. + +The provider is not installed by default, therefore you have to add a dependency +to `@backstage/plugin-catalog-backend-module-ldap` to your backend package. + +```bash +# From your Backstage root directory +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-ldap +``` + +:::note Note + +When configuring to use a Provider instead of a Processor you do not +need to add a _location_ pointing to your LDAP server + +::: + +Update the catalog plugin initialization in your backend to add the provider and +schedule it: + +```ts title="packages/backend/src/plugins/catalog.ts" +/* highlight-add-next-line */ +import { LdapOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-ldap'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const builder = await CatalogBuilder.create(env); + + /* highlight-add-start */ + // The target parameter below needs to match the ldap.providers.target + // value specified in your app-config. + builder.addEntityProvider( + LdapOrgEntityProvider.fromConfig(env.config, { + id: 'our-ldap-master', + target: 'ldaps://ds.example.net', + logger: env.logger, + schedule: env.scheduler.createScheduledTaskRunner({ + frequency: { minutes: 60 }, + timeout: { minutes: 15 }, + }), + }), + ); + /* highlight-add-end */ + + // .. +} +``` + +After this, you also have to add some configuration in your app-config that +describes what you want to import for that target. + +## Configuration + +The following configuration is a small example of how a setup could look for +importing groups and users from a corporate LDAP server. + +```yaml +ldap: + providers: + - target: ldaps://ds.example.net + bind: + dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net + secret: ${LDAP_SECRET} + users: + dn: ou=people,ou=example,dc=example,dc=net + options: + filter: (uid=*) + map: + description: l + set: + metadata.customField: 'hello' + groups: + dn: ou=access,ou=groups,ou=example,dc=example,dc=net + options: + filter: (&(objectClass=some-group-class)(!(groupType=email))) + map: + description: l + set: + metadata.customField: 'hello' +``` + +There may be many providers, each targeting a specific `target` which is +supposed to match the `target` of a dedicated provider instance - i.e., you will +add one entity provider class instance per target to ingest from. + +These config blocks have a lot of options in them, so we will describe each +"root" key within the block separately. + +### target + +This is the URL of the targeted server, typically on the form +`ldaps://ds.example.net` for SSL enabled servers or `ldap://ds.example.net` +without SSL. + +#### target.tls.keys + +`keys` in TLS options specifies location of a file, that contains private keys +to establish connection with your LDAP server, in PEM format. See an example +for Google Secure LDAP Service below. + +#### target.tls.certs + +`certs` in TLS options specifies location of a file, that contains certificate +chains to establish connection with your LDAP server, in PEM format. See an +example for Google Secure LDAP Service below. + +### bind + +The bind block specifies how the plugin should bind (essentially, to +authenticate) towards the server. It has the following fields. + +```yaml +dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net +secret: ${LDAP_SECRET} +``` + +The `dn` is the full LDAP Distinguished Name for the user that the plugin +authenticates itself as. At this point, only regular user based authentication +is supported. + +The `secret` is the password of the same user. In this example, it is given in +the form of an environment variable `LDAP_SECRET`, that has to be set when the +backend starts. + +### users + +The `users` block defines the settings that govern the reading and +interpretation of users. Its fields are explained in separate sections below. + +#### users.dn + +The DN under which users are stored, e.g. +`ou=people,ou=example,dc=example,dc=net`. + +#### users.options + +The search options to use when sending the query to the server, when reading all +users. All the options are shown below, with their default values, but they are +all optional. + +```yaml +options: + # One of 'base', 'one', or 'sub'. + scope: one + # The filter is the one that you commonly will want to specify explicitly. It + # is a string on the standard LDAP query format. Use it to select out the set + # of users that are of actual interest to ingest. For example, you may want + # to filter out disabled users. + filter: (uid=*) + # The attribute selectors for each item, as passed to the LDAP server. + attributes: ['*', '+'] + # This field is either 'false' to disable paging when reading from the + # server, or an object on the form '{ pageSize: 100, pagePause: true }' that + # specifies the details of how the paging shall work. + paged: false +``` + +#### users.set + +This optional piece lets you specify a number of JSON paths (on a.b.c form) and +hard coded values to set on those paths. This can be useful for example if you +want to hard code a namespace or similar on the generated entities. + +```yaml +set: + # Just an example; the key and value can be anything + metadata.namespace: 'ldap' +``` + +#### users.map + +Mappings from well known entity fields, to LDAP attribute names. This is where +you are able to define how to interpret the attributes of each LDAP result item, +and to move them into the corresponding entity fields. All the options are shown +below, with their default values, but they are all optional. + +If you leave out an optional mapping, it will still be copied using that default +value. For example, even if you do not put in the field `displayName` in your +config, the provider will still copy the attribute `cn` into the entity field +`spec.profile.displayName`. + +```yaml +map: + # The name of the attribute that holds the relative + # distinguished name of each entry. + rdn: uid + # The name of the attribute that shall be used for the value of + # the metadata.name field of the entity. + name: uid + # The name of the attribute that shall be used for the value of + # the metadata.description field of the entity. + description: description + # The name of the attribute that shall be used for the value of + # the spec.profile.displayName field of the entity. + displayName: cn + # The name of the attribute that shall be used for the value of + # the spec.profile.email field of the entity. + email: mail + # The name of the attribute that shall be used for the value of + # the spec.profile.picture field of the entity. + picture: + # The name of the attribute that shall be used for the values of + # the spec.memberOf field of the entity. + memberOf: memberOf +``` + +### groups + +The `groups` block defines the settings that govern the reading and +interpretation of groups. Its fields are explained in separate sections below. + +#### groups.dn + +The DN under which groups are stored, e.g. +`ou=people,ou=example,dc=example,dc=net`. + +#### groups.options + +The search options to use when sending the query to the server, when reading all +groups. All the options are shown below, with their default values, but they are +all optional. + +```yaml +options: + # One of 'base', 'one', or 'sub'. + scope: one + # The filter is the one that you commonly will want to specify explicitly. It + # is a string on the standard LDAP query format. Use it to select out the set + # of groups that are of actual interest to ingest. For example, you may want + # to filter out disabled groups. + filter: (&(objectClass=some-group-class)(!(groupType=email))) + # The attribute selectors for each item, as passed to the LDAP server. + attributes: ['*', '+'] + # This field is either 'false' to disable paging when reading from the + # server, or an object on the form '{ pageSize: 100, pagePause: true }' that + # specifies the details of how the paging shall work. + paged: false +``` + +#### groups.set + +This optional piece lets you specify a number of JSON paths (on a.b.c form) and +hard coded values to set on those paths. This can be useful for example if you +want to hard code a namespace or similar on the generated entities. + +```yaml +set: + # Just an example; the key and value can be anything + metadata.namespace: 'ldap' +``` + +#### groups.map + +Mappings from well known entity fields, to LDAP attribute names. This is where +you are able to define how to interpret the attributes of each LDAP result item, +and to move them into the corresponding entity fields. All of the options are +shown below, with their default values, but they are all optional. + +If you leave out an optional mapping, it will still be copied using that default +value. For example, even if you do not put in the field `displayName` in your +config, the provider will still copy the attribute `cn` into the entity field +`spec.profile.displayName`. If the target field is optional, such as the display +name, the importer will accept missing attributes and just leave the target +field unset. If the target field is mandatory, such as the name of the entity, +validation will fail if the source attribute is missing. + +```yaml +map: + # The name of the attribute that holds the relative + # distinguished name of each entry. This value is copied into a + # well known annotation to be able to query by it later. + rdn: cn + # The name of the attribute that shall be used for the value of + # the metadata.name field of the entity. + name: cn + # The name of the attribute that shall be used for the value of + # the metadata.description field of the entity. + description: description + # The name of the attribute that shall be used for the value of + # the spec.type field of the entity. + type: groupType + # The name of the attribute that shall be used for the value of + # the spec.profile.displayName field of the entity. + displayName: cn + # The name of the attribute that shall be used for the value of + # the spec.profile.email field of the entity. + email: + # The name of the attribute that shall be used for the value of + # the spec.profile.picture field of the entity. + picture: + # The name of the attribute that shall be used for the values of + # the spec.parent field of the entity. + memberOf: memberOf + # The name of the attribute that shall be used for the values of + # the spec.children field of the entity. + members: member +``` + +## Customize the Provider + +In case you want to customize the ingested entities, the provider allows to pass +transformers for users and groups. Here we will show an example of overriding +the group transformer. + +1. Create a transformer: + + ```ts + export async function myGroupTransformer( + vendor: LdapVendor, + config: GroupConfig, + group: SearchEntry, + ): Promise { + // Transformations may change namespace, change entity naming pattern, fill + // profile with more or other details... + + // Create the group entity on your own, or wrap the default transformer + return await defaultGroupTransformer(vendor, config, group); + } + ``` + +2. Configure the provider with the transformer: + + ```ts + const ldapEntityProvider = LdapOrgEntityProvider.fromConfig(env.config, { + id: 'our-ldap-master', + target: 'ldaps://ds.example.net', + logger: env.logger, + groupTransformer: myGroupTransformer, + }); + ``` + +## Using a Processor instead of a Provider + +An alternative to using the Provider for ingesting LDAP entries is to use a +Processor. This is the old way that's based on registering locations with the +proper type and target, triggering the processor to run. + +The drawback of this method is that it will leave orphaned Group/User entities +whenever they are deleted on your LDAP server, and you cannot control the +frequency with which they are refreshed, separately from other processors. + +### Processor Installation + +The `LdapOrgReaderProcessor` is not registered by default, so you have to +register it in the catalog plugin: + +```typescript title="packages/backend/src/plugins/catalog.ts" +builder.addProcessor( + LdapOrgReaderProcessor.fromConfig(env.config, { + logger: env.logger, + }), +); +``` + +### Driving LDAP Org Processor Ingestion with Locations + +Locations point out the specific org(s) you want to import. The `type` of these +locations must be `ldap-org`, and the `target` must point to the exact URL +(starting with `ldap://` or `ldaps://`) of the targeted LDAP server. You can +have several such location entries if you want, but typically you will have just +one. + +```yaml +catalog: + locations: + - type: ldap-org + target: ldaps://ds.example.net + rules: + - allow: [User, Group] +``` + +### Example configurations + +#### Google Secure LDAP Service + +To sync Google Workspace/Cloud Identity organization data to users and groups in backstage, +you must [configure Secure LDAP Service](https://support.google.com/a/answer/9048516) first. + +Once Secure LDAP Service is configured, you can enable TLS options in LDAP configuration, +as mentioned below. `keys` and `certs` specify the location of files that are generated +while configuring Secure LDAP Service above. + +```yaml +ldap: + providers: + - target: ldaps://ldap.google.com:636 + tls: + rejectUnauthorized: false + keys: '/var/secrets/tls/gldap.key' + certs: '/var/secrets/tls/gldap.crt' + users: + # users configuration comes here + groups: + # groups configuration comes here +``` diff --git a/docs/integrations/ldap/org.md b/docs/integrations/ldap/org.md index 49a2776fb1..f70d02afed 100644 --- a/docs/integrations/ldap/org.md +++ b/docs/integrations/ldap/org.md @@ -18,9 +18,6 @@ Backstage in general supports OpenLDAP compatible vendors, as well as Active Dir ## Installation -This guide will use the Entity Provider method. If you for some reason prefer -the Processor method (not recommended), it is described separately below. - The provider is not installed by default, therefore you have to add a dependency to `@backstage/plugin-catalog-backend-module-ldap` to your backend package. @@ -29,43 +26,30 @@ to `@backstage/plugin-catalog-backend-module-ldap` to your backend package. yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-ldap ``` -> Note: When configuring to use a Provider instead of a Processor you do not -> need to add a _location_ pointing to your LDAP server +Next add the basic configuration to `app-config.yaml` -Update the catalog plugin initialization in your backend to add the provider and -schedule it: - -```ts title="packages/backend/src/plugins/catalog.ts" -/* highlight-add-next-line */ -import { LdapOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-ldap'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const builder = await CatalogBuilder.create(env); - - /* highlight-add-start */ - // The target parameter below needs to match the ldap.providers.target - // value specified in your app-config. - builder.addEntityProvider( - LdapOrgEntityProvider.fromConfig(env.config, { - id: 'our-ldap-master', - target: 'ldaps://ds.example.net', - logger: env.logger, - schedule: env.scheduler.createScheduledTaskRunner({ - frequency: { minutes: 60 }, - timeout: { minutes: 15 }, - }), - }), - ); - /* highlight-add-end */ - - // .. -} +```yaml title="app-config.yaml" +catalog: + providers: + ldapOrg: + default: + target: ldaps://ds.example.net + bind: + dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net + secret: ${LDAP_SECRET} + schedule: + frequency: PT1H + timeout: PT15M ``` -After this, you also have to add some configuration in your app-config that -describes what you want to import for that target. +Finally, updated your backend by adding the following line: + +```ts title="packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-catalog-backend/alpha')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-catalog-backend-module-ldap')); +/* highlight-add-end */ +``` ## Configuration @@ -73,34 +57,32 @@ The following configuration is a small example of how a setup could look for importing groups and users from a corporate LDAP server. ```yaml -ldap: +catalog: providers: - - target: ldaps://ds.example.net - bind: - dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net - secret: ${LDAP_SECRET} - users: - dn: ou=people,ou=example,dc=example,dc=net - options: - filter: (uid=*) - map: - description: l - set: - metadata.customField: 'hello' - groups: - dn: ou=access,ou=groups,ou=example,dc=example,dc=net - options: - filter: (&(objectClass=some-group-class)(!(groupType=email))) - map: - description: l - set: - metadata.customField: 'hello' + ldapOrg: + default: + target: ldaps://ds.example.net + bind: + dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net + secret: ${LDAP_SECRET} + users: + dn: ou=people,ou=example,dc=example,dc=net + options: + filter: (uid=*) + map: + description: l + set: + metadata.customField: 'hello' + groups: + dn: ou=access,ou=groups,ou=example,dc=example,dc=net + options: + filter: (&(objectClass=some-group-class)(!(groupType=email))) + map: + description: l + set: + metadata.customField: 'hello' ``` -There may be many providers, each targeting a specific `target` which is -supposed to match the `target` of a dedicated provider instance - i.e., you will -add one entity provider class instance per target to ingest from. - These config blocks have a lot of options in them, so we will describe each "root" key within the block separately. @@ -317,97 +299,34 @@ map: ## Customize the Provider In case you want to customize the ingested entities, the provider allows to pass -transformers for users and groups. Here we will show an example of overriding -the group transformer. +transformers for users and groups. -1. Create a transformer: +Transformers can be configured by extending `ldapOrgEntityProviderTransformExtensionPoint`. Here is an example: - ```ts - export async function myGroupTransformer( - vendor: LdapVendor, - config: GroupConfig, - group: SearchEntry, - ): Promise { - // Transformations may change namespace, change entity naming pattern, fill - // profile with more or other details... +```ts title="packages/backend/src/index.ts" +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { ldapOrgEntityProviderTransformExtensionPoint } from '@backstage/plugin-catalog-backend-module-ldap'; +import { myUserTransformer, myGroupTransformer } from './transformers'; - // Create the group entity on your own, or wrap the default transformer - return await defaultGroupTransformer(vendor, config, group); - } - ``` - -2. Configure the provider with the transformer: - - ```ts - const ldapEntityProvider = LdapOrgEntityProvider.fromConfig(env.config, { - id: 'our-ldap-master', - target: 'ldaps://ds.example.net', - logger: env.logger, - groupTransformer: myGroupTransformer, - }); - ``` - -## Using a Processor instead of a Provider - -An alternative to using the Provider for ingesting LDAP entries is to use a -Processor. This is the old way that's based on registering locations with the -proper type and target, triggering the processor to run. - -The drawback of this method is that it will leave orphaned Group/User entities -whenever they are deleted on your LDAP server, and you cannot control the -frequency with which they are refreshed, separately from other processors. - -### Processor Installation - -The `LdapOrgReaderProcessor` is not registered by default, so you have to -register it in the catalog plugin: - -```typescript title="packages/backend/src/plugins/catalog.ts" -builder.addProcessor( - LdapOrgReaderProcessor.fromConfig(env.config, { - logger: env.logger, +backend.add( + createBackendModule({ + pluginId: 'catalog', + moduleId: 'ldap-extensions', + register(env) { + env.registerInit({ + deps: { + /* highlight-add-start */ + ldapTransformers: ldapOrgEntityProviderTransformExtensionPoint, + /* highlight-add-end */ + }, + async init({ ldapTransformers }) { + /* highlight-add-start */ + ldapTransformers.setUserTransformer(myUserTransformer); + ldapTransformers.setGroupTransformer(myGroupTransformer); + /* highlight-add-end */ + }, + }); + }, }), ); ``` - -### Driving LDAP Org Processor Ingestion with Locations - -Locations point out the specific org(s) you want to import. The `type` of these -locations must be `ldap-org`, and the `target` must point to the exact URL -(starting with `ldap://` or `ldaps://`) of the targeted LDAP server. You can -have several such location entries if you want, but typically you will have just -one. - -```yaml -catalog: - locations: - - type: ldap-org - target: ldaps://ds.example.net - rules: - - allow: [User, Group] -``` - -### Example configurations - -#### Google Secure LDAP Service - -To sync Google Workspace/Cloud Identity organization data to users and groups in backstage, -you must [configure Secure LDAP Service](https://support.google.com/a/answer/9048516) first. - -Once Secure LDAP Service is configured, you can enable TLS options in LDAP configuration, -as mentioned below. `keys` and `certs` specify the location of files that are generated -while configuring Secure LDAP Service above. - -```yaml -ldap: - providers: - - target: ldaps://ldap.google.com:636 - tls: - rejectUnauthorized: false - keys: '/var/secrets/tls/gldap.key' - certs: '/var/secrets/tls/gldap.crt' - users: - # users configuration comes here - groups: - # groups configuration comes here -``` diff --git a/docs/permissions/getting-started--new.md b/docs/permissions/getting-started--new.md new file mode 100644 index 0000000000..b6198a222f --- /dev/null +++ b/docs/permissions/getting-started--new.md @@ -0,0 +1,36 @@ +--- +id: getting-started--new +title: Getting Started +description: How to get started with the permission framework as an integrator +--- + +Backstage integrators control permissions by writing a policy. In general terms, a policy is simply an async function which receives a request to authorize a specific action for a user and (optional) resource, and returns a decision on whether to authorize that permission. Integrators can implement their own policies from scratch, or adopt reusable policies written by others. + +## Prerequisites + +The permissions framework depends on a few other Backstage systems, which must be set up before we can dive into writing a policy. + +### Upgrade to the latest version of Backstage + +To ensure your version of Backstage has all the latest permission-related functionality, it’s important to upgrade to the latest version. The [Backstage upgrade helper](https://backstage.github.io/upgrade-helper/) is a great tool to help ensure that you’ve made all the necessary changes during the upgrade! + +### Supply an identity resolver to populate group membership on sign in + +**Note**: If you are working off of an existing Backstage instance, you likely already have some form of an identity resolver set up. + +Like many other parts of Backstage, the permissions framework relies on information about group membership. This simplifies authoring policies through the use of groups, rather than requiring each user to be listed in the configuration. Group membership is also often useful for conditional permissions, for example allowing permissions to act on an entity to be granted when a user is a member of a group that owns that entity. + +[The IdentityResolver docs](../auth/identity-resolver.md) describe the process for resolving group membership on sign in. + +## Enable and test the permissions system + +All you need to do now is enable the permissions system in your Backstage instance! + +1. Set the property `permission.enabled` to `true` in `app-config.yaml`. + +```yaml title="app-config.yaml" +permission: + enabled: true +``` + +Congratulations! Now that the framework is configured, you can craft a permission policy that works best for your organization by utilizing a provided authorization method or by [writing your own policy](./writing-a-policy.md)! diff --git a/docs/permissions/getting-started.md b/docs/permissions/getting-started.md index e241f0e38e..4dabf3fcbd 100644 --- a/docs/permissions/getting-started.md +++ b/docs/permissions/getting-started.md @@ -8,7 +8,11 @@ If you prefer to watch a video instead, you can start with this video introducti -> Note: This video was recorded in the January 2022 Contributors Session using `@backstage/create-app@0.4.14`. Some aspects of the demo may have changed in later releases. +:::note Note + +This video was recorded in the January 2022 Contributors Session using `@backstage/create-app@0.4.14`. Some aspects of the demo may have changed in later releases. + +::: Backstage integrators control permissions by writing a policy. In general terms, a policy is simply an async function which receives a request to authorize a specific action for a user and (optional) resource, and returns a decision on whether to authorize that permission. Integrators can implement their own policies from scratch, or adopt reusable policies written by others. diff --git a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md index 8541d64465..aacd60af8c 100644 --- a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md +++ b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md @@ -39,7 +39,7 @@ For this tutorial, we've automatically exported all permissions from this file ( :::note Note -We use a separate `todo-list-common` package since all permissions authorized by your plugin should be exported from a ["common-library" package](https://backstage.io/docs/local-dev/cli-build-system#package-roles). This allows Backstage integrators to reference them in frontend components as well as permission policies. +We use a separate `todo-list-common` package since all permissions authorized by your plugin should be exported from a ["common-library" package](https://backstage.io/docs/tooling/cli/build-system#package-roles). This allows Backstage integrators to reference them in frontend components as well as permission policies. ::: diff --git a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md index a9e62040c5..84e86140fc 100644 --- a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md +++ b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md @@ -36,7 +36,11 @@ This approach will work for simple cases, but it has a downside: it forces us to To avoid this situation, the permissions framework has support for filtering items in the data source itself. In this part of the tutorial, we'll describe the steps required to use that behavior. -> Note: in order to perform authorization filtering in this way, the data source must allow filters to be logically combined with AND, OR, and NOT operators. The conditional decisions returned by the permissions framework use a [nested object](https://backstage.io/docs/reference/plugin-permission-common.permissioncriteria) to combine conditions. If you're implementing a filter API from scratch, we recommend using the same shape for ease of interoperability. If not, you'll need to implement a function which transforms the nested object into your own format. +:::note Note + +In order to perform authorization filtering in this way, the data source must allow filters to be logically combined with AND, OR, and NOT operators. The conditional decisions returned by the permissions framework use a [nested object](https://backstage.io/docs/reference/plugin-permission-common.permissioncriteria) to combine conditions. If you're implementing a filter API from scratch, we recommend using the same shape for ease of interoperability. If not, you'll need to implement a function which transforms the nested object into your own format. + +::: ## Creating the read permission diff --git a/docs/permissions/plugin-authors/05-frontend-authorization.md b/docs/permissions/plugin-authors/05-frontend-authorization.md index 92d855698f..60458aaf4d 100644 --- a/docs/permissions/plugin-authors/05-frontend-authorization.md +++ b/docs/permissions/plugin-authors/05-frontend-authorization.md @@ -8,7 +8,11 @@ In the previous sections, we learned how to protect our plugin's backend API rou Take, for example, the "Add" button in our todo list application. When a user clicks this button, the frontend makes a `POST` request to the `/todos` route of our backend. If a user tries to add a todo but is not authorized, they will have no way of knowing this until they perform the action and are faced with an error. This is a poor user experience. We can do better by disabling the add button. -> Note: Placing frontend components behind authorization cannot take the place of placing your backend routes behind authorization. Authorization checks on the frontend should be used in _addition_ to the corresponding backend authorization, as an improvement to the user experience. If you do not place your backend route behind authorization, a malicious actor can still send a request to the route even if you disabled the corresponding frontend component. +:::note Note + +Placing frontend components behind authorization cannot take the place of placing your backend routes behind authorization. Authorization checks on the frontend should be used in _addition_ to the corresponding backend authorization, as an improvement to the user experience. If you do not place your backend route behind authorization, a malicious actor can still send a request to the route even if you disabled the corresponding frontend component. + +::: ## Using `usePermission` diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index 7a51cb635c..255ab4f865 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -44,7 +44,11 @@ cd plugins/carmen-backend yarn start ``` -> Note: this documentation assumes you are using the latest version of Backstage and the new backend system. If you are not, please upgrade and migrate your backend using the [Migration Guide](../backend-system/building-backends/08-migrating.md) +:::note Note + +This documentation assumes you are using the latest version of Backstage and the new backend system. If you are not, please upgrade and migrate your backend using the [Migration Guide](../backend-system/building-backends/08-migrating.md) + +::: This will think for a bit, and then say `Listening on :7007`. In a different terminal window, now run diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md index b18bae31c6..1a9c0babf9 100644 --- a/docs/plugins/composability.md +++ b/docs/plugins/composability.md @@ -325,6 +325,33 @@ concrete routes directly. Although there can be some benefits to using the full routing system even in internal plugins. It can help you structure your routes, and as you will see further down it also helps you manage route parameters. +You can also use static configuration to bind routes, removing the need to make +changes to the app code. It does however mean that you won't get type safety +when binding routes and compile-time validation of the bindings. Static +configuration of route bindings is done under the `app.routes.bindings` key in +`app-config.yaml`. It works the same way as [route bindings in the new frontend system](../frontend-system/architecture/07-routes.md#binding-external-route-references), +for example: + +```yaml +app: + routes: + bindings: + bar.headerLink: foo.root +``` + +### Default Targets for External Route References + +Following the `1.28` release of Backstage you can now define default targets for +external route references. They work the same way as [default targets in the new frontend system](../frontend-system/architecture/07-routes.md#default-targets-for-external-route-references), +for example: + +```ts +export const createComponentExternalRouteRef = createExternalRouteRef({ + // highlight-next-line + defaultTarget: 'scaffolder.createComponent', +}); +``` + ### Optional External Routes When creating an `ExternalRouteRef` it is possible to mark it as optional: diff --git a/docs/plugins/create-a-plugin.md b/docs/plugins/create-a-plugin.md index 481b610e4e..d80e9cd57e 100644 --- a/docs/plugins/create-a-plugin.md +++ b/docs/plugins/create-a-plugin.md @@ -11,7 +11,7 @@ A Backstage Plugin adds functionality to Backstage. To create a new frontend plugin, make sure you've run `yarn install` and installed dependencies, then run the following on your command line (a shortcut to invoking the -[`backstage-cli new --select plugin`](../local-dev/cli-commands.md#new)) +[`backstage-cli new --select plugin`](../tooling/cli/03-commands.md#new)) from the root of your project. ```bash diff --git a/docs/plugins/proxying.md b/docs/plugins/proxying.md index bff38fea40..0b64c6eb37 100644 --- a/docs/plugins/proxying.md +++ b/docs/plugins/proxying.md @@ -28,12 +28,7 @@ backend.add(import('@backstage/plugin-proxy-backend/alpha')); In `packages/backend/src/index.ts`: ```ts -const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); - -const service = createServiceBuilder(module) - .loadConfig(configReader) - /** ... other routers ... */ - .addRouter('/proxy', await proxy(proxyEnv)); +backend.add(import('@backstage/plugin-proxy-backend/alpha')); ``` ## Configuration @@ -50,6 +45,7 @@ proxy: /simple-example: http://simple.example.com:8080 '/larger-example/v1': target: http://larger.example.com:8080/svc.v1 + credentials: require headers: Authorization: ${EXAMPLE_AUTH_HEADER} # ...or interpolating a value into part of a string, @@ -66,6 +62,23 @@ backend requests to `/api/proxy/simple-example/...` and The value inside each route is either a simple URL string, or an object on the format accepted by [http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware). +Additionally, it has an optional `credentials` key which can have the following +values: + +- `require`: Callers must provide Backstage user or service credentials with + each request. The credentials are not forwarded to the proxy target. This is + the default. +- `forward`: Callers must provide Backstage user or service credentials with + each request, and those credentials are forwarded to the proxy target. +- `dangerously-allow-unauthenticated`: No Backstage credentials are required to + access this proxy target. The target can still apply its own credentials + checks, but the proxy will not help block non-Backstage-blessed callers. If + you also add `allowedHeaders: ['Authorization']` to an endpoint configuration, + then the Backstage token (if provided) WILL be forwarded. + +Note that if you have `backend.auth.dangerouslyDisableDefaultAuthPolicy` set to +`true`, the `credentials` value does not apply; the proxy will behave as if all +endpoints were set to `dangerously-allow-unauthenticated`. If the value is a string, it is assumed to correspond to: @@ -74,6 +87,7 @@ target: changeOrigin: true pathRewrite: '^/': '/' +credentials: require ``` When the target is an object, it is given verbatim to `http-proxy-middleware` @@ -86,6 +100,7 @@ except with the following caveats for convenience: `'^/api/proxy/larger-example/v1/': '/'` is added. That means that a request to `/api/proxy/larger-example/v1/some/path` will be translated to a request to `http://larger.example.com:8080/svc.v1/some/path`. +- If `credentials` is not specified, it is set to `require`. There are also additional settings: diff --git a/docs/plugins/testing.md b/docs/plugins/testing.md index bc212a2581..a97b3d8166 100644 --- a/docs/plugins/testing.md +++ b/docs/plugins/testing.md @@ -369,4 +369,4 @@ Note: wrapping in the test application **requires** you to do a `find()` or ## Debugging Jest Tests -You can find it [here](https://backstage.io/docs/local-dev/cli-build-system#debugging-jest-tests) +You can find it [here](https://backstage.io/docs/tooling/cli/build-system#debugging-jest-tests) diff --git a/docs/references/glossary.md b/docs/references/glossary.md index 6db16443e2..906260e710 100644 --- a/docs/references/glossary.md +++ b/docs/references/glossary.md @@ -201,7 +201,7 @@ A service that hosts [packages](#package). The most prominent example is [NPM](h ## Package Role -The declared role of a package, see [package roles](../local-dev/cli-build-system.md#package-roles). +The declared role of a package, see [package roles](../tooling/cli/02-build-system.md#package-roles). ## Permission (core Backstage plugin) diff --git a/docs/releases/v1.20.0.md b/docs/releases/v1.20.0.md index 921b8219fb..0d0075aa41 100644 --- a/docs/releases/v1.20.0.md +++ b/docs/releases/v1.20.0.md @@ -12,7 +12,7 @@ A huge thanks to the whole team of maintainers and contributors as well as the a ### Support for experimental type build has been removed -The `--experimental-type-build` option is no longer supported by any commands in the Backstage CLI. Existing usage should be migrated to using [subpath exports](https://backstage.io/docs/local-dev/cli-build-system#subpath-exports) instead. +The `--experimental-type-build` option is no longer supported by any commands in the Backstage CLI. Existing usage should be migrated to using [subpath exports](https://backstage.io/docs/tooling/cli/build-system#subpath-exports) instead. ### Experimental support for Vite ⚡ diff --git a/docs/releases/v1.28.0-next.0-changelog.md b/docs/releases/v1.28.0-next.0-changelog.md new file mode 100644 index 0000000000..69c17d7c7d --- /dev/null +++ b/docs/releases/v1.28.0-next.0-changelog.md @@ -0,0 +1,2101 @@ +# Release v1.28.0-next.0 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.28.0-next.0](https://backstage.github.io/upgrade-helper/?to=1.28.0-next.0) + +## @backstage/plugin-catalog-backend@1.23.0-next.0 + +### Minor Changes + +- c7528b0: Pass through `EventsService` too in the new backend system + +### Patch Changes + +- 8869b8e: Updated local development setup. +- 1779188: Start using the `isDatabaseConflictError` helper from the `@backstage/backend-plugin-api` package in order to avoid dependency with the soon to deprecate `@backstage/backend-common` package. +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.25-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/backend-openapi-utils@0.1.12-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-proxy-backend@0.5.0-next.0 + +### Minor Changes + +- 88480e4: **BREAKING**: The proxy backend plugin is now protected by Backstage auth, by + default. Unless specifically configured (see below), all proxy endpoints will + reject requests immediately unless a valid Backstage user or service token is + passed along with the request. This aligns the proxy with how other Backstage + backends behave out of the box, and serves to protect your upstreams from + unauthorized access. + + A proxy configuration section can now look as follows: + + ```yaml + proxy: + endpoints: + '/pagerduty': + target: https://api.pagerduty.com + credentials: require # NEW! + headers: + Authorization: Token token=${PAGERDUTY_TOKEN} + ``` + + There are three possible `credentials` settings at this point: + + - `require`: Callers must provide Backstage user or service credentials with + each request. The credentials are not forwarded to the proxy target. + - `forward`: Callers must provide Backstage user or service credentials with + each request, and those credentials are forwarded to the proxy target. + - `dangerously-allow-unauthenticated`: No Backstage credentials are required to + access this proxy target. The target can still apply its own credentials + checks, but the proxy will not help block non-Backstage-blessed callers. If + you also add `allowedHeaders: ['Authorization']` to an endpoint configuration, + then the Backstage token (if provided) WILL be forwarded. + + The value `dangerously-allow-unauthenticated` was the old default. + + The value `require` is the new default, so requests that were previously + permitted may now start resulting in `401 Unauthorized` responses. If you have + `backend.auth.dangerouslyDisableDefaultAuthPolicy` set to `true`, this does not + apply; the proxy will behave as if all endpoints were set to + `dangerously-allow-unauthenticated`. + + If you have proxy endpoints that require unauthenticated access still, please + add `credentials: dangerously-allow-unauthenticated` to their declarations in + your app-config. + +### Patch Changes + +- 8869b8e: Updated local development setup. +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/app-defaults@1.5.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-plugin-api@1.9.2 + - @backstage/plugin-permission-react@0.4.22 + +## @backstage/backend-app-api@0.7.6-next.0 + +### Patch Changes + +- b7de623: Fixed a potential crash when passing an object with a `null` prototype as log meta. +- 7d30d95: Fixing issue with log meta fields possibly being circular refs +- 6a576dc: Stop using `getVoidLogger` in tests to reduce the dependency on the soon-to-deprecate `backstage-common` package. +- 6551b3d: Deprecated core service factories and implementations and moved them over to + subpath exports on `@backstage/backend-defaults` instead. E.g. + `@backstage/backend-defaults/scheduler` is where the service factory and default + implementation of `coreServices.scheduler` now lives. +- d617103: Updating the logger redaction message to something less dramatic +- Updated dependencies + - @backstage/cli-node@0.2.6-next.0 + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-common@0.22.1-next.0 + +### Patch Changes + +- c6c0919: Updated configuration schema to include the `useRedisSets` cache config option. +- 1779188: In preparation to the new backend system stable release, the `isDatabaseConflictError` helper have been moved to the `@backstage/backend-plugin-api` package and deprecated from `@backstage/backend-common`. +- 8869b8e: We are deprecating the legacy `createServiceBuilder` factory, so if you are still using it, please checkout the migration guide and [migrate](https://backstage.io/docs/backend-system/building-plugins-and-modules/migrating) your plugin to use the new backend system. +- 3bd04bb: We are deprecating the legacy router handlers and contexts in preparation for the new backend system stable release. +- 6a576dc: Deprecate legacy service logger helpers and stop using `getVoidLogger` in tests. +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + +## @backstage/backend-defaults@0.2.19-next.0 + +### Patch Changes + +- 6551b3d: Added core service factories and implementations from + `@backstage/backend-app-api`. They are now available as subpath exports, e.g. + `@backstage/backend-defaults/scheduler` is where the service factory and default + implementation of `coreServices.scheduler` now lives. They have been marked as + deprecated in their old locations. +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-dynamic-feature-service@0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.0 + - @backstage/cli-node@0.2.6-next.0 + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-catalog-backend@1.23.0-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-events-backend@0.3.6-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/plugin-app-node@0.1.19-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/backend-openapi-utils@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/errors@1.2.4 + +## @backstage/backend-plugin-api@0.6.19-next.0 + +### Patch Changes + +- 6551b3d: Moved the declaration of the `SchedulerService` here, along with prefixed versions of all of the types it depends on, from `@backstage/backend-tasks` +- 1779188: Start using the `isDatabaseConflictError` helper from the `@backstage/backend-plugin-api` package in order to avoid dependency with the soon to deprecate `@backstage/backend-common` package. +- Updated dependencies + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/backend-tasks@0.5.24-next.0 + +### Patch Changes + +- 736bc3c: Marked all exports as deprecated and pointed at `@backstage/backend-plugin-api` and `@backstage/backend-defaults` +- 6a576dc: Deprecate the legacy `TaskScheduler.fromConfig` method and stop using the `getVoidlogger` in tests files to reduce the dependency on the soon-to-deprecate `backstage-common` package. +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-test-utils@0.3.9-next.0 + +### Patch Changes + +- 6a576dc: Fix the logger service mock to prevent returning `undefined` from the `child` method. +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/cli@0.26.6-next.0 + +### Patch Changes + +- 009da47: Fix `versions:check --fix` when `yarn.lock` has multiple joint versions in the same section +- 9ee948a: Bump `esbuild` target for package builds to `ES2022`. +- Updated dependencies + - @backstage/cli-node@0.2.6-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/integration@1.11.0 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## @backstage/cli-node@0.2.6-next.0 + +### Patch Changes + +- 93be042: Upgraded @yarnpkg/parsers to stable 3.0 +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/core-compat-api@0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.9.2 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/version-bridge@1.0.8 + +## @backstage/core-components@0.14.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/version-bridge@1.0.8 + +## @backstage/create-app@0.5.16-next.0 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.13 + +## @backstage/dev-utils@1.0.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/app-defaults@1.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-plugin-api@1.9.2 + - @backstage/integration-react@1.1.27 + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## @backstage/frontend-app-api@0.7.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/frontend-plugin-api@0.6.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/frontend-test-utils@0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-app-api@0.7.1-next.0 + - @backstage/test-utils@1.5.6-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/types@1.1.1 + +## @backstage/repo-tools@0.9.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-node@0.2.6-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.13 + - @backstage/errors@1.2.4 + +## @techdocs/cli@1.8.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-techdocs-node@1.12.5-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + +## @backstage/test-utils@1.5.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-plugin-api@1.9.2 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-permission-react@0.4.22 + +## @backstage/theme@0.5.6-next.0 + +### Patch Changes + +- 702fa7d: Internal refactor to fix an issue where the MUI 5 `v5-` class prefixing gets removed by tree shaking. + +## @backstage/plugin-api-docs@0.11.6-next.0 + +### Patch Changes + +- 96cd13e: `DefaultApiExplorerPage` now accepts an optional `ownerPickerMode` for toggling the behavior of the `EntityOwnerPicker`, exposing a new mode `` particularly suitable for larger catalogs. In this new mode, `EntityOwnerPicker` will display all the users and groups present in the catalog. +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/plugin-catalog@1.20.1-next.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-catalog-react@1.12.1-next.0 + - @backstage/plugin-permission-react@0.4.22 + +## @backstage/plugin-app-backend@0.3.68-next.0 + +### Patch Changes + +- 8869b8e: Updated local development setup. +- 82c2b90: Restore the support of external config schema in the router of the `app-backend` plugin, which was broken in release `1.26.0`. + This support is critical for dynamic frontend plugins to have access to their config values. +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-app-node@0.1.19-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-app-node@0.1.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config-loader@1.8.0 + +## @backstage/plugin-app-visualizer@0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/frontend-plugin-api@0.6.6-next.0 + +## @backstage/plugin-auth-backend@0.22.6-next.0 + +### Patch Changes + +- 8869b8e: Updated local development setup. +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.11-next.0 + - @backstage/plugin-auth-backend-module-oidc-provider@0.1.10-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.1.11-next.0 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.2-next.0 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.2-next.0 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.2-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.16-next.0 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.16-next.0 + - @backstage/plugin-auth-backend-module-google-provider@0.1.16-next.0 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.14-next.0 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.1.16-next.0 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.12-next.0 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.14-next.0 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.12-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-auth-backend@0.22.6-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend-module-github-provider@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + +## @backstage/plugin-auth-backend-module-google-provider@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + +## @backstage/plugin-auth-backend-module-guest-provider@0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-auth-backend@0.22.6-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + +## @backstage/plugin-auth-backend-module-okta-provider@0.0.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-auth-node@0.4.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-react@0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog@1.20.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/plugin-search-react@1.7.12-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/integration-react@1.1.27 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-catalog-react@1.12.1-next.0 + - @backstage/plugin-permission-react@0.4.22 + - @backstage/plugin-scaffolder-common@1.5.2 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-catalog-backend-module-aws@0.3.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-kubernetes-common@0.7.6 + +## @backstage/plugin-catalog-backend-module-azure@0.1.39-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.11.0 + - @backstage/plugin-catalog-common@1.0.23 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/backend-openapi-utils@0.1.12-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.2.6-next.0 + +### Patch Changes + +- 6a576dc: Replace the usage of `getVoidLogger` with `mockServices.logger.mock` in order to remove the dependency with the soon-to-be-deprecated `backend-common` package. +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.11.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.19 + - @backstage/plugin-catalog-common@1.0.23 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-kubernetes-common@0.7.6 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + +## @backstage/plugin-catalog-backend-module-github@0.6.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-catalog-backend@1.23.0-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.11.0 + - @backstage/plugin-catalog-common@1.0.23 + +## @backstage/plugin-catalog-backend-module-github-org@0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-backend-module-github@0.6.2-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.3.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.11.0 + - @backstage/plugin-catalog-common@1.0.23 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.0.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.17-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-catalog-backend@1.23.0-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.23 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.23 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.37-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-catalog-backend@1.23.0-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.11.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.23 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-scaffolder-common@1.5.2 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.4.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.1 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-catalog-graph@0.4.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## @backstage/plugin-catalog-import@0.11.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/integration@1.11.0 + - @backstage/integration-react@1.1.27 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## @backstage/plugin-catalog-node@1.12.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-catalog-react@1.12.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/integration-react@1.1.27 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-permission-react@0.4.22 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + +## @backstage/plugin-config-schema@0.1.56-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-devtools@0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/plugin-devtools-common@0.1.9 + - @backstage/plugin-permission-react@0.4.22 + +## @backstage/plugin-devtools-backend@0.3.5-next.0 + +### Patch Changes + +- 8869b8e: Updated local development setup. +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-devtools-common@0.1.9 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-events-backend@0.3.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-module-aws-sqs@0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-events-backend-module-azure@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + +## @backstage/plugin-events-backend-module-gerrit@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + +## @backstage/plugin-events-backend-module-github@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-module-gitlab@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-test-utils@0.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.5-next.0 + +## @backstage/plugin-events-node@0.3.5-next.0 + +### Patch Changes + +- 6a576dc: Replace the usage of `getVoidLogger` with `mockServices.logger.mock` in order to remove the dependency with the soon-to-be-deprecated `backend-common` package. +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + +## @backstage/plugin-home@0.7.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.0 + - @backstage/plugin-home-react@0.1.14-next.0 + +## @backstage/plugin-home-react@0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/core-plugin-api@1.9.2 + +## @backstage/plugin-kubernetes@0.11.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/plugin-catalog-react@1.12.1-next.0 + - @backstage/plugin-kubernetes-common@0.7.6 + - @backstage/plugin-kubernetes-react@0.3.6-next.0 + +## @backstage/plugin-kubernetes-backend@0.17.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-kubernetes-node@0.1.13-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.7.6 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-kubernetes-cluster@0.0.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/plugin-catalog-react@1.12.1-next.0 + - @backstage/plugin-kubernetes-common@0.7.6 + - @backstage/plugin-kubernetes-react@0.3.6-next.0 + +## @backstage/plugin-kubernetes-node@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.7.6 + +## @backstage/plugin-kubernetes-react@0.3.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.7.6 + +## @backstage/plugin-notifications@0.2.2-next.0 + +### Patch Changes + +- 7f02684: Do not always show scrollbars in notification description +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.3 + - @backstage/plugin-signals-react@0.0.3 + +## @backstage/plugin-notifications-backend@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-notifications-node@0.1.5-next.0 + - @backstage/plugin-signals-node@0.1.5-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-notifications-common@0.0.3 + +## @backstage/plugin-notifications-backend-module-email@0.0.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-notifications-node@0.1.5-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.3 + +## @backstage/plugin-notifications-node@0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-signals-node@0.1.5-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-notifications-common@0.0.3 + +## @backstage/plugin-org@0.6.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## @backstage/plugin-org-react@0.1.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## @backstage/plugin-permission-backend@0.5.43-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-permission-node@0.7.30-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-scaffolder@1.20.1-next.0 + +### Patch Changes + +- 612a453: Change owner to project for azure host +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.8.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/integration@1.11.0 + - @backstage/integration-react@1.1.27 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-catalog-react@1.12.1-next.0 + - @backstage/plugin-permission-react@0.4.22 + - @backstage/plugin-scaffolder-common@1.5.2 + +## @backstage/plugin-scaffolder-backend@1.22.8-next.0 + +### Patch Changes + +- 7d30d95: Fixing issue with log meta fields possibly being circular refs +- d617103: Updating the logger redaction message to something less dramatic +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.2.9-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.1-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.11-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.9-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.9-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.9-next.0 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.11-next.0 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.9-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-scaffolder-common@1.5.2 + +## @backstage/plugin-scaffolder-backend-module-azure@0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.9-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.9-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.43-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + +## @backstage/plugin-scaffolder-backend-module-github@0.2.9-next.0 + +### Patch Changes + +- f145a04: Added handling for dry run to githubPullRequest and githubWebhook and added tests for this functionality +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.0.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-notifications-node@0.1.5-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/plugin-notifications-common@0.0.3 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node-test-utils@0.1.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-node@0.4.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-common@1.5.2 + +## @backstage/plugin-scaffolder-node-test-utils@0.1.5-next.0 + +### Patch Changes + +- 6a576dc: Replace the usage of `getVoidLogger` with `mockServices.logger.mock` in order to remove the dependency with the soon-to-be-deprecated `backend-common` package. +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-test-utils@0.3.9-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-react@1.8.6-next.0 + +### Patch Changes + +- 86dc29d: Links that are rendered in the markdown in the `ScaffolderField` component are now opened in new tabs. +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-catalog-react@1.12.1-next.0 + - @backstage/plugin-scaffolder-common@1.5.2 + +## @backstage/plugin-search@1.4.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/plugin-search-react@1.7.12-next.0 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-catalog-react@1.12.1-next.0 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-backend@1.5.10-next.0 + +### Patch Changes + +- 8869b8e: Updated local development setup. +- 5b6f979: Split backend search plugin startup into "init" and "start" stages to ensure necessary initialization has happened before startup +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/backend-defaults@0.2.19-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/repo-tools@0.9.1-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/backend-openapi-utils@0.1.12-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-backend-module-catalog@0.1.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-backend-module-elasticsearch@1.4.2-next.0 + +### Patch Changes + +- 6a576dc: Replace the usage of `getVoidLogger` with `mockServices.logger.mock` in order to remove the dependency with the soon-to-be-deprecated `backend-common` package. +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-backend-module-explore@0.1.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-backend-module-pg@0.5.28-next.0 + +### Patch Changes + +- 6a576dc: Replace the usage of `getVoidLogger` with `mockServices.logger.mock` in order to remove the dependency with the soon-to-be-deprecated `backend-common` package. +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-backend-module-techdocs@0.1.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-techdocs-node@1.12.5-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-backend-node@1.2.24-next.0 + +### Patch Changes + +- 6a576dc: Replace the usage of `getVoidLogger` with `mockServices.logger.mock` in order to remove the dependency with the soon-to-be-deprecated `backend-common` package. +- 5b6f979: Split backend search plugin startup into "init" and "start" stages to ensure necessary initialization has happened before startup +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-react@1.7.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-signals@0.0.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.3 + +## @backstage/plugin-signals-backend@0.1.5-next.0 + +### Patch Changes + +- 6a576dc: Replace the usage of `getVoidLogger` with `mockServices.logger.mock` in order to remove the dependency with the soon-to-be-deprecated `backend-common` package. +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-signals-node@0.1.5-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-signals-node@0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-techdocs@1.10.6-next.0 + +### Patch Changes + +- 654af4a: mkdocs-material have updated their CSS variable template, and a few are unset in Backstage. This patch adds the missing variables to ensure coverage. +- 96cd13e: `TechDocsIndexPage` now accepts an optional `ownerPickerMode` for toggling the behavior of the `EntityOwnerPicker`, exposing a new mode `` particularly suitable for larger catalogs. In this new mode, `EntityOwnerPicker` will display all the users and groups present in the catalog. +- e40bd9a: Fixed bug in CopyToClipboardButton component where positioning of the "Copy to clipboard" button in techdocs code snippets was broken in some cases +- 1256d88: Fix weird opening behaviour of the component. +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/plugin-search-react@1.7.12-next.0 + - @backstage/plugin-techdocs-react@1.2.5-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/integration@1.11.0 + - @backstage/integration-react@1.1.27 + - @backstage/plugin-auth-react@0.1.3-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.0 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.6-next.0 + - @backstage/test-utils@1.5.6-next.0 + - @backstage/plugin-search-react@1.7.12-next.0 + - @backstage/plugin-techdocs-react@1.2.5-next.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-plugin-api@1.9.2 + - @backstage/integration-react@1.1.27 + - @backstage/plugin-catalog@1.20.1-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## @backstage/plugin-techdocs-backend@1.10.6-next.0 + +### Patch Changes + +- 8869b8e: Updated local development setup. +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.0 + - @backstage/plugin-techdocs-node@1.12.5-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/plugin-techdocs-react@1.2.5-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/integration@1.11.0 + - @backstage/integration-react@1.1.27 + +## @backstage/plugin-techdocs-node@1.12.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-techdocs-react@1.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-user-settings@0.8.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## @backstage/plugin-user-settings-backend@0.2.18-next.0 + +### Patch Changes + +- 8869b8e: Updated local development setup. +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## example-app@0.2.98-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.6-next.0 + - @backstage/plugin-notifications@0.2.2-next.0 + - @backstage/plugin-scaffolder-react@1.8.6-next.0 + - @backstage/theme@0.5.6-next.0 + - @backstage/plugin-api-docs@0.11.6-next.0 + - @backstage/cli@0.26.6-next.0 + - @backstage/plugin-scaffolder@1.20.1-next.0 + - @backstage/app-defaults@1.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/frontend-app-api@0.7.1-next.0 + - @backstage/plugin-home@0.7.5-next.0 + - @backstage/plugin-search-react@1.7.12-next.0 + - @backstage/plugin-signals@0.0.7-next.0 + - @backstage/plugin-techdocs-react@1.2.5-next.0 + - @backstage/plugin-user-settings@0.8.7-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-plugin-api@1.9.2 + - @backstage/integration-react@1.1.27 + - @backstage/plugin-auth-react@0.1.3-next.0 + - @backstage/plugin-catalog@1.20.1-next.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-catalog-graph@0.4.6-next.0 + - @backstage/plugin-catalog-import@0.11.1-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.5-next.0 + - @backstage/plugin-devtools@0.1.15-next.0 + - @backstage/plugin-kubernetes@0.11.11-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.12-next.0 + - @backstage/plugin-org@0.6.26-next.0 + - @backstage/plugin-permission-react@0.4.22 + - @backstage/plugin-search@1.4.12-next.0 + - @backstage/plugin-search-common@1.2.11 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.11-next.0 + +## example-app-next@0.0.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.6-next.0 + - @backstage/plugin-notifications@0.2.2-next.0 + - @backstage/plugin-scaffolder-react@1.8.6-next.0 + - @backstage/theme@0.5.6-next.0 + - @backstage/plugin-api-docs@0.11.6-next.0 + - @backstage/cli@0.26.6-next.0 + - @backstage/plugin-scaffolder@1.20.1-next.0 + - @backstage/app-defaults@1.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/frontend-app-api@0.7.1-next.0 + - @backstage/plugin-home@0.7.5-next.0 + - @backstage/plugin-search-react@1.7.12-next.0 + - @backstage/plugin-signals@0.0.7-next.0 + - @backstage/plugin-techdocs-react@1.2.5-next.0 + - @backstage/plugin-user-settings@0.8.7-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/integration-react@1.1.27 + - @backstage/plugin-app-visualizer@0.1.7-next.0 + - @backstage/plugin-auth-react@0.1.3-next.0 + - @backstage/plugin-catalog@1.20.1-next.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-catalog-graph@0.4.6-next.0 + - @backstage/plugin-catalog-import@0.11.1-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.5-next.0 + - @backstage/plugin-kubernetes@0.11.11-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.12-next.0 + - @backstage/plugin-org@0.6.26-next.0 + - @backstage/plugin-permission-react@0.4.22 + - @backstage/plugin-search@1.4.12-next.0 + - @backstage/plugin-search-common@1.2.11 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.11-next.0 + +## app-next-example-plugin@0.0.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.0 + +## example-backend@0.0.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.2.9-next.0 + - @backstage/plugin-devtools-backend@0.3.5-next.0 + - @backstage/plugin-techdocs-backend@1.10.6-next.0 + - @backstage/plugin-catalog-backend@1.23.0-next.0 + - @backstage/plugin-search-backend@1.5.10-next.0 + - @backstage/plugin-proxy-backend@0.5.0-next.0 + - @backstage/plugin-auth-backend@0.22.6-next.0 + - @backstage/plugin-app-backend@0.3.68-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/plugin-signals-backend@0.1.5-next.0 + - @backstage/backend-defaults@0.2.19-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-scaffolder-backend@1.22.8-next.0 + - @backstage/plugin-kubernetes-backend@0.17.2-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.2-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.25-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.25-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.5-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.1.37-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.6-next.0 + - @backstage/plugin-notifications-backend@0.2.2-next.0 + - @backstage/plugin-permission-backend@0.5.43-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.16-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.16-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-permission-common@0.7.13 + +## example-backend-legacy@0.2.99-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-devtools-backend@0.3.5-next.0 + - @backstage/plugin-techdocs-backend@1.10.6-next.0 + - @backstage/plugin-catalog-backend@1.23.0-next.0 + - @backstage/plugin-search-backend@1.5.10-next.0 + - @backstage/plugin-proxy-backend@0.5.0-next.0 + - @backstage/plugin-auth-backend@0.22.6-next.0 + - @backstage/plugin-app-backend@0.3.68-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.4.2-next.0 + - @backstage/plugin-search-backend-module-pg@0.5.28-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/plugin-signals-backend@0.1.5-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/plugin-scaffolder-backend@1.22.8-next.0 + - @backstage/plugin-kubernetes-backend@0.17.2-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.25-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.25-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.0 + - example-app@0.2.98-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.6-next.0 + - @backstage/plugin-events-backend@0.3.6-next.0 + - @backstage/plugin-permission-backend@0.5.43-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.20-next.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.1-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.36-next.0 + - @backstage/plugin-signals-node@0.1.5-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.11.0 + - @backstage/plugin-permission-common@0.7.13 + +## e2e-test@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.16-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/errors@1.2.4 + +## techdocs-cli-embedded-app@0.2.97-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.6-next.0 + - @backstage/theme@0.5.6-next.0 + - @backstage/cli@0.26.6-next.0 + - @backstage/app-defaults@1.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/test-utils@1.5.6-next.0 + - @backstage/plugin-techdocs-react@1.2.5-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-plugin-api@1.9.2 + - @backstage/integration-react@1.1.27 + - @backstage/plugin-catalog@1.20.1-next.0 + +## @internal/plugin-todo-list@1.0.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/core-plugin-api@1.9.2 + +## @internal/plugin-todo-list-backend@1.0.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/errors@1.2.4 diff --git a/docs/releases/v1.28.0-next.1-changelog.md b/docs/releases/v1.28.0-next.1-changelog.md new file mode 100644 index 0000000000..cb5502738d --- /dev/null +++ b/docs/releases/v1.28.0-next.1-changelog.md @@ -0,0 +1,1014 @@ +# Release v1.28.0-next.1 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.28.0-next.1](https://backstage.github.io/upgrade-helper/?to=1.28.0-next.1) + +## @backstage/backend-common@0.23.0-next.1 + +### Minor Changes + +- 02103be: Deprecated and moved over core services to `@backstage/backend-defaults` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/config-loader@1.8.0 + - @backstage/plugin-auth-node@0.4.14-next.1 + +## @backstage/backend-defaults@0.3.0-next.1 + +### Minor Changes + +- 02103be: Deprecated and moved over core services to `@backstage/backend-defaults` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-permission-node@0.7.30-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/config-loader@1.8.0 + - @backstage/plugin-events-node@0.3.5-next.0 + +## @backstage/backend-test-utils@0.4.0-next.1 + +### Minor Changes + +- 805cbe7: Added `TestCaches` that functions just like `TestDatabases` + +### Patch Changes + +- 9e63318: Made it possible to give access restrictions to `mockCredentials.service` +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + - @backstage/plugin-events-node@0.3.5-next.0 + +## @backstage/plugin-catalog-backend-module-ldap@0.6.0-next.1 + +### Minor Changes + +- debcc8c: Migrate LDAP catalog module to the new backend system. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + +## @backstage/plugin-catalog-import@0.12.0-next.1 + +### Minor Changes + +- 4f92394: Migrate from identityApi to fetchApi in frontend plugins. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## @backstage/plugin-kubernetes-backend@0.18.0-next.1 + +### Minor Changes + +- 0177f75: Update kubernetes plugins to use autoscaling/v2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-permission-node@0.7.30-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-kubernetes-common@0.8.0-next.0 + - @backstage/plugin-auth-node@0.4.14-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/plugin-kubernetes-node@0.1.13-next.1 + +## @backstage/plugin-kubernetes-common@0.8.0-next.0 + +### Minor Changes + +- 0177f75: Update kubernetes plugins to use autoscaling/v2 + +## @backstage/plugin-kubernetes-react@0.4.0-next.1 + +### Minor Changes + +- 4f92394: Migrate from identityApi to fetchApi in frontend plugins. +- 0177f75: Update kubernetes plugins to use autoscaling/v2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.8.0-next.0 + +## @backstage/plugin-notifications-backend@0.3.0-next.1 + +### Minor Changes + +- 07a789b: adding filtering of notifications by processors + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-notifications-node@0.2.0-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/plugin-signals-node@0.1.5-next.1 + +## @backstage/plugin-notifications-backend-module-email@0.1.0-next.1 + +### Minor Changes + +- 07a789b: add notification filters + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-notifications-node@0.2.0-next.1 + - @backstage/backend-common@0.23.0-next.1 + +## @backstage/plugin-notifications-node@0.2.0-next.1 + +### Minor Changes + +- 07a789b: add notifications filtering by processors + +### Patch Changes + +- 1354d81: Use `node-fetch` instead of native fetch, as per +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-signals-node@0.1.5-next.1 + +## @backstage/backend-app-api@0.7.6-next.1 + +### Patch Changes + +- 398b82a: Add support for JWKS tokens in ExternalTokenHandler. +- 9e63318: Added an optional `accessRestrictions` to external access service tokens and service principals in general, such that you can limit their access to certain plugins or permissions. +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-permission-node@0.7.30-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/cli-node@0.2.6-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/plugin-auth-node@0.4.14-next.1 + +## @backstage/backend-dynamic-feature-service@0.2.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.1 + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-permission-node@0.7.30-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-backend@1.23.0-next.1 + - @backstage/cli-node@0.2.6-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/plugin-auth-node@0.4.14-next.1 + - @backstage/plugin-events-backend@0.3.6-next.1 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + +## @backstage/backend-plugin-api@0.6.19-next.1 + +### Patch Changes + +- 9e63318: Added an optional `accessRestrictions` to external access service tokens and service principals in general, such that you can limit their access to certain plugins or permissions. +- 0665b7e: Renamed `BackendPluginConfig`, `BackendModuleConfig`, and `ExtensionPointConfig` respectively to `CreateBackendPluginOptions`, `CreateBackendModuleOptions`, and `CreateExtensionPointOptions` to standardize frontend and backend factories signatures. +- Updated dependencies + - @backstage/plugin-auth-node@0.4.14-next.1 + +## @backstage/backend-tasks@0.5.24-next.1 + +### Patch Changes + +- ed473cd: Updated the `TaskScheduleDefinitionConfig` deprecated comment to point to `SchedulerServiceTaskScheduleDefinitionConfig` +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + +## @backstage/cli@0.26.7-next.1 + +### Patch Changes + +- 788eca7: Fix readme for new plugins created using cli +- c00f7ee: Fix issue with `esm` loaded dependencies being different from the `cjs` import for Vite dependencies +- Updated dependencies + - @backstage/cli-node@0.2.6-next.0 + - @backstage/config-loader@1.8.0 + +## @backstage/create-app@0.5.16-next.1 + +### Patch Changes + +- Bumped create-app version. + +## @backstage/repo-tools@0.9.1-next.1 + +### Patch Changes + +- 8721a02: Add `--client-additional-properties` option to `openapi generate` command +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/cli-node@0.2.6-next.0 + - @backstage/config-loader@1.8.0 + +## @techdocs/cli@1.8.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-techdocs-node@1.12.5-next.1 + +## @backstage/plugin-app-backend@0.3.68-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/config-loader@1.8.0 + - @backstage/plugin-auth-node@0.4.14-next.1 + +## @backstage/plugin-auth-backend@0.22.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.2-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.1.11-next.0 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.11-next.1 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.2-next.0 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.2-next.0 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.14-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.16-next.0 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.16-next.0 + - @backstage/plugin-auth-backend-module-google-provider@0.1.16-next.0 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.14-next.0 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.1.16-next.0 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.12-next.0 + - @backstage/plugin-auth-backend-module-oidc-provider@0.1.10-next.1 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.12-next.0 + - @backstage/plugin-auth-node@0.4.14-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-auth-backend@0.22.6-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.2-next.1 + +### Patch Changes + +- 1354d81: Use `node-fetch` instead of native fetch, as per +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + +## @backstage/plugin-auth-backend-module-guest-provider@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-auth-backend@0.22.6-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + +## @backstage/plugin-auth-node@0.4.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + +## @backstage/plugin-catalog@1.20.1-next.1 + +### Patch Changes + +- a2d2649: Export `catalogTranslationRef` under `/alpha` + +- bcec60f: updated the ContextMenu, ActionsPage, OngoingTask and TemplateCard frontend components to support the new scaffolder permissions: + + - `scaffolder.task.create` + - `scaffolder.task.cancel` + - `scaffolder.task.read` + +- Updated dependencies + - @backstage/plugin-scaffolder-common@1.5.3-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## @backstage/plugin-catalog-backend@1.23.0-next.1 + +### Patch Changes + +- d779e3b: Added a regex test to check commit hash. If url is from git commit branch ignore the edit url. +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-permission-node@0.7.30-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.25-next.1 + +## @backstage/plugin-catalog-backend-module-aws@0.3.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-kubernetes-common@0.8.0-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + +## @backstage/plugin-catalog-backend-module-azure@0.1.39-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-kubernetes-common@0.8.0-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + +## @backstage/plugin-catalog-backend-module-github@0.6.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-backend@1.23.0-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + +## @backstage/plugin-catalog-backend-module-github-org@0.1.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-backend-module-github@0.6.2-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.3.17-next.1 + +### Patch Changes + +- 150fc77: Fixed an issue in `GitlabOrgDiscoveryEntityProvider` where a missing `orgEnabled` config key was throwing an error. +- f271164: Fixed an issue in `GitlabDiscoveryEntityProvider` where the fallback branch was taking precedence over the GitLab default branch. +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.0.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.17-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-backend@1.23.0-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.37-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-backend@1.23.0-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.25-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-scaffolder-common@1.5.3-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.4.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + +## @backstage/plugin-devtools-backend@0.3.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-permission-node@0.7.30-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/config-loader@1.8.0 + +## @backstage/plugin-events-backend@0.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-events-node@0.3.5-next.0 + +## @backstage/plugin-events-backend-module-aws-sqs@0.3.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-events-node@0.3.5-next.0 + +## @backstage/plugin-kubernetes@0.11.11-next.1 + +### Patch Changes + +- 4f92394: Migrate from identityApi to fetchApi in frontend plugins. +- Updated dependencies + - @backstage/plugin-kubernetes-react@0.4.0-next.1 + - @backstage/plugin-kubernetes-common@0.8.0-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## @backstage/plugin-kubernetes-cluster@0.0.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-react@0.4.0-next.1 + - @backstage/plugin-kubernetes-common@0.8.0-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## @backstage/plugin-kubernetes-node@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-kubernetes-common@0.8.0-next.0 + +## @backstage/plugin-notifications@0.2.2-next.1 + +### Patch Changes + +- 6d196b4: Fixes performance issue with Notifications title counter. + +## @backstage/plugin-permission-backend@0.5.43-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-permission-node@0.7.30-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + +## @backstage/plugin-permission-node@0.7.30-next.1 + +### Patch Changes + +- 9e63318: Ensure that service token access restrictions, when present, are taken into account +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + +## @backstage/plugin-proxy-backend@0.5.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + +## @backstage/plugin-scaffolder@1.20.2-next.1 + +### Patch Changes + +- 75dcd7e: Fixing bug in `formData` type as it should be `optional` as it's possibly undefined + +- bcec60f: updated the ContextMenu, ActionsPage, OngoingTask and TemplateCard frontend components to support the new scaffolder permissions: + + - `scaffolder.task.create` + - `scaffolder.task.cancel` + - `scaffolder.task.read` + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.8.7-next.1 + - @backstage/plugin-scaffolder-common@1.5.3-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## @backstage/plugin-scaffolder-backend@1.22.8-next.1 + +### Patch Changes + +- bcec60f: added the following new permissions to the scaffolder backend endpoints: + + - `scaffolder.task.create` + - `scaffolder.task.cancel` + - `scaffolder.task.read` + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-permission-node@0.7.30-next.1 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.9-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.1-next.1 + - @backstage/plugin-scaffolder-common@1.5.3-next.0 + - @backstage/plugin-auth-node@0.4.14-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.9-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.9-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.9-next.0 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.11-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.2.9-next.1 + - @backstage/plugin-scaffolder-node@0.4.5-next.1 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-scaffolder-node@0.4.5-next.1 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.43-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-scaffolder-node@0.4.5-next.1 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.1.9-next.1 + +### Patch Changes + +- 1354d81: Use `node-fetch` instead of native fetch, as per +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-scaffolder-node@0.4.5-next.1 + +## @backstage/plugin-scaffolder-backend-module-github@0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-scaffolder-node@0.4.5-next.1 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.4.1-next.1 + +### Patch Changes + +- 829e0ec: Add new `gitlab:pipeline:trigger` action to trigger GitLab pipelines. +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-scaffolder-node@0.4.5-next.1 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.0.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-notifications-node@0.2.0-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-scaffolder-node@0.4.5-next.1 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-scaffolder-node@0.4.5-next.1 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.27-next.1 + +### Patch Changes + +- 1354d81: Use `node-fetch` instead of native fetch, as per +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-scaffolder-node@0.4.5-next.1 + +## @backstage/plugin-scaffolder-common@1.5.3-next.0 + +### Patch Changes + +- bcec60f: added the following new permissions to the scaffolder backend endpoints: + + - `scaffolder.task.create` + - `scaffolder.task.cancel` + - `scaffolder.task.read` + +## @backstage/plugin-scaffolder-node@0.4.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-scaffolder-common@1.5.3-next.0 + +## @backstage/plugin-scaffolder-node-test-utils@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.4.0-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-scaffolder-node@0.4.5-next.1 + +## @backstage/plugin-scaffolder-react@1.8.7-next.1 + +### Patch Changes + +- 75dcd7e: Fixing bug in `formData` type as it should be `optional` as it's possibly undefined +- 928cfa0: Fixed a typo ' + +## @backstage/plugin-search@1.4.12-next.1 + +### Patch Changes + +- 4f92394: Migrate from identityApi to fetchApi in frontend plugins. +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## @backstage/plugin-search-backend@1.5.10-next.1 + +### Patch Changes + +- 34dc47d: Move @backstage/repo-tools to devDependencies +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-permission-node@0.7.30-next.1 + - @backstage/backend-defaults@0.3.0-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + +## @backstage/plugin-search-backend-module-catalog@0.1.25-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + +## @backstage/plugin-search-backend-module-elasticsearch@1.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + +## @backstage/plugin-search-backend-module-explore@0.1.25-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + +## @backstage/plugin-search-backend-module-pg@0.5.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + +## @backstage/plugin-search-backend-module-techdocs@0.1.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + - @backstage/plugin-techdocs-node@1.12.5-next.1 + +## @backstage/plugin-search-backend-node@1.2.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + +## @backstage/plugin-signals-backend@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/plugin-signals-node@0.1.5-next.1 + +## @backstage/plugin-signals-node@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + - @backstage/plugin-events-node@0.3.5-next.0 + +## @backstage/plugin-techdocs-backend@1.10.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.1 + - @backstage/plugin-techdocs-node@1.12.5-next.1 + +## @backstage/plugin-techdocs-node@1.12.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + +## @backstage/plugin-user-settings-backend@0.2.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + +## example-app@0.2.98-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.26.7-next.1 + - @backstage/plugin-catalog-import@0.12.0-next.1 + - @backstage/plugin-kubernetes@0.11.11-next.1 + - @backstage/plugin-search@1.4.12-next.1 + - @backstage/plugin-notifications@0.2.2-next.1 + - @backstage/plugin-scaffolder-react@1.8.7-next.1 + - @backstage/plugin-scaffolder@1.20.2-next.1 + - @backstage/plugin-catalog@1.20.1-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.12-next.1 + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## example-app-next@0.0.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.26.7-next.1 + - @backstage/plugin-catalog-import@0.12.0-next.1 + - @backstage/plugin-kubernetes@0.11.11-next.1 + - @backstage/plugin-search@1.4.12-next.1 + - @backstage/plugin-notifications@0.2.2-next.1 + - @backstage/plugin-scaffolder-react@1.8.7-next.1 + - @backstage/plugin-scaffolder@1.20.2-next.1 + - @backstage/plugin-catalog@1.20.1-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.12-next.1 + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## example-backend@0.0.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-permission-node@0.7.30-next.1 + - @backstage/plugin-search-backend@1.5.10-next.1 + - @backstage/backend-defaults@0.3.0-next.1 + - @backstage/plugin-kubernetes-backend@0.18.0-next.1 + - @backstage/plugin-catalog-backend@1.23.0-next.1 + - @backstage/plugin-scaffolder-backend@1.22.8-next.1 + - @backstage/plugin-notifications-backend@0.3.0-next.1 + - @backstage/plugin-app-backend@0.3.68-next.1 + - @backstage/plugin-auth-backend@0.22.6-next.1 + - @backstage/plugin-auth-backend-module-github-provider@0.1.16-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.5-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + - @backstage/plugin-catalog-backend-module-openapi@0.1.37-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.1 + - @backstage/plugin-devtools-backend@0.3.5-next.1 + - @backstage/plugin-permission-backend@0.5.43-next.1 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.16-next.0 + - @backstage/plugin-proxy-backend@0.5.0-next.1 + - @backstage/plugin-scaffolder-backend-module-github@0.2.9-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.25-next.1 + - @backstage/plugin-search-backend-module-explore@0.1.25-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + - @backstage/plugin-signals-backend@0.1.5-next.1 + - @backstage/plugin-techdocs-backend@1.10.6-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.6-next.1 + +## example-backend-legacy@0.2.99-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/plugin-permission-node@0.7.30-next.1 + - @backstage/plugin-search-backend@1.5.10-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.1-next.1 + - @backstage/plugin-kubernetes-backend@0.18.0-next.1 + - @backstage/plugin-catalog-backend@1.23.0-next.1 + - @backstage/plugin-scaffolder-backend@1.22.8-next.1 + - @backstage/plugin-app-backend@0.3.68-next.1 + - @backstage/plugin-auth-backend@0.22.6-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/plugin-devtools-backend@0.3.5-next.1 + - @backstage/plugin-events-backend@0.3.6-next.1 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/plugin-permission-backend@0.5.43-next.1 + - @backstage/plugin-proxy-backend@0.5.0-next.1 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.20-next.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.36-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.25-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.4.2-next.1 + - @backstage/plugin-search-backend-module-explore@0.1.25-next.1 + - @backstage/plugin-search-backend-module-pg@0.5.28-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + - @backstage/plugin-signals-backend@0.1.5-next.1 + - @backstage/plugin-techdocs-backend@1.10.6-next.1 + - example-app@0.2.98-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.6-next.1 + - @backstage/plugin-signals-node@0.1.5-next.1 + +## @internal/plugin-todo-list-backend@1.0.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 diff --git a/docs/releases/v1.28.0-next.2-changelog.md b/docs/releases/v1.28.0-next.2-changelog.md new file mode 100644 index 0000000000..88a2e6010b --- /dev/null +++ b/docs/releases/v1.28.0-next.2-changelog.md @@ -0,0 +1,2127 @@ +# Release v1.28.0-next.2 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.28.0-next.2](https://backstage.github.io/upgrade-helper/?to=1.28.0-next.2) + +## @backstage/backend-common@0.23.0-next.2 + +### Minor Changes + +- 395b973: Implemented `readTree` for Harness provider to support TechDocs functionality + +### Patch Changes + +- 0634fdc: Deprecated `dropDatabase` +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/backend-app-api@0.7.6-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/config-loader@1.8.0 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + +## @backstage/integration@1.12.0-next.0 + +### Minor Changes + +- 395b973: Implemented `readTree` for Harness provider to support TechDocs functionality + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog@1.21.0-next.2 + +### Minor Changes + +- 863a800: Added the following default targets for external routes: + + - `createComponent` binds to the Scaffolder page. + - `viewTechDoc` binds to the TechDocs entity documentation page. + - `createFromTemplate` binds to the Scaffolder selected template page. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-search-react@1.7.12-next.1 + - @backstage/plugin-permission-react@0.4.23-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-scaffolder-common@1.5.3-next.0 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-scaffolder@1.21.0-next.2 + +### Minor Changes + +- 60085dd: Added the following default targets for external routes: + + - `registerComponent` binds to the catalog import page. + - `viewTechDoc` binds to the TechDocs entity documentation page. + +### Patch Changes + +- cbebad1: Internal updates to allow reusing Backstage's `fetchApi` implementation for event source requests. This allows you to for example, override the `Authorization` header. +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/integration@1.12.0-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-scaffolder-react@1.8.7-next.2 + - @backstage/plugin-permission-react@0.4.23-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-scaffolder-common@1.5.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-github@0.3.0-next.2 + +### Minor Changes + +- 403394a: Allow empty author info in createPullRequest action for Github + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/app-defaults@1.5.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/plugin-permission-react@0.4.23-next.0 + - @backstage/theme@0.5.6-next.0 + +## @backstage/backend-app-api@0.7.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-node@0.2.6-next.1 + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-permission-node@0.7.30-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/config-loader@1.8.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-defaults@0.3.0-next.2 + +### Patch Changes + +- 0634fdc: Deprecated `dropDatabase` +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-permission-node@0.7.30-next.2 + - @backstage/backend-app-api@0.7.6-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/config-loader@1.8.0 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-dynamic-feature-service@0.2.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-node@0.2.6-next.1 + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-permission-node@0.7.30-next.2 + - @backstage/backend-app-api@0.7.6-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-app-node@0.1.19-next.1 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-catalog-backend@1.23.0-next.2 + - @backstage/plugin-events-backend@0.3.6-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/config-loader@1.8.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/backend-openapi-utils@0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/errors@1.2.4 + +## @backstage/backend-plugin-api@0.6.19-next.2 + +### Patch Changes + +- 3aa3fc7: Marked the `TokenManagerService` and `IdentityService` types as deprecated +- Updated dependencies + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/backend-tasks@0.5.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-test-utils@0.4.0-next.2 + +### Patch Changes + +- 0634fdc: Refactored `TestDatabases` to no longer depend on `backend-common` +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-app-api@0.7.6-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/cli@0.26.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-node@0.2.6-next.1 + - @backstage/integration@1.12.0-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## @backstage/cli-node@0.2.6-next.1 + +### Patch Changes + +- a1ae9cc: Updated doc link. +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/core-app-api@1.12.6-next.0 + +### Patch Changes + +- 35fbe09: Added support for configuration of route bindings through static configuration, and default targets for external route refs. + + In addition to configuring route bindings through code, it is now also possible to configure route bindings under the `app.routes.bindings` key, for example: + + ```yaml + app: + routes: + bindings: + catalog.createComponent: catalog-import.importPage + ``` + + Each key in the route binding object is of the form `.`, where the route name is key used in the `externalRoutes` object passed to `createPlugin`. The value is of the same form, but with the name taken from the plugin `routes` option instead. + + The equivalent of the above configuration in code is the following: + + ```ts + const app = createApp({ + // ... + bindRoutes({ bind }) { + bind(catalogPlugin.externalRoutes, { + createComponent: catalogImportPlugin.routes.importPage, + }); + }, + }); + ``` + +- Updated dependencies + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/core-compat-api@0.2.6-next.1 + +### Patch Changes + +- 35fbe09: Add support for forwarding default target from legacy external route refs. +- Updated dependencies + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/core-components@0.14.8-next.1 + +### Patch Changes + +- a0b46f6: Having tooltip inherit font size for consistency in catalog table columns +- eae0e4d: Fixed an issue causing `SidebarSubmenu` text to not follow the theme color +- 3e175c8: Removed max width from `Select` component. +- 57d7582: Fixed a bug in `SupportButton` where the title was rendered with the characteristics of a button. +- Updated dependencies + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6-next.0 + - @backstage/version-bridge@1.0.8 + +## @backstage/core-plugin-api@1.9.3-next.0 + +### Patch Changes + +- 35fbe09: Added a new `defaultTarget` option to `createExternalRouteRef`. I lets you specify a default target of the route by name, for example `'catalog.catalogIndex'`, which will be used if the target route is present in the app and there is no explicit route binding. +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/create-app@0.5.16-next.2 + +### Patch Changes + +- 78363f6: Bumped TypeScript to version `5.4`. +- 81507c8: Updated `node-gyp` to v10 +- Updated dependencies + - @backstage/cli-common@0.1.13 + +## @backstage/dev-utils@1.0.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/app-defaults@1.5.6-next.1 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/theme@0.5.6-next.0 + +## @backstage/frontend-app-api@0.7.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/frontend-plugin-api@0.6.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/frontend-test-utils@0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-app-api@0.7.1-next.1 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/test-utils@1.5.6-next.1 + - @backstage/types@1.1.1 + +## @backstage/integration-react@1.1.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/integration@1.12.0-next.0 + - @backstage/config@1.2.0 + +## @backstage/repo-tools@0.9.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-node@0.2.6-next.1 + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/config-loader@1.8.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.13 + - @backstage/errors@1.2.4 + +## @techdocs/cli@1.8.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.5-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + +## @backstage/test-utils@1.5.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/plugin-permission-react@0.4.23-next.0 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-api-docs@0.11.6-next.1 + +### Patch Changes + +- 7f84039: The `registerComponent` external route will now by default bind to the catalog import page if it is available. +- 9cdc651: Make sure that the toggle button state is properly reflected in API cards +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/plugin-catalog@1.21.0-next.2 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-permission-react@0.4.23-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-catalog-common@1.0.23 + +## @backstage/plugin-app-backend@0.3.68-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-app-node@0.1.19-next.1 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/config-loader@1.8.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-app-node@0.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/config-loader@1.8.0 + +## @backstage/plugin-app-visualizer@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + +## @backstage/plugin-auth-backend@0.22.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-auth-backend-module-oidc-provider@0.1.10-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.1.11-next.1 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.11-next.2 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.2-next.1 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.2-next.1 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.2-next.2 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.14-next.1 + - @backstage/plugin-auth-backend-module-github-provider@0.1.16-next.1 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.16-next.1 + - @backstage/plugin-auth-backend-module-google-provider@0.1.16-next.1 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.14-next.1 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.1.16-next.1 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.12-next.1 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.12-next.1 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-auth-backend@0.22.6-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend-module-github-provider@0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + +## @backstage/plugin-auth-backend-module-google-provider@0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + +## @backstage/plugin-auth-backend-module-guest-provider@0.1.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.1.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.1.10-next.2 + +### Patch Changes + +- 4f21993: if oidc server do not provide revocation_endpoint,we should not call revoke function +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-auth-backend@0.22.6-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + +## @backstage/plugin-auth-backend-module-okta-provider@0.0.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-auth-node@0.4.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-react@0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-bitbucket-cloud-common@0.2.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.12.0-next.0 + +## @backstage/plugin-catalog-backend@1.23.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-permission-node@0.7.30-next.2 + - @backstage/backend-openapi-utils@0.1.12-next.1 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.25-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-catalog-backend-module-aws@0.3.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-kubernetes-common@0.8.0-next.0 + +## @backstage/plugin-catalog-backend-module-azure@0.1.39-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.23 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-openapi-utils@0.1.12-next.1 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.2.6-next.2 + +### Patch Changes + +- b51e823: Remove debug console logging statement +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/plugin-bitbucket-cloud-common@0.2.20-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.23 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.33-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-kubernetes-common@0.8.0-next.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.36-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-github@0.6.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-backend@1.23.0-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.23 + +## @backstage/plugin-catalog-backend-module-github-org@0.1.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-backend-module-github@0.6.2-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.3.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.23 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.0.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.18-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/plugin-events-node@0.3.5-next.1 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-backend@1.23.0-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-catalog-backend-module-ldap@0.6.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.23 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.23 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.37-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-catalog-backend@1.23.0-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.23 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.25-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-scaffolder-common@1.5.3-next.0 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.4.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.1 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-catalog-graph@0.4.6-next.1 + +### Patch Changes + +- cd6aeea: The `catalogEntity` external route will now by default bind to the catalog entity page if it is available. +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-import@0.12.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/integration@1.12.0-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-common@1.0.23 + +## @backstage/plugin-catalog-node@1.12.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-permission-node@0.7.30-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-catalog-react@1.12.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-permission-react@0.4.23-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-config-schema@0.1.56-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-devtools@0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/plugin-permission-react@0.4.23-next.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-devtools-common@0.1.9 + +## @backstage/plugin-devtools-backend@0.3.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-permission-node@0.7.30-next.2 + - @backstage/config-loader@1.8.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-devtools-common@0.1.9 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-events-backend@0.3.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-module-aws-sqs@0.3.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-events-backend-module-azure@0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + +## @backstage/plugin-events-backend-module-gerrit@0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + +## @backstage/plugin-events-backend-module-github@0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-module-gitlab@0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-test-utils@0.1.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.5-next.1 + +## @backstage/plugin-events-node@0.3.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + +## @backstage/plugin-home@0.7.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-home-react@0.1.14-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6-next.0 + +## @backstage/plugin-home-react@0.1.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + +## @backstage/plugin-kubernetes@0.11.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-kubernetes-react@0.4.0-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-kubernetes-common@0.8.0-next.0 + +## @backstage/plugin-kubernetes-backend@0.18.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-permission-node@0.7.30-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/plugin-kubernetes-node@0.1.13-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.8.0-next.0 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-kubernetes-cluster@0.0.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-kubernetes-react@0.4.0-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-kubernetes-common@0.8.0-next.0 + +## @backstage/plugin-kubernetes-node@0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.8.0-next.0 + +## @backstage/plugin-kubernetes-react@0.4.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.8.0-next.0 + +## @backstage/plugin-notifications@0.2.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/plugin-signals-react@0.0.4-next.0 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.3 + +## @backstage/plugin-notifications-backend@0.3.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/plugin-notifications-node@0.2.0-next.2 + - @backstage/plugin-signals-node@0.1.5-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-notifications-common@0.0.3 + +## @backstage/plugin-notifications-backend-module-email@0.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-notifications-node@0.2.0-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.3 + +## @backstage/plugin-notifications-node@0.2.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-signals-node@0.1.5-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-notifications-common@0.0.3 + +## @backstage/plugin-org@0.6.26-next.1 + +### Patch Changes + +- d8e2f53: The `catalogIndex` external route is now optional and will by default bind to the catalog index page if it is available. +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-catalog-common@1.0.23 + +## @backstage/plugin-org-react@0.1.25-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-permission-backend@0.5.43-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-permission-node@0.7.30-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-permission-node@0.7.30-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-permission-node@0.7.30-next.2 + +### Patch Changes + +- c7b0dd1: Import `tokenManager` definition from `@backstage/backend-plugin-api` +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-permission-react@0.4.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-proxy-backend@0.5.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend@1.22.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.1-next.2 + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-permission-node@0.7.30-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.3.0-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.11-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.9-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.9-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.9-next.1 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.11-next.1 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.9-next.2 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-scaffolder-common@1.5.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-azure@0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.9-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.9-next.1 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.43-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.4.1-next.2 + +### Patch Changes + +- cf96041: Added `gitlab:issue:edit` action to edit existing GitLab issues +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.0.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-notifications-node@0.2.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/plugin-notifications-common@0.0.3 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.36-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.3.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/plugin-scaffolder-node-test-utils@0.1.5-next.2 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-node@0.4.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-common@1.5.3-next.0 + +## @backstage/plugin-scaffolder-node-test-utils@0.1.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.4.0-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-react@1.8.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-permission-react@0.4.23-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/theme@0.5.6-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-scaffolder-common@1.5.3-next.0 + +## @backstage/plugin-search@1.4.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-search-react@1.7.12-next.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-backend@1.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-defaults@0.3.0-next.2 + - @backstage/plugin-permission-node@0.7.30-next.2 + - @backstage/backend-openapi-utils@0.1.12-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-backend-module-catalog@0.1.25-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-backend-module-elasticsearch@1.4.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-backend-module-explore@0.1.25-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/config@1.2.0 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-backend-module-pg@0.5.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-app-api@0.7.6-next.2 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/config@1.2.0 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.1.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/config@1.2.0 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-backend-module-techdocs@0.1.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.5-next.2 + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-backend-node@1.2.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-search-react@1.7.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/theme@0.5.6-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-signals@0.0.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/plugin-signals-react@0.0.4-next.0 + - @backstage/theme@0.5.6-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-signals-backend@0.1.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/plugin-signals-node@0.1.5-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-signals-node@0.1.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-signals-react@0.0.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-techdocs@1.10.6-next.1 + +### Patch Changes + +- cbebad1: Internal updates to allow reusing Backstage's `fetchApi` implementation for event source requests. This allows you to for example, override the `Authorization` header. +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/integration@1.12.0-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-auth-react@0.1.3-next.1 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-search-react@1.7.12-next.1 + - @backstage/plugin-techdocs-react@1.2.5-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6-next.0 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/plugin-catalog@1.21.0-next.2 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-search-react@1.7.12-next.1 + - @backstage/plugin-techdocs-react@1.2.5-next.1 + - @backstage/test-utils@1.5.6-next.1 + +## @backstage/plugin-techdocs-backend@1.10.6-next.2 + +### Patch Changes + +- e64bfb2: Allow defining custom build log transport for techdocs builder +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.5-next.2 + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/integration@1.12.0-next.0 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-techdocs-react@1.2.5-next.1 + +## @backstage/plugin-techdocs-node@1.12.5-next.2 + +### Patch Changes + +- e64bfb2: Allow defining custom build log transport for techdocs builder +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-common@1.2.11 + +## @backstage/plugin-techdocs-react@1.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-user-settings@0.8.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-user-settings-backend@0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## example-app@0.2.98-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/plugin-api-docs@0.11.6-next.1 + - @backstage/plugin-scaffolder@1.21.0-next.2 + - @backstage/plugin-techdocs@1.10.6-next.1 + - @backstage/plugin-org@0.6.26-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/plugin-catalog-graph@0.4.6-next.1 + - @backstage/plugin-catalog@1.21.0-next.2 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/cli@0.26.7-next.2 + - @backstage/app-defaults@1.5.6-next.1 + - @backstage/frontend-app-api@0.7.1-next.1 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-auth-react@0.1.3-next.1 + - @backstage/plugin-catalog-import@0.12.0-next.2 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.5-next.1 + - @backstage/plugin-devtools@0.1.15-next.1 + - @backstage/plugin-home@0.7.5-next.1 + - @backstage/plugin-kubernetes@0.11.11-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.12-next.2 + - @backstage/plugin-notifications@0.2.2-next.2 + - @backstage/plugin-scaffolder-react@1.8.7-next.2 + - @backstage/plugin-search@1.4.12-next.2 + - @backstage/plugin-search-react@1.7.12-next.1 + - @backstage/plugin-signals@0.0.7-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.11-next.1 + - @backstage/plugin-techdocs-react@1.2.5-next.1 + - @backstage/plugin-user-settings@0.8.7-next.1 + - @backstage/plugin-permission-react@0.4.23-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6-next.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-search-common@1.2.11 + +## example-app-next@0.0.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/plugin-api-docs@0.11.6-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/plugin-scaffolder@1.21.0-next.2 + - @backstage/plugin-techdocs@1.10.6-next.1 + - @backstage/plugin-org@0.6.26-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/plugin-catalog-graph@0.4.6-next.1 + - @backstage/plugin-catalog@1.21.0-next.2 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/cli@0.26.7-next.2 + - @backstage/app-defaults@1.5.6-next.1 + - @backstage/frontend-app-api@0.7.1-next.1 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-app-visualizer@0.1.7-next.1 + - @backstage/plugin-auth-react@0.1.3-next.1 + - @backstage/plugin-catalog-import@0.12.0-next.2 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.5-next.1 + - @backstage/plugin-home@0.7.5-next.1 + - @backstage/plugin-kubernetes@0.11.11-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.12-next.2 + - @backstage/plugin-notifications@0.2.2-next.2 + - @backstage/plugin-scaffolder-react@1.8.7-next.2 + - @backstage/plugin-search@1.4.12-next.2 + - @backstage/plugin-search-react@1.7.12-next.1 + - @backstage/plugin-signals@0.0.7-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.11-next.1 + - @backstage/plugin-techdocs-react@1.2.5-next.1 + - @backstage/plugin-user-settings@0.8.7-next.1 + - @backstage/plugin-permission-react@0.4.23-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6-next.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-search-common@1.2.11 + +## app-next-example-plugin@0.0.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/frontend-plugin-api@0.6.6-next.1 + +## example-backend@0.0.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-backend@1.10.6-next.2 + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-defaults@0.3.0-next.2 + - @backstage/plugin-permission-node@0.7.30-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.3.0-next.2 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.2 + - @backstage/plugin-scaffolder-backend@1.22.8-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-app-backend@0.3.68-next.2 + - @backstage/plugin-auth-backend@0.22.6-next.2 + - @backstage/plugin-auth-backend-module-github-provider@0.1.16-next.1 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.5-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-catalog-backend@1.23.0-next.2 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.2-next.1 + - @backstage/plugin-catalog-backend-module-openapi@0.1.37-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.6-next.2 + - @backstage/plugin-devtools-backend@0.3.5-next.2 + - @backstage/plugin-kubernetes-backend@0.18.0-next.2 + - @backstage/plugin-notifications-backend@0.3.0-next.2 + - @backstage/plugin-permission-backend@0.5.43-next.2 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.16-next.1 + - @backstage/plugin-proxy-backend@0.5.0-next.2 + - @backstage/plugin-search-backend@1.5.10-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.25-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.25-next.2 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/plugin-signals-backend@0.1.5-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-permission-common@0.7.13 + +## example-backend-legacy@0.2.99-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-backend@1.10.6-next.2 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.1-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-permission-node@0.7.30-next.2 + - example-app@0.2.98-next.2 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.2 + - @backstage/plugin-scaffolder-backend@1.22.8-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-app-backend@0.3.68-next.2 + - @backstage/plugin-auth-backend@0.22.6-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-catalog-backend@1.23.0-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.6-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/plugin-devtools-backend@0.3.5-next.2 + - @backstage/plugin-events-backend@0.3.6-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/plugin-kubernetes-backend@0.18.0-next.2 + - @backstage/plugin-permission-backend@0.5.43-next.2 + - @backstage/plugin-proxy-backend@0.5.0-next.2 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.20-next.2 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.36-next.2 + - @backstage/plugin-search-backend@1.5.10-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.25-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.4.2-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.25-next.2 + - @backstage/plugin-search-backend-module-pg@0.5.28-next.2 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/plugin-signals-backend@0.1.5-next.2 + - @backstage/plugin-signals-node@0.1.5-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-permission-common@0.7.13 + +## e2e-test@0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.16-next.2 + - @backstage/cli-common@0.1.13 + - @backstage/errors@1.2.4 + +## techdocs-cli-embedded-app@0.2.97-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/plugin-techdocs@1.10.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/plugin-catalog@1.21.0-next.2 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/cli@0.26.7-next.2 + - @backstage/app-defaults@1.5.6-next.1 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-techdocs-react@1.2.5-next.1 + - @backstage/test-utils@1.5.6-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6-next.0 + +## @internal/plugin-todo-list@1.0.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + +## @internal/plugin-todo-list-backend@1.0.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/errors@1.2.4 diff --git a/docs/local-dev/cli-overview.md b/docs/tooling/cli/01-overview.md similarity index 82% rename from docs/local-dev/cli-overview.md rename to docs/tooling/cli/01-overview.md index 4ab8bb21ac..78c4b2755f 100644 --- a/docs/local-dev/cli-overview.md +++ b/docs/tooling/cli/01-overview.md @@ -1,5 +1,5 @@ --- -id: cli-overview +id: overview title: Overview description: Overview of the Backstage CLI --- @@ -7,12 +7,12 @@ description: Overview of the Backstage CLI ## Introduction A goal of Backstage is to provide a delightful developer experience in and -around the project. Creating new [apps](../references/glossary.md#app) and -[plugins](../references/glossary.md#plugin) should be simple, iteration +around the project. Creating new [apps](../../references/glossary.md#app) and +[plugins](../../references/glossary.md#plugin) should be simple, iteration speed should be fast, and the overhead of maintaining custom tooling should be minimal. As a part of accomplishing this goal, Backstage provides its own build system and tooling, delivered primarily through the -[`@backstage/cli`](https://www.npmjs.com/package/@backstage/cli) [package](../references/glossary.md#package). When +[`@backstage/cli`](https://www.npmjs.com/package/@backstage/cli) [package](../../references/glossary.md#package). When creating an app using [`@backstage/create-app`](https://www.npmjs.com/package/@backstage/create-app), you receive a project that's already prepared with a typical setup and package @@ -23,8 +23,8 @@ Under the hood the CLI uses [Webpack](https://webpack.js.org/) for bundling, [Jest](https://jestjs.io/) for testing, and [eslint](https://eslint.org/) for linting. It also includes tooling for working within Backstage apps, for example for keeping the app up to date and verifying static configuration. For a more -in-depth look into the tooling, see the [build system](./cli-build-system.md) -page, and for a list of commands, see the [commands](./cli-commands.md) page. +in-depth look into the tooling, see the [build system](./02-build-system.md) +page, and for a list of commands, see the [commands](./03-commands.md) page. While the Backstage tooling is opinionated in how it works, it is also possible to use your own tooling either partially or fully. For example, the CLI provides diff --git a/docs/local-dev/cli-build-system.md b/docs/tooling/cli/02-build-system.md similarity index 99% rename from docs/local-dev/cli-build-system.md rename to docs/tooling/cli/02-build-system.md index ddcfa44cf5..bfdb21f45f 100644 --- a/docs/local-dev/cli-build-system.md +++ b/docs/tooling/cli/02-build-system.md @@ -1,5 +1,5 @@ --- -id: cli-build-system +id: build-system title: Build System description: A deep dive into the Backstage build system --- @@ -74,7 +74,7 @@ implemented in a typical Backstage app. ## Package Roles -> Package roles were introduced in March 2022. To migrate existing projects, see the [migration guide](../tutorials/package-role-migration.md). +> Package roles were introduced in March 2022. To migrate existing projects, see the [migration guide](../../tutorials/package-role-migration.md). The Backstage build system uses the concept of package roles in order to help keep configuration lean, provide utility and tooling, and enable optimizations. A package @@ -258,7 +258,7 @@ When building CommonJS or ESM output, the build commands will always use `src/index.ts` as the entrypoint. All non-relative modules imports are considered external, meaning the Rollup build will only compile the source code of the package itself. All import statements of external dependencies, even within the same -[monorepo](../references/glossary.md#monorepo), will stay intact. +[monorepo](../../references/glossary.md#monorepo), will stay intact. The build of the type definitions works quite differently. The entrypoint of the type definition build is the relative location of the package within the @@ -308,7 +308,7 @@ support for them instead. ### Frontend Production The frontend production bundling creates your typical web content -[bundle](../references/glossary.md#bundle), all contained within a single +[bundle](../../references/glossary.md#bundle), all contained within a single folder, ready for static serving. It is used when building packages with the `'frontend'` role, and unlike the development bundling there is no way to build a production bundle of an individual plugin. diff --git a/docs/local-dev/cli-commands.md b/docs/tooling/cli/03-commands.md similarity index 95% rename from docs/local-dev/cli-commands.md rename to docs/tooling/cli/03-commands.md index 173d616e61..234d0d04b7 100644 --- a/docs/local-dev/cli-commands.md +++ b/docs/tooling/cli/03-commands.md @@ -1,5 +1,5 @@ --- -id: cli-commands +id: commands title: Commands description: Descriptions of all commands available in the CLI. --- @@ -102,7 +102,7 @@ Options: ## package start -Starts the package for local development. See the frontend and backend development parts in the build system [bundling](./cli-build-system.md#bundling) section for more details. +Starts the package for local development. See the frontend and backend development parts in the build system [bundling](./02-build-system.md#bundling) section for more details. ```text Usage: backstage-cli package start [options] @@ -119,7 +119,7 @@ Options: ## package build -Build an individual package based on its role. See the build system [building](./cli-build-system.md#building) and [bundling](./cli-build-system.md#bundling) sections for more details. +Build an individual package based on its role. See the build system [building](./02-build-system.md#building) and [bundling](./02-build-system.md#bundling) sections for more details. ```text Usage: backstage-cli package build [options] @@ -139,7 +139,7 @@ Options: Lint a package. In addition to the default `eslint` behavior, this command will include TypeScript files, treat warnings as errors, and default to linting the entire directory if no specific files are listed. For more information, see the -build system [linting](./cli-build-system.md#linting) section. +build system [linting](./02-build-system.md#linting) section. ```text Usage: backstage-cli package lint [options] @@ -165,7 +165,7 @@ a yarn workspaces monorepo by automatically creating one grouped configuration that includes all packages that have `backstage-cli test` in their package `test` script. -For more information about configuration overrides and editor support, see the [Jest Configuration section](./cli-build-system.md#jest-configuration) in the build system documentation. +For more information about configuration overrides and editor support, see the [Jest Configuration section](./02-build-system.md#jest-configuration) in the build system documentation. ```text Usage: backstage-cli package test [options] @@ -190,7 +190,7 @@ Delete cache directories This command should be added as `scripts.prepack` in all packages. It enables packaging- and publish-time overrides for fields inside `packages.json`. -For more details, see the build system [publishing](./cli-build-system.md#publishing) section. +For more details, see the build system [publishing](./02-build-system.md#publishing) section. ```text Usage: backstage-cli package prepack [options] @@ -369,8 +369,8 @@ Usage: backstage-cli build-workspace [options] ## create-github-app Creates a GitHub App in your GitHub organization. This is an alternative to -token-based [GitHub integration](../integrations/github/locations.md). See -[GitHub Apps for Backstage Authentication](../integrations/github/github-apps.md). +token-based [GitHub integration](../../integrations/github/locations.md). See +[GitHub Apps for Backstage Authentication](../../integrations/github/github-apps.md). Launches a browser to create the App through GitHub and saves the result as a YAML file that can be referenced in the GitHub integration configuration. diff --git a/docs/local-dev/debugging.md b/docs/tooling/local-dev/debugging.md similarity index 100% rename from docs/local-dev/debugging.md rename to docs/tooling/local-dev/debugging.md diff --git a/docs/local-dev/linking-local-packages.md b/docs/tooling/local-dev/linking-local-packages.md similarity index 100% rename from docs/local-dev/linking-local-packages.md rename to docs/tooling/local-dev/linking-local-packages.md diff --git a/docs/tutorials/package-role-migration.md b/docs/tutorials/package-role-migration.md index db84defd91..dcd8a66ee6 100644 --- a/docs/tutorials/package-role-migration.md +++ b/docs/tutorials/package-role-migration.md @@ -10,7 +10,7 @@ information about the change can be found in the [original RFC](https://github.c Package roles are implemented through a well-known `"backstage"."role"` field in the `package.json` of each package. There are a handful of roles defined so far, and it -is not possible to use values outside the [set of predefined roles](../local-dev/cli-build-system.md#package-roles). +is not possible to use values outside the [set of predefined roles](../tooling/cli/02-build-system.md#package-roles). With roles in place in all packages, the Backstage CLI is able to automatically determine how to handle each package. For example, the different build commands @@ -55,7 +55,7 @@ yarn backstage-cli migrate package-roles The automatic detection is not perfect, so it is recommended to manually review the roles that were assigned to each package. -You can use the [package role definitions](../local-dev/cli-build-system.md#package-roles) as a reference. +You can use the [package role definitions](../tooling/cli/02-build-system.md#package-roles) as a reference. ### Step 2 - Migrate package scripts @@ -85,7 +85,7 @@ If you in the end do not want to use this exact script setup, it is still recomm ### Step 3 - Migrate package ESLint configurations -An area that has been simplified as part of the move to package roles is the ESLint configuration. Rather than having each package select which configuration they want (and getting it wrong), they now use a shared configuration factory that utilizes the package role. You can read more about the new configuration setup in the [build system documentation](../local-dev/cli-build-system.md#linting). +An area that has been simplified as part of the move to package roles is the ESLint configuration. Rather than having each package select which configuration they want (and getting it wrong), they now use a shared configuration factory that utilizes the package role. You can read more about the new configuration setup in the [build system documentation](../tooling/cli/02-build-system.md#linting). To migrate the ESLint configuration of all packages in your project, run the following command: @@ -97,7 +97,7 @@ This will migrate all existing `.eslintrc.js` that extend the old configuration ### Step 4 - Use `backstage-cli repo` -The Backstage CLI recently introduced a new `repo` command category, which houses commands that operate on an entire monorepo at once. These commands work particularly well once packages have been migrated to use roles, as that allows for some very effective optimizations. It is typically much faster to use these commands compared to using tools like `lerna`, as they're able to avoid the overhead of calling package scripts through `yarn` and can operate on multiple packages at once. You can read more about the `repo` command in the [CLI command documentation](../local-dev/cli-commands.md#repo-build). +The Backstage CLI recently introduced a new `repo` command category, which houses commands that operate on an entire monorepo at once. These commands work particularly well once packages have been migrated to use roles, as that allows for some very effective optimizations. It is typically much faster to use these commands compared to using tools like `lerna`, as they're able to avoid the overhead of calling package scripts through `yarn` and can operate on multiple packages at once. You can read more about the `repo` command in the [CLI command documentation](../tooling/cli/03-commands.md#repo-build). The way to execute this step of the migration is not as well defined as the previous steps, as it depends on what your development and CI/CD setup looks like. Look for the following patterns to replace in your root `package.json` as well as CI/CD setup: diff --git a/docs/tutorials/using-backstage-proxy-within-plugin.md b/docs/tutorials/using-backstage-proxy-within-plugin.md index 3002eb1aff..1abfe9e5b6 100644 --- a/docs/tutorials/using-backstage-proxy-within-plugin.md +++ b/docs/tutorials/using-backstage-proxy-within-plugin.md @@ -125,7 +125,7 @@ export class MyAwesomeApiClient implements MyAwesomeApi { private async fetch(input: string, init?: RequestInit): Promise { // As configured previously for the backend proxy - const proxyUri = '${await this.discoveryApi.getBaseUrl('proxy')}/'; + const proxyUri = `${await this.discoveryApi.getBaseUrl('proxy')}/`; const resp = await fetch(`${proxyUri}${input}`, init); if (!resp.ok) throw new Error(resp); diff --git a/microsite/data/plugins/apollo-explorer.yaml b/microsite/data/plugins/apollo-explorer.yaml index cebf36654e..2b0f834f92 100644 --- a/microsite/data/plugins/apollo-explorer.yaml +++ b/microsite/data/plugins/apollo-explorer.yaml @@ -1,7 +1,7 @@ --- title: Apollo Explorer -author: unredundant -authorUrl: https://github.com/unredundant +author: brizzbuzz +authorUrl: https://github.com/brizzbuzz category: Debugging description: Integrates Apollo Explorer graphs as a tool to browse GraphQL API endpoints inside Backstage. documentation: https://github.com/backstage/community-plugins/blob/main/workspaces/apollo-explorer/plugins/apollo-explorer/README.md diff --git a/microsite/data/plugins/graphql-catalog.yaml b/microsite/data/plugins/graphql-catalog.yaml new file mode 100644 index 0000000000..9255f475e7 --- /dev/null +++ b/microsite/data/plugins/graphql-catalog.yaml @@ -0,0 +1,14 @@ +--- +title: GraphQL Catalog +author: Frontside Software +authorUrl: https://frontside.com/ +category: Discovery +description: Adds the GraphQL Endpoint to Backstage Catalog as a plugin. +documentation: https://github.com/thefrontside/playhouse/blob/main/plugins/graphql-backend/README.md +iconUrl: https://raw.githubusercontent.com/thefrontside/frontside.com/production/legacy/src/img/frontside-logo.png +npmPackageName: '@frontside/backstage-plugin-graphql-backend' +tags: + - graphql + - catalog + - graphql-catalog +addedDate: '2024-05-01' diff --git a/microsite/data/plugins/ibm-apic-backend.yaml b/microsite/data/plugins/ibm-apic-backend.yaml index 9228524a2d..dcb41a1cef 100644 --- a/microsite/data/plugins/ibm-apic-backend.yaml +++ b/microsite/data/plugins/ibm-apic-backend.yaml @@ -5,7 +5,7 @@ authorUrl: https://croz.net/?utm_source=backstage.io&utm_medium=marketplace&utm_ category: API Management description: Bring IBM APIC to Backstage. documentation: https://github.com/croz-ltd/apic-backend-plugin?utm_source=backstage.io&utm_medium=marketplace&utm_campaign=backstage-ibm-apic-backend -iconUrl: https://croz.net/wp-content/uploads/2024/02/croz_large-o.png +iconUrl: https://croz.net/app/uploads/2024/05/apple-touch-icon.png npmPackageName: '@croz/plugin-ibm-apic-backend' tags: - openapic diff --git a/microsite/data/plugins/infrawallet.yaml b/microsite/data/plugins/infrawallet.yaml new file mode 100644 index 0000000000..bb6c3de7cf --- /dev/null +++ b/microsite/data/plugins/infrawallet.yaml @@ -0,0 +1,14 @@ +--- +title: InfraWallet +author: Electrolux Group +authorUrl: https://opensource.electrolux.one/ +category: FinOps +description: Control your cloud costs just in the way how you control your bank accounts. +documentation: https://github.com/electrolux-oss/infrawallet/blob/main/README.md +iconUrl: /img/infrawallet-logo.png +npmPackageName: '@electrolux-oss/plugin-infrawallet' +tags: + - finops + - cost + - infrastructure +addedDate: '2024-06-03' diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index b7b98bf9e5..75a67406b6 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -171,6 +171,10 @@ const config: Config = { from: '/docs/getting-started/configuration', to: '/docs/getting-started/#next-steps', }, + { + from: '/docs/features/software-templates/authorizing-parameters-steps-and-actions', + to: '/docs/features/software-templates/authorizing-scaffolder-template-details', + }, ], }, ], @@ -181,6 +185,7 @@ const config: Config = { hideIcon: true, customFont: true, buttonStyle: 'dark', + ratingMode: 'stars', }, ], ], diff --git a/microsite/package.json b/microsite/package.json index edc4c08612..fc3405c744 100644 --- a/microsite/package.json +++ b/microsite/package.json @@ -43,7 +43,7 @@ "@types/webpack-env": "^1.18.0", "js-yaml": "^4.1.0", "prettier": "^2.6.2", - "typescript": "~5.1.0", + "typescript": "~5.2.0", "yaml-loader": "^0.8.0" } } diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 624fe83029..5ddd2f97f1 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -69,19 +69,6 @@ "getting-started/keeping-backstage-updated", "getting-started/concepts" ], - "Local Development": [ - { - "type": "category", - "label": "CLI", - "items": [ - "local-dev/cli-overview", - "local-dev/cli-build-system", - "local-dev/cli-commands" - ] - }, - "local-dev/linking-local-packages", - "local-dev/debugging" - ], "Core Features": [ { "type": "category", @@ -130,7 +117,7 @@ "features/software-templates/writing-tests-for-actions", "features/software-templates/writing-custom-field-extensions", "features/software-templates/writing-custom-step-layouts", - "features/software-templates/authorizing-parameters-steps-and-actions", + "features/software-templates/authorizing-scaffolder-template-details", "features/software-templates/migrating-to-rjsf-v5", "features/software-templates/migrating-from-v1beta2-to-v1beta3" ] @@ -358,6 +345,25 @@ ] } ], + "Tooling": [ + { + "type": "category", + "label": "Backstage CLI", + "items": [ + "tooling/cli/overview", + "tooling/cli/build-system", + "tooling/cli/commands" + ] + }, + { + "type": "category", + "label": "Local Development", + "items": [ + "tooling/local-dev/linking-local-packages", + "tooling/local-dev/debugging" + ] + } + ], "Deployment": [ "deployment/index", "deployment/scaling", diff --git a/microsite/static/img/infrawallet-logo.png b/microsite/static/img/infrawallet-logo.png new file mode 100644 index 0000000000..d6bdbe6448 Binary files /dev/null and b/microsite/static/img/infrawallet-logo.png differ diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 572a8fe5fd..ebaeb33c2f 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -3237,9 +3237,9 @@ __metadata: linkType: hard "@types/webpack-env@npm:^1.18.0": - version: 1.18.4 - resolution: "@types/webpack-env@npm:1.18.4" - checksum: f195b3ae974ac3b631477b57737dad7b6c44ecca86770cf3c29f284e02961c9f2dfc619e3e253d8c23966864cb052b1e8437e9834ede32ac97972e6e2235bb51 + version: 1.18.5 + resolution: "@types/webpack-env@npm:1.18.5" + checksum: 4ca8eb4c44e1e1807c3e245442fce7aaf2816a163056de9436bbac44cc47c8bc5b1c9a330dc05748d6616431b1fb5bd5379733fb1da0b78d03c59f4ec824c184 languageName: node linkType: hard @@ -3865,7 +3865,7 @@ __metadata: react-dom: ^18.0.0 sass: ^1.57.1 swc-loader: ^0.2.3 - typescript: ~5.1.0 + typescript: ~5.2.0 yaml-loader: ^0.8.0 languageName: unknown linkType: soft @@ -11266,23 +11266,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:~5.1.0": - version: 5.1.6 - resolution: "typescript@npm:5.1.6" +"typescript@npm:~5.2.0": + version: 5.2.2 + resolution: "typescript@npm:5.2.2" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: b2f2c35096035fe1f5facd1e38922ccb8558996331405eb00a5111cc948b2e733163cc22fab5db46992aba7dd520fff637f2c1df4996ff0e134e77d3249a7350 + checksum: 7912821dac4d962d315c36800fe387cdc0a6298dba7ec171b350b4a6e988b51d7b8f051317786db1094bd7431d526b648aba7da8236607febb26cf5b871d2d3c languageName: node linkType: hard -"typescript@patch:typescript@~5.1.0#~builtin": - version: 5.1.6 - resolution: "typescript@patch:typescript@npm%3A5.1.6#~builtin::version=5.1.6&hash=5da071" +"typescript@patch:typescript@~5.2.0#~builtin": + version: 5.2.2 + resolution: "typescript@patch:typescript@npm%3A5.2.2#~builtin::version=5.2.2&hash=f3b441" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: f53bfe97f7c8b2b6d23cf572750d4e7d1e0c5fff1c36d859d0ec84556a827b8785077bc27676bf7e71fae538e517c3ecc0f37e7f593be913d884805d931bc8be + checksum: 0f4da2f15e6f1245e49db15801dbee52f2bbfb267e1c39225afdab5afee1a72839cd86000e65ee9d7e4dfaff12239d28beaf5ee431357fcced15fb08583d72ca languageName: node linkType: hard diff --git a/mkdocs.yml b/mkdocs.yml index 66ce7d8def..026e76d531 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,12 +33,6 @@ nav: - Create a component: 'getting-started/create-a-component.md' - Keeping Backstage Updated: 'getting-started/keeping-backstage-updated.md' - Key Concepts: 'getting-started/concepts.md' - - Local Development: - - CLI: - - Overview: 'local-dev/cli-overview.md' - - Build System: 'local-dev/cli-build-system.md' - - Commands: 'local-dev/cli-commands.md' - - Linking in Local Packages: 'local-dev/linking-local-packages.md' - Core Features: - Software Catalog: - Overview: 'features/software-catalog/index.md' @@ -178,6 +172,14 @@ nav: - Contributing New Providers: 'auth/add-auth-provider.md' - Service to Service Auth: 'auth/service-to-service-auth.md' - Troubleshooting Auth: 'auth/troubleshooting.md' + - Tooling: + - Backstage CLI: + - Overview: 'tooling/cli/01-overview.md' + - Build System: 'tooling/cli/02-build-system.md' + - Commands: 'tooling/cli/03-commands.md' + - Local Development: + - Linking in Local Packages: 'tooling/local-dev/linking-local-packages.md' + - Debugging Backstage: 'tooling/local-dev/debugging.md' - Deployment: - Deploying Backstage: 'deployment/index.md' - Scaling: 'deployment/scaling.md' diff --git a/package.json b/package.json index 53c87544af..2951eb8540 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.27.0", + "version": "1.28.0-next.2", "private": true, "repository": { "type": "git", @@ -17,7 +17,7 @@ "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-reports": "yarn build:api-reports:only --tsc", - "build:api-reports:only": "backstage-repo-tools api-reports --allow-warnings 'packages/core-components,plugins/+(catalog|catalog-import|git-release-manager|jenkins|kubernetes)' -o ae-wrong-input-file-type --validate-release-tags", + "build:api-reports:only": "NODE_OPTIONS=--max-old-space-size=8192 backstage-repo-tools api-reports --allow-warnings 'packages/core-components,plugins/+(catalog|catalog-import|git-release-manager|jenkins|kubernetes)' -o 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", @@ -129,7 +129,7 @@ "shx": "^0.3.2", "sloc": "^0.3.1", "sort-package-json": "^2.8.0", - "typescript": "~5.1.0" + "typescript": "~5.2.0" }, "packageManager": "yarn@3.8.1", "engines": { diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index 5ab386f512..74ed9e5945 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/app-defaults +## 1.5.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/plugin-permission-react@0.4.23-next.0 + - @backstage/theme@0.5.6-next.0 + +## 1.5.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-plugin-api@1.9.2 + - @backstage/plugin-permission-react@0.4.22 + ## 1.5.5 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 27533f2b9f..de173ffecc 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/app-defaults", - "version": "1.5.5", + "version": "1.5.6-next.1", "description": "Provides the default wiring of a Backstage App", "backstage": { "role": "web-library" diff --git a/packages/app-next-example-plugin/CHANGELOG.md b/packages/app-next-example-plugin/CHANGELOG.md index 356ed1f378..70b5b29965 100644 --- a/packages/app-next-example-plugin/CHANGELOG.md +++ b/packages/app-next-example-plugin/CHANGELOG.md @@ -1,5 +1,21 @@ # app-next-example-plugin +## 0.0.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/frontend-plugin-api@0.6.6-next.1 + +## 0.0.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.0 + ## 0.0.11 ### Patch Changes diff --git a/packages/app-next-example-plugin/package.json b/packages/app-next-example-plugin/package.json index e601961ef6..bca9420d69 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.11", + "version": "0.0.12-next.1", "description": "Backstage internal example plugin", "backstage": { "role": "frontend-plugin" diff --git a/packages/app-next/CHANGELOG.md b/packages/app-next/CHANGELOG.md index 3f03cac54f..35842adb21 100644 --- a/packages/app-next/CHANGELOG.md +++ b/packages/app-next/CHANGELOG.md @@ -1,5 +1,107 @@ # example-app-next +## 0.0.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/plugin-api-docs@0.11.6-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/plugin-scaffolder@1.21.0-next.2 + - @backstage/plugin-techdocs@1.10.6-next.1 + - @backstage/plugin-org@0.6.26-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/plugin-catalog-graph@0.4.6-next.1 + - @backstage/plugin-catalog@1.21.0-next.2 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/cli@0.26.7-next.2 + - @backstage/app-defaults@1.5.6-next.1 + - @backstage/frontend-app-api@0.7.1-next.1 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-app-visualizer@0.1.7-next.1 + - @backstage/plugin-auth-react@0.1.3-next.1 + - @backstage/plugin-catalog-import@0.12.0-next.2 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.5-next.1 + - @backstage/plugin-home@0.7.5-next.1 + - @backstage/plugin-kubernetes@0.11.11-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.12-next.2 + - @backstage/plugin-notifications@0.2.2-next.2 + - @backstage/plugin-scaffolder-react@1.8.7-next.2 + - @backstage/plugin-search@1.4.12-next.2 + - @backstage/plugin-search-react@1.7.12-next.1 + - @backstage/plugin-signals@0.0.7-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.11-next.1 + - @backstage/plugin-techdocs-react@1.2.5-next.1 + - @backstage/plugin-user-settings@0.8.7-next.1 + - @backstage/plugin-permission-react@0.4.23-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6-next.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-search-common@1.2.11 + +## 0.0.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.26.7-next.1 + - @backstage/plugin-catalog-import@0.12.0-next.1 + - @backstage/plugin-kubernetes@0.11.11-next.1 + - @backstage/plugin-search@1.4.12-next.1 + - @backstage/plugin-notifications@0.2.2-next.1 + - @backstage/plugin-scaffolder-react@1.8.7-next.1 + - @backstage/plugin-scaffolder@1.20.2-next.1 + - @backstage/plugin-catalog@1.20.1-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.12-next.1 + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## 0.0.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.6-next.0 + - @backstage/plugin-notifications@0.2.2-next.0 + - @backstage/plugin-scaffolder-react@1.8.6-next.0 + - @backstage/theme@0.5.6-next.0 + - @backstage/plugin-api-docs@0.11.6-next.0 + - @backstage/cli@0.26.6-next.0 + - @backstage/plugin-scaffolder@1.20.1-next.0 + - @backstage/app-defaults@1.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/frontend-app-api@0.7.1-next.0 + - @backstage/plugin-home@0.7.5-next.0 + - @backstage/plugin-search-react@1.7.12-next.0 + - @backstage/plugin-signals@0.0.7-next.0 + - @backstage/plugin-techdocs-react@1.2.5-next.0 + - @backstage/plugin-user-settings@0.8.7-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/integration-react@1.1.27 + - @backstage/plugin-app-visualizer@0.1.7-next.0 + - @backstage/plugin-auth-react@0.1.3-next.0 + - @backstage/plugin-catalog@1.20.1-next.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-catalog-graph@0.4.6-next.0 + - @backstage/plugin-catalog-import@0.11.1-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.5-next.0 + - @backstage/plugin-kubernetes@0.11.11-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.12-next.0 + - @backstage/plugin-org@0.6.26-next.0 + - @backstage/plugin-permission-react@0.4.22 + - @backstage/plugin-search@1.4.12-next.0 + - @backstage/plugin-search-common@1.2.11 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.11-next.0 + ## 0.0.11 ### Patch Changes diff --git a/packages/app-next/package.json b/packages/app-next/package.json index d5f69915a9..c3496f5d3b 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -1,6 +1,6 @@ { "name": "example-app-next", - "version": "0.0.11", + "version": "0.0.12-next.2", "private": true, "repository": { "type": "git", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index fa01c31fd6..fc9f12b26f 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,103 @@ # example-app +## 0.2.98-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/plugin-api-docs@0.11.6-next.1 + - @backstage/plugin-scaffolder@1.21.0-next.2 + - @backstage/plugin-techdocs@1.10.6-next.1 + - @backstage/plugin-org@0.6.26-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/plugin-catalog-graph@0.4.6-next.1 + - @backstage/plugin-catalog@1.21.0-next.2 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/cli@0.26.7-next.2 + - @backstage/app-defaults@1.5.6-next.1 + - @backstage/frontend-app-api@0.7.1-next.1 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-auth-react@0.1.3-next.1 + - @backstage/plugin-catalog-import@0.12.0-next.2 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.5-next.1 + - @backstage/plugin-devtools@0.1.15-next.1 + - @backstage/plugin-home@0.7.5-next.1 + - @backstage/plugin-kubernetes@0.11.11-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.12-next.2 + - @backstage/plugin-notifications@0.2.2-next.2 + - @backstage/plugin-scaffolder-react@1.8.7-next.2 + - @backstage/plugin-search@1.4.12-next.2 + - @backstage/plugin-search-react@1.7.12-next.1 + - @backstage/plugin-signals@0.0.7-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.11-next.1 + - @backstage/plugin-techdocs-react@1.2.5-next.1 + - @backstage/plugin-user-settings@0.8.7-next.1 + - @backstage/plugin-permission-react@0.4.23-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6-next.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-search-common@1.2.11 + +## 0.2.98-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.26.7-next.1 + - @backstage/plugin-catalog-import@0.12.0-next.1 + - @backstage/plugin-kubernetes@0.11.11-next.1 + - @backstage/plugin-search@1.4.12-next.1 + - @backstage/plugin-notifications@0.2.2-next.1 + - @backstage/plugin-scaffolder-react@1.8.7-next.1 + - @backstage/plugin-scaffolder@1.20.2-next.1 + - @backstage/plugin-catalog@1.20.1-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.12-next.1 + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## 0.2.98-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.6-next.0 + - @backstage/plugin-notifications@0.2.2-next.0 + - @backstage/plugin-scaffolder-react@1.8.6-next.0 + - @backstage/theme@0.5.6-next.0 + - @backstage/plugin-api-docs@0.11.6-next.0 + - @backstage/cli@0.26.6-next.0 + - @backstage/plugin-scaffolder@1.20.1-next.0 + - @backstage/app-defaults@1.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/frontend-app-api@0.7.1-next.0 + - @backstage/plugin-home@0.7.5-next.0 + - @backstage/plugin-search-react@1.7.12-next.0 + - @backstage/plugin-signals@0.0.7-next.0 + - @backstage/plugin-techdocs-react@1.2.5-next.0 + - @backstage/plugin-user-settings@0.8.7-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-plugin-api@1.9.2 + - @backstage/integration-react@1.1.27 + - @backstage/plugin-auth-react@0.1.3-next.0 + - @backstage/plugin-catalog@1.20.1-next.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-catalog-graph@0.4.6-next.0 + - @backstage/plugin-catalog-import@0.11.1-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.5-next.0 + - @backstage/plugin-devtools@0.1.15-next.0 + - @backstage/plugin-kubernetes@0.11.11-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.12-next.0 + - @backstage/plugin-org@0.6.26-next.0 + - @backstage/plugin-permission-react@0.4.22 + - @backstage/plugin-search@1.4.12-next.0 + - @backstage/plugin-search-common@1.2.11 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.11-next.0 + ## 0.2.97 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 9978c6fcb4..b08b838f7b 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.97", + "version": "0.2.98-next.2", "backstage": { "role": "frontend" }, diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 652582984e..d741dea098 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -33,22 +33,14 @@ import { OAuthRequestDialog, SignInPage, } from '@backstage/core-components'; -import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; -import { - CatalogEntityPage, - CatalogIndexPage, - catalogPlugin, -} from '@backstage/plugin-catalog'; +import { ApiExplorerPage } from '@backstage/plugin-api-docs'; +import { CatalogEntityPage, CatalogIndexPage } from '@backstage/plugin-catalog'; import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; -import { - CatalogImportPage, - catalogImportPlugin, -} from '@backstage/plugin-catalog-import'; -import { orgPlugin } from '@backstage/plugin-org'; +import { CatalogImportPage } from '@backstage/plugin-catalog-import'; import { HomepageCompositionRoot, VisitListener } from '@backstage/plugin-home'; -import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder'; +import { ScaffolderPage } from '@backstage/plugin-scaffolder'; import { ScaffolderFieldExtensions, ScaffolderLayouts, @@ -56,7 +48,6 @@ import { import { SearchPage } from '@backstage/plugin-search'; import { TechDocsIndexPage, - techdocsPlugin, TechDocsReaderPage, } from '@backstage/plugin-techdocs'; import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; @@ -119,23 +110,6 @@ const app = createApp({ ); }, }, - bindRoutes({ bind }) { - bind(catalogPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - viewTechDoc: techdocsPlugin.routes.docRoot, - createFromTemplate: scaffolderPlugin.routes.selectedTemplate, - }); - bind(apiDocsPlugin.externalRoutes, { - registerApi: catalogImportPlugin.routes.importPage, - }); - bind(scaffolderPlugin.externalRoutes, { - registerComponent: catalogImportPlugin.routes.importPage, - viewTechDoc: techdocsPlugin.routes.docRoot, - }); - bind(orgPlugin.externalRoutes, { - catalogIndex: catalogPlugin.routes.catalogIndex, - }); - }, }); const routes = ( diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md index 96259c80da..7d3bb8dabf 100644 --- a/packages/backend-app-api/CHANGELOG.md +++ b/packages/backend-app-api/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/backend-app-api +## 0.7.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-node@0.2.6-next.1 + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-permission-node@0.7.30-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/config-loader@1.8.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.7.6-next.1 + +### Patch Changes + +- 398b82a: Add support for JWKS tokens in ExternalTokenHandler. +- 9e63318: Added an optional `accessRestrictions` to external access service tokens and service principals in general, such that you can limit their access to certain plugins or permissions. +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-permission-node@0.7.30-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/cli-node@0.2.6-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/plugin-auth-node@0.4.14-next.1 + +## 0.7.6-next.0 + +### Patch Changes + +- b7de623: Fixed a potential crash when passing an object with a `null` prototype as log meta. +- 7d30d95: Fixing issue with log meta fields possibly being circular refs +- 6a576dc: Stop using `getVoidLogger` in tests to reduce the dependency on the soon-to-deprecate `backstage-common` package. +- 6551b3d: Deprecated core service factories and implementations and moved them over to + subpath exports on `@backstage/backend-defaults` instead. E.g. + `@backstage/backend-defaults/scheduler` is where the service factory and default + implementation of `coreServices.scheduler` now lives. +- d617103: Updating the logger redaction message to something less dramatic +- Updated dependencies + - @backstage/cli-node@0.2.6-next.0 + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.7.3 ### Patch Changes diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 5720c1c60f..11d9dd90d4 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -8,7 +8,7 @@ import type { AppConfig } from '@backstage/config'; import { AuthService } from '@backstage/backend-plugin-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; -import { CacheClient } from '@backstage/backend-common'; +import { CacheService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { ConfigSchema } from '@backstage/config-loader'; import { CorsOptions } from 'cors'; @@ -65,8 +65,8 @@ export interface Backend { stop(): Promise; } -// @public (undocumented) -export const cacheServiceFactory: () => ServiceFactory; +// @public @deprecated (undocumented) +export const cacheServiceFactory: () => ServiceFactory; // @public (undocumented) export function createConfigSecretEnumerator(options: { @@ -100,7 +100,7 @@ export interface CreateSpecializedBackendOptions { defaultServiceFactories: ServiceFactoryOrFunction[]; } -// @public (undocumented) +// @public @deprecated (undocumented) export const databaseServiceFactory: () => ServiceFactory< PluginDatabaseManager, 'plugin' @@ -121,7 +121,7 @@ export interface DefaultRootHttpRouterOptions { indexPath?: string | false; } -// @public (undocumented) +// @public @deprecated (undocumented) export const discoveryServiceFactory: () => ServiceFactory< DiscoveryService, 'plugin' @@ -137,7 +137,7 @@ export interface ExtendedHttpServer extends http.Server { stop(): Promise; } -// @public +// @public @deprecated export class HostDiscovery implements DiscoveryService { static fromConfig( config: Config, @@ -190,13 +190,13 @@ export type HttpServerOptions = { }; }; -// @public +// @public @deprecated export type IdentityFactoryOptions = { issuer?: string; algorithms?: string[]; }; -// @public (undocumented) +// @public @deprecated (undocumented) export const identityServiceFactory: ( options?: IdentityFactoryOptions | undefined, ) => ServiceFactory; @@ -208,7 +208,7 @@ export interface LifecycleMiddlewareOptions { startupRequestPauseTimeout?: HumanDuration; } -// @public +// @public @deprecated export const lifecycleServiceFactory: () => ServiceFactory< LifecycleService, 'plugin' @@ -255,7 +255,7 @@ export interface MiddlewareFactoryOptions { logger: LoggerService; } -// @public (undocumented) +// @public @deprecated (undocumented) export const permissionsServiceFactory: () => ServiceFactory< PermissionsService, 'plugin' @@ -270,7 +270,7 @@ export function readHelmetOptions(config?: Config): HelmetOptions; // @public export function readHttpServerOptions(config?: Config): HttpServerOptions; -// @public (undocumented) +// @public @deprecated (undocumented) export interface RootConfigFactoryOptions { argv?: string[]; remote?: Pick; @@ -278,7 +278,7 @@ export interface RootConfigFactoryOptions { watch?: boolean; } -// @public (undocumented) +// @public @deprecated (undocumented) export const rootConfigServiceFactory: ( options?: RootConfigFactoryOptions | undefined, ) => ServiceFactory; @@ -314,7 +314,7 @@ export const rootHttpRouterServiceFactory: ( options?: RootHttpRouterFactoryOptions | undefined, ) => ServiceFactory; -// @public +// @public @deprecated export const rootLifecycleServiceFactory: () => ServiceFactory< RootLifecycleService, 'root' @@ -326,19 +326,19 @@ export const rootLoggerServiceFactory: () => ServiceFactory< 'root' >; -// @public (undocumented) +// @public @deprecated (undocumented) export const schedulerServiceFactory: () => ServiceFactory< SchedulerService, 'plugin' >; -// @public (undocumented) +// @public @deprecated (undocumented) export const tokenManagerServiceFactory: () => ServiceFactory< TokenManagerService, 'plugin' >; -// @public (undocumented) +// @public @deprecated (undocumented) export const urlReaderServiceFactory: () => ServiceFactory; // @public (undocumented) diff --git a/packages/backend-app-api/config.d.ts b/packages/backend-app-api/config.d.ts index f84493828a..80f97cb4b5 100644 --- a/packages/backend-app-api/config.d.ts +++ b/packages/backend-app-api/config.d.ts @@ -88,6 +88,46 @@ export interface Config { */ subject: string; }; + /** + * Restricts what types of access that are permitted for this access + * method. If no access restrictions are given, it'll have unlimited + * access. This access restriction applies for the framework level; + * individual plugins may have their own access control mechanisms + * on top of this. + */ + accessRestrictions?: Array<{ + /** + * Permit access to make requests to this plugin. + * + * Can be further refined by setting additional fields below. + */ + plugin: string; + /** + * If given, this method is limited to only performing actions + * with these named permissions in this plugin. + * + * Note that this only applies where permissions checks are + * enabled in the first place. Endpoints that are not protected by + * the permissions system at all, are not affected by this + * setting. + */ + permission?: string | Array; + /** + * If given, this method is limited to only performing actions + * whose permissions have these attributes. + * + * Note that this only applies where permissions checks are + * enabled in the first place. Endpoints that are not protected by + * the permissions system at all, are not affected by this + * setting. + */ + permissionAttribute?: { + /** + * One of more of 'create', 'read', 'update', or 'delete'. + */ + action?: string | Array; + }; + }>; } | { /** @@ -130,6 +170,87 @@ export interface Config { */ subject: string; }; + /** + * Restricts what types of access that are permitted for this access + * method. If no access restrictions are given, it'll have unlimited + * access. This access restriction applies for the framework level; + * individual plugins may have their own access control mechanisms + * on top of this. + */ + accessRestrictions?: Array<{ + /** + * Permit access to make requests to this plugin. + * + * Can be further refined by setting additional fields below. + */ + plugin: string; + /** + * If given, this method is limited to only performing actions + * with these named permissions in this plugin. + * + * Note that this only applies where permissions checks are + * enabled in the first place. Endpoints that are not protected by + * the permissions system at all, are not affected by this + * setting. + */ + permission?: string | Array; + /** + * If given, this method is limited to only performing actions + * whose permissions have these attributes. + * + * Note that this only applies where permissions checks are + * enabled in the first place. Endpoints that are not protected by + * the permissions system at all, are not affected by this + * setting. + */ + permissionAttribute?: { + /** + * One of more of 'create', 'read', 'update', or 'delete'. + */ + action?: string | Array; + }; + }>; + } + | { + /** + * This access method consists of a JWKS endpoint that can be used to + * verify JWT tokens. + * + * Callers generate JWT tokens via 3rd party tooling + * and pass them in the Authorization header: + * + * ``` + * Authorization: Bearer eZv5o+fW3KnR3kVabMW4ZcDNLPl8nmMW + * ``` + */ + type: 'jwks'; + options: { + /** + * The full URL of the JWKS endpoint. + */ + url: string; + /** + * Sets the algorithm(s) that should be used to verify the JWT tokens. + * The passed JWTs must have been signed using one of the listed algorithms. + */ + algorithm?: string | string[]; + /** + * Sets the issuer(s) that should be used to verify the JWT tokens. + * Passed JWTs must have an `iss` claim which matches one of the specified issuers. + */ + issuer?: string | string[]; + /** + * Sets the audience(s) that should be used to verify the JWT tokens. + * The passed JWTs must have an "aud" claim that matches one of the audiences specified, + * or have no audience specified. + */ + audience?: string | string[]; + /** + * Sets an optional subject prefix. Passes the subject to called plugins. + * Useful for debugging and tracking purposes. + */ + subjectPrefix?: string; + }; } >; }; diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 414a4333e7..efeff8a96d 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-app-api", - "version": "0.7.3", + "version": "0.7.6-next.2", "description": "Core API used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/backend-app-api/src/logging/WinstonLogger.test.ts b/packages/backend-app-api/src/logging/WinstonLogger.test.ts index 4b608f8e57..0f9e079943 100644 --- a/packages/backend-app-api/src/logging/WinstonLogger.test.ts +++ b/packages/backend-app-api/src/logging/WinstonLogger.test.ts @@ -50,7 +50,7 @@ describe('WinstonLogger', () => { expect.objectContaining({ [MESSAGE]: JSON.stringify({ level: 'error', - message: '[REDACTED]) from this file', + message: '***) from this file', }), }), expect.any(Function), @@ -83,10 +83,10 @@ describe('WinstonLogger', () => { [MESSAGE]: JSON.stringify({ level: 'error', message: 'something went wrong', - nested: '[REDACTED] (world) from nested object', + nested: '*** (world) from nested object', null: null, nullProto: { - foo: '[REDACTED] foo', + foo: '*** foo', }, }), }), diff --git a/packages/backend-app-api/src/logging/WinstonLogger.ts b/packages/backend-app-api/src/logging/WinstonLogger.ts index 8ee86d247f..64e97b230c 100644 --- a/packages/backend-app-api/src/logging/WinstonLogger.ts +++ b/packages/backend-app-api/src/logging/WinstonLogger.ts @@ -92,7 +92,7 @@ export class WinstonLogger implements RootLoggerService { return obj; } - obj[MESSAGE] = obj[MESSAGE]?.replace?.(redactionPattern, '[REDACTED]'); + obj[MESSAGE] = obj[MESSAGE]?.replace?.(redactionPattern, '***'); return obj; })(), diff --git a/packages/backend-app-api/src/services/implementations/auth/DefaultAuthService.ts b/packages/backend-app-api/src/services/implementations/auth/DefaultAuthService.ts index dc76a8761b..b4feb46cce 100644 --- a/packages/backend-app-api/src/services/implementations/auth/DefaultAuthService.ts +++ b/packages/backend-app-api/src/services/implementations/auth/DefaultAuthService.ts @@ -82,7 +82,11 @@ export class DefaultAuthService implements AuthService { const externalResult = await this.externalTokenHandler.verifyToken(token); if (externalResult) { - return createCredentialsWithServicePrincipal(externalResult.subject); + return createCredentialsWithServicePrincipal( + externalResult.subject, + undefined, + externalResult.accessRestrictions, + ); } throw new AuthenticationError('Illegal token'); diff --git a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts index 96d4ce4dc9..71fbe64963 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts @@ -42,7 +42,26 @@ const mockDeps = [ data: { backend: { baseUrl: 'http://localhost', - auth: { keys: [{ secret: 'abc' }] }, + auth: { + keys: [{ secret: 'abc' }], + externalAccess: [ + { + type: 'static', + options: { + token: 'limited-static-token', + subject: 'limited-static-subject', + }, + accessRestrictions: [{ plugin: 'catalog', permission: 'do.it' }], + }, + { + type: 'static', + options: { + token: 'unlimited-static-token', + subject: 'unlimited-static-subject', + }, + }, + ], + }, }, }, }), @@ -385,4 +404,43 @@ describe('authServiceFactory', () => { "Unable to call 'kubernetes' plugin on behalf of user, because the target plugin does not support on-behalf-of tokens or the plugin doesn't exist", ); }); + + it('should eagerly reject access to external access tokens based on plugin id', async () => { + const tester = ServiceFactoryTester.from(authServiceFactory, { + dependencies: mockDeps, + }); + + const catalogAuth = await tester.get('catalog'); + + await expect( + catalogAuth.authenticate('limited-static-token'), + ).resolves.toMatchObject({ + principal: { + subject: 'limited-static-subject', + accessRestrictions: { permissionNames: ['do.it'] }, + }, + }); + + await expect( + catalogAuth.authenticate('unlimited-static-token'), + ).resolves.toMatchObject({ + principal: { + subject: 'unlimited-static-subject', + }, + }); + + const scaffolderAuth = await tester.get('scaffolder'); + + await expect( + scaffolderAuth.authenticate('limited-static-token'), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"This token's access is restricted to plugin(s) 'catalog'"`, + ); + + await expect( + scaffolderAuth.authenticate('unlimited-static-token'), + ).resolves.toMatchObject({ + principal: { subject: 'unlimited-static-subject' }, + }); + }); }); diff --git a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts index 7bbd012c01..ad4fb17d88 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts @@ -39,19 +39,7 @@ export const authServiceFactory = createServiceFactory({ // new auth services in the new backend system. tokenManager: coreServices.tokenManager, }, - async createRootContext({ config, logger }) { - const externalTokens = ExternalTokenHandler.create({ - config, - logger, - }); - return { - externalTokens, - }; - }, - async factory( - { config, discovery, plugin, tokenManager, logger, database }, - { externalTokens }, - ) { + async factory({ config, discovery, plugin, tokenManager, logger, database }) { const disableDefaultAuthPolicy = Boolean( config.getOptionalBoolean( 'backend.auth.dangerouslyDisableDefaultAuthPolicy', @@ -73,6 +61,11 @@ export const authServiceFactory = createServiceFactory({ publicKeyStore, discovery, }); + const externalTokens = ExternalTokenHandler.create({ + ownPluginId: plugin.getId(), + config, + logger, + }); return new DefaultAuthService( userTokens, diff --git a/packages/backend-app-api/src/services/implementations/auth/external/ExternalTokenHandler.test.ts b/packages/backend-app-api/src/services/implementations/auth/external/ExternalTokenHandler.test.ts new file mode 100644 index 0000000000..559107c59c --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/auth/external/ExternalTokenHandler.test.ts @@ -0,0 +1,211 @@ +/* + * 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 { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; +import { ExternalTokenHandler } from './ExternalTokenHandler'; +import { TokenHandler } from './types'; +import { + mockServices, + setupRequestMockHandlers, +} from '@backstage/backend-test-utils'; +import { randomBytes } from 'crypto'; +import { SignJWT, exportJWK, generateKeyPair } from 'jose'; +import { DateTime } from 'luxon'; +import { v4 as uuid } from 'uuid'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + +// Simplified copy of TokenFactory in @backstage/plugin-auth-backend +interface AnyJWK extends Record { + use: 'sig'; + alg: string; + kid: string; + kty: string; +} +class FakeTokenFactory { + private readonly keys = new Array(); + + constructor( + private readonly options: { + issuer: string; + keyDurationSeconds: number; + }, + ) {} + + async issueToken(params: { + claims: { + sub: string; + ent?: string[]; + }; + }): Promise { + const pair = await generateKeyPair('RS256'); + const publicKey = await exportJWK(pair.publicKey); + const kid = uuid(); + publicKey.kid = kid; + this.keys.push(publicKey as AnyJWK); + + const iss = this.options.issuer; + const sub = params.claims.sub; + const ent = params.claims.ent; + const aud = 'backstage'; + const iat = Math.floor(Date.now() / 1000); + const exp = iat + this.options.keyDurationSeconds; + + return new SignJWT({ iss, sub, aud, iat, exp, ent, kid }) + .setProtectedHeader({ alg: 'RS256', ent: ent, kid: kid }) + .setIssuer(iss) + .setAudience(aud) + .setSubject(sub) + .setIssuedAt(iat) + .setExpirationTime(exp) + .sign(pair.privateKey); + } + + async listPublicKeys(): Promise<{ keys: AnyJWK[] }> { + return { keys: this.keys }; + } +} + +describe('ExternalTokenHandler', () => { + const server = setupServer(); + setupRequestMockHandlers(server); + + it('skips over inner handlers that do not match, and applies plugin restrictions', async () => { + const handler1: TokenHandler = { + add: jest.fn(), + verifyToken: jest.fn().mockResolvedValue(undefined), + }; + + const handler2: TokenHandler = { + add: jest.fn(), + verifyToken: jest.fn().mockResolvedValue({ + subject: 'sub', + allAccessRestrictions: new Map( + Object.entries({ + plugin1: { + permissionNames: ['do.it'], + } satisfies BackstagePrincipalAccessRestrictions, + }), + ), + }), + }; + + const plugin1 = new ExternalTokenHandler('plugin1', [handler1, handler2]); + const plugin2 = new ExternalTokenHandler('plugin2', [handler1, handler2]); + + await expect(plugin1.verifyToken('token')).resolves.toEqual({ + subject: 'sub', + accessRestrictions: { permissionNames: ['do.it'] }, + }); + await expect( + plugin2.verifyToken('token'), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"This token's access is restricted to plugin(s) 'plugin1'"`, + ); + }); + + it('successfully parses known methods', async () => { + const legacyKey = randomBytes(24); + + const factory = new FakeTokenFactory({ + issuer: 'blah', + keyDurationSeconds: 100, + }); + + server.use( + rest.get( + 'https://example.com/.well-known/jwks.json', + async (_, res, ctx) => { + const keys = await factory.listPublicKeys(); + return res(ctx.json(keys)); + }, + ), + ); + + const handler = ExternalTokenHandler.create({ + ownPluginId: 'catalog', + logger: mockServices.logger.mock(), + config: mockServices.rootConfig({ + data: { + backend: { + auth: { + externalAccess: [ + { + type: 'legacy', + options: { + secret: legacyKey.toString('base64'), + subject: 'legacy-subject', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }, + { + type: 'static', + options: { + token: 'defdefdef', + subject: 'static-subject', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }, + { + type: 'jwks', + options: { + url: 'https://example.com/.well-known/jwks.json', + algorithm: 'RS256', + issuer: 'blah', + audience: 'backstage', + subjectPrefix: 'custom-prefix', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }, + ], + }, + }, + }, + }), + }); + + const legacyToken = await new SignJWT({ + sub: 'backstage-server', + exp: DateTime.now().plus({ minutes: 1 }).toUnixInteger(), + }) + .setProtectedHeader({ alg: 'HS256' }) + .sign(legacyKey); + + await expect(handler.verifyToken(legacyToken)).resolves.toEqual({ + subject: 'legacy-subject', + accessRestrictions: { permissionNames: ['catalog.entity.read'] }, + }); + + await expect(handler.verifyToken('defdefdef')).resolves.toEqual({ + subject: 'static-subject', + accessRestrictions: { permissionNames: ['catalog.entity.read'] }, + }); + + const jwksToken = await factory.issueToken({ + claims: { sub: 'jwks-subject' }, + }); + await expect(handler.verifyToken(jwksToken)).resolves.toEqual({ + subject: 'external:custom-prefix:jwks-subject', + accessRestrictions: { permissionNames: ['catalog.entity.read'] }, + }); + }); +}); diff --git a/packages/backend-app-api/src/services/implementations/auth/external/ExternalTokenHandler.ts b/packages/backend-app-api/src/services/implementations/auth/external/ExternalTokenHandler.ts index 588a1f1794..72eeae6fcf 100644 --- a/packages/backend-app-api/src/services/implementations/auth/external/ExternalTokenHandler.ts +++ b/packages/backend-app-api/src/services/implementations/auth/external/ExternalTokenHandler.ts @@ -15,15 +15,19 @@ */ import { + BackstagePrincipalAccessRestrictions, LoggerService, RootConfigService, } from '@backstage/backend-plugin-api'; +import { NotAllowedError } from '@backstage/errors'; import { LegacyTokenHandler } from './legacy'; import { StaticTokenHandler } from './static'; +import { JWKSHandler } from './jwks'; import { TokenHandler } from './types'; const NEW_CONFIG_KEY = 'backend.auth.externalAccess'; const OLD_CONFIG_KEY = 'backend.auth.keys'; +let loggedDeprecationWarning = false; /** * Handles all types of external caller token types (i.e. not Backstage user @@ -33,16 +37,19 @@ const OLD_CONFIG_KEY = 'backend.auth.keys'; */ export class ExternalTokenHandler { static create(options: { + ownPluginId: string; config: RootConfigService; logger: LoggerService; }): ExternalTokenHandler { - const { config, logger } = options; + const { ownPluginId, config, logger } = options; const staticHandler = new StaticTokenHandler(); const legacyHandler = new LegacyTokenHandler(); + const jwksHandler = new JWKSHandler(); const handlers: Record = { static: staticHandler, legacy: legacyHandler, + jwks: jwksHandler, }; // Load the new-style handlers @@ -58,12 +65,13 @@ export class ExternalTokenHandler { `Unknown type '${type}' in ${NEW_CONFIG_KEY}, expected one of ${valid}`, ); } - handler.add(handlerConfig.getConfig('options')); + handler.add(handlerConfig); } // Load the old keys too const legacyConfigs = config.getOptionalConfigArray(OLD_CONFIG_KEY) ?? []; - if (legacyConfigs.length) { + if (legacyConfigs.length && !loggedDeprecationWarning) { + loggedDeprecationWarning = true; logger.warn( `DEPRECATION WARNING: The ${OLD_CONFIG_KEY} config has been replaced by ${NEW_CONFIG_KEY}, see https://backstage.io/docs/auth/service-to-service-auth`, ); @@ -72,18 +80,48 @@ export class ExternalTokenHandler { legacyHandler.addOld(handlerConfig); } - return new ExternalTokenHandler(Object.values(handlers)); + return new ExternalTokenHandler(ownPluginId, Object.values(handlers)); } - constructor(private readonly handlers: TokenHandler[]) {} + constructor( + private readonly ownPluginId: string, + private readonly handlers: TokenHandler[], + ) {} - async verifyToken(token: string): Promise<{ subject: string } | undefined> { + async verifyToken(token: string): Promise< + | { + subject: string; + accessRestrictions?: BackstagePrincipalAccessRestrictions; + } + | undefined + > { for (const handler of this.handlers) { const result = await handler.verifyToken(token); if (result) { - return result; + const { allAccessRestrictions, ...rest } = result; + if (allAccessRestrictions) { + const accessRestrictions = allAccessRestrictions.get( + this.ownPluginId, + ); + if (!accessRestrictions) { + const valid = [...allAccessRestrictions.keys()] + .map(k => `'${k}'`) + .join(', '); + throw new NotAllowedError( + `This token's access is restricted to plugin(s) ${valid}`, + ); + } + + return { + ...rest, + accessRestrictions, + }; + } + + return rest; } } + return undefined; } } diff --git a/packages/backend-app-api/src/services/implementations/auth/external/helpers.test.ts b/packages/backend-app-api/src/services/implementations/auth/external/helpers.test.ts new file mode 100644 index 0000000000..5b8e40ed4f --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/auth/external/helpers.test.ts @@ -0,0 +1,247 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { + readAccessRestrictionsFromConfig, + readStringOrStringArrayFromConfig, +} from './helpers'; +import { JsonObject } from '@backstage/types'; +import { mockServices } from '@backstage/backend-test-utils'; + +describe('readStringOrStringArrayFromConfig', () => { + it('handles all cases correctly', () => { + const config = mockServices.rootConfig({ + data: { + wrongType: 1, + wrongTypeInArray: [1], + singleString: 'a', + spaceSeparatedString: 'a b c', + commaSeparatedString: 'a,b,c', + mixedSeparatorsString: 'a b,c ,, d', + emptyString: '', + emptyArray: [], + simpleArray: ['a', 'b', 'c'], + arrayWithSeparators: ['a b', 'c,d', 'e'], + complexDuplicates: ['a', 'a b', 'a', 'b, a'], + }, + }); + + expect(() => + readStringOrStringArrayFromConfig(config, 'wrongType'), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'wrongType' in 'mock-config', got number, wanted string"`, + ); + expect(() => + readStringOrStringArrayFromConfig(config, 'wrongTypeInArray'), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'wrongTypeInArray[0]' in 'mock-config', got number, wanted string-array"`, + ); + expect(readStringOrStringArrayFromConfig(config, 'singleString')).toEqual([ + 'a', + ]); + expect( + readStringOrStringArrayFromConfig(config, 'spaceSeparatedString'), + ).toEqual(['a', 'b', 'c']); + expect( + readStringOrStringArrayFromConfig(config, 'commaSeparatedString'), + ).toEqual(['a', 'b', 'c']); + expect( + readStringOrStringArrayFromConfig(config, 'mixedSeparatorsString'), + ).toEqual(['a', 'b', 'c', 'd']); + expect(() => + readStringOrStringArrayFromConfig(config, 'emptyString'), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'emptyString' in 'mock-config', got empty-string, wanted string"`, + ); + expect( + readStringOrStringArrayFromConfig(config, 'emptyArray'), + ).toBeUndefined(); + expect(readStringOrStringArrayFromConfig(config, 'simpleArray')).toEqual([ + 'a', + 'b', + 'c', + ]); + expect( + readStringOrStringArrayFromConfig(config, 'arrayWithSeparators'), + ).toEqual(['a', 'b', 'c', 'd', 'e']); + expect( + readStringOrStringArrayFromConfig(config, 'complexDuplicates'), + ).toEqual(['a', 'b']); + }); +}); + +describe('readAccessRestrictionsFromConfig', () => { + function r(config: JsonObject) { + return readAccessRestrictionsFromConfig(new ConfigReader(config)); + } + + it('handles empty / missing restrictions', () => { + expect(r({})).toBeUndefined(); + expect(r({ accessRestrictions: [] })).toBeUndefined(); + }); + + it('handles type errors', () => { + expect(() => + r({ accessRestrictions: 7 }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'accessRestrictions' in 'mock-config', got number, wanted object-array"`, + ); + expect(() => + r({ accessRestrictions: ['hello'] }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'accessRestrictions[0]' in 'mock-config', got string, wanted object-array"`, + ); + expect(() => + r({ accessRestrictions: [{ unknown: {} }] }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid key 'unknown' in 'accessRestrictions' config, expected one of 'plugin', 'permission', 'permissionAttribute'"`, + ); + expect(() => + r({ accessRestrictions: [{ plugin: 7 }] }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'accessRestrictions[0].plugin' in 'mock-config', got number, wanted string"`, + ); + expect(() => + r({ accessRestrictions: [{ plugin: 'valid', permission: 7 }] }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'accessRestrictions[0].permission' in 'mock-config', got number, wanted string"`, + ); + expect(() => + r({ accessRestrictions: [{ plugin: 'valid', permission: [7] }] }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'accessRestrictions[0].permission[0]' in 'mock-config', got number, wanted string-array"`, + ); + expect(() => + r({ accessRestrictions: [{ plugin: 'valid', permissionAttribute: 7 }] }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'accessRestrictions[0].permissionAttribute' in 'mock-config', got number, wanted object"`, + ); + expect(() => + r({ + accessRestrictions: [ + { plugin: 'valid', permissionAttribute: { a: [] } }, + ], + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid key 'a' in 'permissionAttribute' config, expected 'action'"`, + ); + expect(() => + r({ + accessRestrictions: [ + { plugin: 'valid', permissionAttribute: { action: 7 } }, + ], + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'accessRestrictions[0].permissionAttribute.action' in 'mock-config', got number, wanted string"`, + ); + expect(() => + r({ + accessRestrictions: [ + { plugin: 'valid', permissionAttribute: { action: 'wrong' } }, + ], + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid value 'wrong' at 'action' in 'permissionAttributes' config, valid values are 'create', 'read', 'update', 'delete'"`, + ); + }); + + it('parses valid access restrictions', () => { + expect( + r({ + accessRestrictions: [ + { + plugin: 'a', + }, + ], + }), + ).toEqual( + new Map( + Object.entries({ + a: {}, + }), + ), + ); + + expect( + r({ + accessRestrictions: [ + { + plugin: 'a', + permission: 'a, b a', + }, + ], + }), + ).toEqual( + new Map( + Object.entries({ + a: { permissionNames: ['a', 'b'] }, + }), + ), + ); + + expect( + r({ + accessRestrictions: [ + { + plugin: 'a', + permission: ['a', 'b', 'a'], + }, + ], + }), + ).toEqual( + new Map( + Object.entries({ + a: { permissionNames: ['a', 'b'] }, + }), + ), + ); + + expect( + r({ + accessRestrictions: [ + { + plugin: 'a', + permissionAttribute: { action: 'read, update read' }, + }, + ], + }), + ).toEqual( + new Map( + Object.entries({ + a: { permissionAttributes: { action: ['read', 'update'] } }, + }), + ), + ); + + expect( + r({ + accessRestrictions: [ + { + plugin: 'a', + permissionAttribute: { action: ['read', 'update', 'read'] }, + }, + ], + }), + ).toEqual( + new Map( + Object.entries({ + a: { permissionAttributes: { action: ['read', 'update'] } }, + }), + ), + ); + }); +}); diff --git a/packages/backend-app-api/src/services/implementations/auth/external/helpers.ts b/packages/backend-app-api/src/services/implementations/auth/external/helpers.ts new file mode 100644 index 0000000000..d6aa0a01ff --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/auth/external/helpers.ts @@ -0,0 +1,149 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import { AccessRestriptionsMap } from './types'; + +/** + * Parses and returns the `accessRestrictions` configuration from an + * `externalAccess` entry, or undefined if there wasn't one. + * + * @internal + */ +export function readAccessRestrictionsFromConfig( + externalAccessEntryConfig: Config, +): AccessRestriptionsMap | undefined { + const configs = + externalAccessEntryConfig.getOptionalConfigArray('accessRestrictions') ?? + []; + + const result: AccessRestriptionsMap = new Map(); + for (const config of configs) { + const validKeys = ['plugin', 'permission', 'permissionAttribute']; + for (const key of config.keys()) { + if (!validKeys.includes(key)) { + const valid = validKeys.map(k => `'${k}'`).join(', '); + throw new Error( + `Invalid key '${key}' in 'accessRestrictions' config, expected one of ${valid}`, + ); + } + } + + const pluginId = config.getString('plugin'); + const permissionNames = readPermissionNames(config); + const permissionAttributes = readPermissionAttributes(config); + + if (result.has(pluginId)) { + throw new Error( + `Attempted to declare 'accessRestrictions' twice for plugin '${pluginId}', which is not permitted`, + ); + } + + result.set(pluginId, { + ...(permissionNames ? { permissionNames } : {}), + ...(permissionAttributes ? { permissionAttributes } : {}), + }); + } + + return result.size ? result : undefined; +} + +/** + * Reads a config value as a string or an array of strings, and deduplicates and + * splits by comma/space into a string array. Can also validate against a known + * set of values. Returns undefined if the key didn't exist or if the array + * would have ended up being empty. + * + * @internal + */ +export function readStringOrStringArrayFromConfig( + root: Config, + key: string, + validValues?: readonly T[], +): T[] | undefined { + if (!root.has(key)) { + return undefined; + } + + const rawValues = Array.isArray(root.get(key)) + ? root.getStringArray(key) + : [root.getString(key)]; + + const values = [ + ...new Set( + rawValues + .map(v => v.split(/[ ,]/)) + .flat() + .filter(Boolean), + ), + ]; + + if (!values.length) { + return undefined; + } + + if (validValues?.length) { + for (const value of values) { + if (!validValues.includes(value as T)) { + const valid = validValues.map(k => `'${k}'`).join(', '); + throw new Error( + `Invalid value '${value}' at '${key}' in 'permissionAttributes' config, valid values are ${valid}`, + ); + } + } + } + + return values as T[]; +} + +function readPermissionNames(externalAccessEntryConfig: Config) { + return readStringOrStringArrayFromConfig( + externalAccessEntryConfig, + 'permission', + ); +} + +function readPermissionAttributes(externalAccessEntryConfig: Config) { + const config = externalAccessEntryConfig.getOptionalConfig( + 'permissionAttribute', + ); + if (!config) { + return undefined; + } + + const validKeys = ['action']; + for (const key of config.keys()) { + if (!validKeys.includes(key)) { + const valid = validKeys.map(k => `'${k}'`).join(', '); + throw new Error( + `Invalid key '${key}' in 'permissionAttribute' config, expected ${valid}`, + ); + } + } + + const action = readStringOrStringArrayFromConfig(config, 'action', [ + 'create', + 'read', + 'update', + 'delete', + ]); + + const result = { + ...(action ? { action } : {}), + }; + + return Object.keys(result).length ? result : undefined; +} diff --git a/packages/backend-app-api/src/services/implementations/auth/external/jwks.test.ts b/packages/backend-app-api/src/services/implementations/auth/external/jwks.test.ts new file mode 100644 index 0000000000..56930e4480 --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/auth/external/jwks.test.ts @@ -0,0 +1,261 @@ +/* + * 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 { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { SignJWT, exportJWK, generateKeyPair } from 'jose'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { v4 as uuid } from 'uuid'; +import { JWKSHandler } from './jwks'; + +// Simplified copy of TokenFactory in @backstage/plugin-auth-backend +interface AnyJWK extends Record { + use: 'sig'; + alg: string; + kid: string; + kty: string; +} +class FakeTokenFactory { + private readonly keys = new Array(); + + constructor( + private readonly options: { + issuer: string; + keyDurationSeconds: number; + }, + ) {} + + async issueToken(params: { + claims: { + sub: string; + ent?: string[]; + }; + }): Promise { + const pair = await generateKeyPair('RS256'); + const publicKey = await exportJWK(pair.publicKey); + const kid = uuid(); + publicKey.kid = kid; + this.keys.push(publicKey as AnyJWK); + + const iss = this.options.issuer; + const sub = params.claims.sub; + const ent = params.claims.ent; + const aud = 'backstage'; + const iat = Math.floor(Date.now() / 1000); + const exp = iat + this.options.keyDurationSeconds; + + return new SignJWT({ iss, sub, aud, iat, exp, ent, kid }) + .setProtectedHeader({ alg: 'RS256', ent: ent, kid: kid }) + .setIssuer(iss) + .setAudience(aud) + .setSubject(sub) + .setIssuedAt(iat) + .setExpirationTime(exp) + .sign(pair.privateKey); + } + + async listPublicKeys(): Promise<{ keys: AnyJWK[] }> { + return { keys: this.keys }; + } +} + +const server = setupServer(); +const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; + +describe('JWKSHandler', () => { + let factory: FakeTokenFactory; + let mockSubject: string; + const keyDurationSeconds = 5; + + setupRequestMockHandlers(server); + + beforeEach(() => { + mockSubject = 'test_subject'; + + factory = new FakeTokenFactory({ + issuer: mockBaseUrl, + keyDurationSeconds, + }); + + server.use( + rest.get(`${mockBaseUrl}/.well-known/jwks.json`, async (_, res, ctx) => { + const keys = await factory.listPublicKeys(); + return res(ctx.json(keys)); + }), + ); + }); + + it('verifies token with valid entry', async () => { + const validEntry = { + options: { + url: `${mockBaseUrl}/.well-known/jwks.json`, + algorithm: 'RS256', + issuer: mockBaseUrl, + audience: 'backstage', + }, + }; + const jwksHandler = new JWKSHandler(); + + jwksHandler.add(new ConfigReader(validEntry)); + + const token = await factory.issueToken({ + claims: { sub: mockSubject }, + }); + + const result = await jwksHandler.verifyToken(token); + + expect(result).toEqual({ subject: `external:${mockSubject}` }); + }); + + it('skips invalid entry and continues verification', async () => { + const invalidEntry = { + options: { + url: `${mockBaseUrl}/.well-known/jwks.json`, + algorithm: 'RS256', + issuer: ['fakeIssuer'], + audience: ['fakeAud'], + }, + }; + + const validEntry = { + options: { + url: `${mockBaseUrl}/.well-known/jwks.json`, + algorithm: 'RS256', + issuer: ['multiple-issuers', mockBaseUrl], + audience: ['multiple-audiences', 'backstage'], + }, + }; + const jwksHandler = new JWKSHandler(); + + jwksHandler.add(new ConfigReader(invalidEntry)); + jwksHandler.add(new ConfigReader(validEntry)); + + const token = await factory.issueToken({ + claims: { sub: mockSubject }, + }); + + const result = await jwksHandler.verifyToken(token); + + expect(result).toEqual({ subject: `external:${mockSubject}` }); + }); + + it('returns undefined if no valid entry found', async () => { + const invalidEntry1 = { + options: { + url: `${mockBaseUrl}/.well-known/jwks.json`, + algorithm: 'RS256', + issuer: 'wrong', + }, + }; + + const invalidEntry2 = { + options: { + url: `${mockBaseUrl}/.well-known/jwks.json`, + algorithm: ['HS256'], + audience: 'wrong', + }, + }; + const jwksHandler = new JWKSHandler(); + + jwksHandler.add(new ConfigReader(invalidEntry1)); + jwksHandler.add(new ConfigReader(invalidEntry2)); + + const token = await factory.issueToken({ + claims: { sub: mockSubject }, + }); + + const result = await jwksHandler.verifyToken(token); + + expect(result).toBeUndefined(); + }); + + it('rejects bad config', () => { + const jwksHandler = new JWKSHandler(); + + expect(() => { + jwksHandler.add( + new ConfigReader({ + options: { + url: 'https://exampl e.com/jwks', + }, + }), + ); + }).toThrow('Illegal JWKS URL, must be a set of non-space characters'); + expect(() => { + jwksHandler.add( + new ConfigReader({ + options: { + url: 'https://example.com/jwks\n', + }, + }), + ); + }).toThrow('Illegal JWKS URL, must be a set of non-space characters'); + }); + + it('gracefully handles no added tokens', async () => { + const handler = new JWKSHandler(); + await expect(handler.verifyToken('ghi')).resolves.toBeUndefined(); + }); + + it('uses custom subject prefix if provided', async () => { + const validEntry = { + options: { + url: `${mockBaseUrl}/.well-known/jwks.json`, + algorithm: 'RS256', + issuer: mockBaseUrl, + audience: 'backstage', + subjectPrefix: 'custom-prefix', + }, + }; + const jwksHandler = new JWKSHandler(); + + jwksHandler.add(new ConfigReader(validEntry)); + + const token = await factory.issueToken({ + claims: { sub: mockSubject }, + }); + + const result = await jwksHandler.verifyToken(token); + + expect(result).toEqual({ + subject: `external:${validEntry.options.subjectPrefix}:${mockSubject}`, + }); + }); + + it('carries over access restrictions', async () => { + const jwksHandler = new JWKSHandler(); + jwksHandler.add( + new ConfigReader({ + options: { + url: `${mockBaseUrl}/.well-known/jwks.json`, + }, + accessRestrictions: [{ plugin: 'scaffolder', permission: 'do.it' }], + }), + ); + + const token = await factory.issueToken({ claims: { sub: mockSubject } }); + + await expect(jwksHandler.verifyToken(token)).resolves.toEqual({ + subject: `external:${mockSubject}`, + allAccessRestrictions: new Map( + Object.entries({ + scaffolder: { permissionNames: ['do.it'] }, + }), + ), + }); + }); +}); diff --git a/packages/backend-app-api/src/services/implementations/auth/external/jwks.ts b/packages/backend-app-api/src/services/implementations/auth/external/jwks.ts new file mode 100644 index 0000000000..d88dc62a47 --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/auth/external/jwks.ts @@ -0,0 +1,99 @@ +/* + * 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 { jwtVerify, createRemoteJWKSet, JWTVerifyGetKey } from 'jose'; +import { Config } from '@backstage/config'; +import { + readAccessRestrictionsFromConfig, + readStringOrStringArrayFromConfig, +} from './helpers'; +import { AccessRestriptionsMap, TokenHandler } from './types'; + +/** + * Handles `type: jwks` access. + * + * @internal + */ +export class JWKSHandler implements TokenHandler { + #entries: Array<{ + algorithms?: string[]; + audiences?: string[]; + issuers?: string[]; + subjectPrefix?: string; + url: URL; + jwks: JWTVerifyGetKey; + allAccessRestrictions?: AccessRestriptionsMap; + }> = []; + + add(config: Config) { + if (!config.getString('options.url').match(/^\S+$/)) { + throw new Error( + 'Illegal JWKS URL, must be a set of non-space characters', + ); + } + + const algorithms = readStringOrStringArrayFromConfig( + config, + 'options.algorithm', + ); + const issuers = readStringOrStringArrayFromConfig(config, 'options.issuer'); + const audiences = readStringOrStringArrayFromConfig( + config, + 'options.audience', + ); + const subjectPrefix = config.getOptionalString('options.subjectPrefix'); + const url = new URL(config.getString('options.url')); + const jwks = createRemoteJWKSet(url); + const allAccessRestrictions = readAccessRestrictionsFromConfig(config); + + this.#entries.push({ + algorithms, + audiences, + issuers, + jwks, + subjectPrefix, + url, + allAccessRestrictions, + }); + } + + async verifyToken(token: string) { + for (const entry of this.#entries) { + try { + const { + payload: { sub }, + } = await jwtVerify(token, entry.jwks, { + algorithms: entry.algorithms, + issuer: entry.issuers, + audience: entry.audiences, + }); + + if (sub) { + const prefix = entry.subjectPrefix + ? `external:${entry.subjectPrefix}:` + : 'external:'; + return { + subject: `${prefix}${sub}`, + allAccessRestrictions: entry.allAccessRestrictions, + }; + } + } catch { + continue; + } + } + return undefined; + } +} diff --git a/packages/backend-app-api/src/services/implementations/auth/external/legacy.test.ts b/packages/backend-app-api/src/services/implementations/auth/external/legacy.test.ts index 8b2469f6b7..c674e46ec9 100644 --- a/packages/backend-app-api/src/services/implementations/auth/external/legacy.test.ts +++ b/packages/backend-app-api/src/services/implementations/auth/external/legacy.test.ts @@ -25,17 +25,35 @@ describe('LegacyTokenHandler', () => { const key1 = randomBytes(24); const key2 = randomBytes(24); const key3 = randomBytes(24); + const accessRestrictions1 = new Map( + Object.entries({ + scaffolder: {}, + }), + ); + const accessRestrictions2 = new Map( + Object.entries({ + catalog: { permissionNames: ['catalog.entity.read'] }, + }), + ); tokenHandler.add( new ConfigReader({ - secret: key1.toString('base64'), - subject: 'key1', + options: { + secret: key1.toString('base64'), + subject: 'key1', + }, + accessRestrictions: [{ plugin: 'scaffolder' }], }), ); tokenHandler.add( new ConfigReader({ - secret: key2.toString('base64'), - subject: 'key2', + options: { + secret: key2.toString('base64'), + subject: 'key2', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], }), ); tokenHandler.addOld( @@ -54,6 +72,7 @@ describe('LegacyTokenHandler', () => { await expect(tokenHandler.verifyToken(token1)).resolves.toEqual({ subject: 'key1', + allAccessRestrictions: accessRestrictions1, }); const token2 = await new SignJWT({ @@ -65,6 +84,7 @@ describe('LegacyTokenHandler', () => { await expect(tokenHandler.verifyToken(token2)).resolves.toEqual({ subject: 'key2', + allAccessRestrictions: accessRestrictions2, }); const token3 = await new SignJWT({ @@ -147,39 +167,93 @@ describe('LegacyTokenHandler', () => { // new style add, bad secrets expect(() => - handler.add(new ConfigReader({ _missingsecret: true, subject: 'ok' })), - ).toThrow(/secret/); + handler.add( + new ConfigReader({ options: { _missingsecret: true, subject: 'ok' } }), + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Missing required config value at 'options.secret' in 'mock-config'"`, + ); expect(() => - handler.add(new ConfigReader({ secret: '', subject: 'ok' })), - ).toThrow(/secret/); + handler.add(new ConfigReader({ options: { secret: '', subject: 'ok' } })), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'options.secret' in 'mock-config', got empty-string, wanted string"`, + ); expect(() => - handler.add(new ConfigReader({ secret: 'has spaces', subject: 'ok' })), - ).toThrow(/secret/); + handler.add( + new ConfigReader({ options: { secret: 'has spaces', subject: 'ok' } }), + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Illegal secret, must be a valid base64 string"`, + ); expect(() => - handler.add(new ConfigReader({ secret: 'hasnewline\n', subject: 'ok' })), - ).toThrow(/secret/); + handler.add( + new ConfigReader({ + options: { secret: 'hasnewline\n', subject: 'ok' }, + }), + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Illegal secret, must be a valid base64 string"`, + ); expect(() => - handler.add(new ConfigReader({ secret: 3, subject: 'ok' })), - ).toThrow(/secret/); + handler.add(new ConfigReader({ options: { secret: 3, subject: 'ok' } })), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'options.secret' in 'mock-config', got number, wanted string"`, + ); // new style add, bad subjects expect(() => - handler.add(new ConfigReader({ secret: 'b2s=', _missingsubject: true })), - ).toThrow(/subject/); - expect(() => - handler.add(new ConfigReader({ secret: 'b2s=', subject: '' })), - ).toThrow(/subject/); - expect(() => - handler.add(new ConfigReader({ secret: 'b2s=', subject: 'has spaces' })), - ).toThrow(/subject/); + handler.add( + new ConfigReader({ + options: { secret: 'b2s=', _missingsubject: true }, + }), + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Missing required config value at 'options.subject' in 'mock-config'"`, + ); expect(() => handler.add( - new ConfigReader({ secret: 'b2s=', subject: 'hasnewline\n' }), + new ConfigReader({ options: { secret: 'b2s=', subject: '' } }), ), - ).toThrow(/subject/); + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'options.subject' in 'mock-config', got empty-string, wanted string"`, + ); expect(() => - handler.add(new ConfigReader({ secret: 'b2s=', subject: 3 })), - ).toThrow(/subject/); + handler.add( + new ConfigReader({ + options: { secret: 'b2s=', subject: 'has spaces' }, + }), + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Illegal subject, must be a set of non-space characters"`, + ); + expect(() => + handler.add( + new ConfigReader({ + options: { secret: 'b2s=', subject: 'hasnewline\n' }, + }), + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Illegal subject, must be a set of non-space characters"`, + ); + expect(() => + handler.add( + new ConfigReader({ options: { secret: 'b2s=', subject: 3 } }), + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'options.subject' in 'mock-config', got number, wanted string"`, + ); + + // new style add, bad access restrictions + expect(() => + handler.add( + new ConfigReader({ + options: { secret: 'b2s=', subject: 'subject' }, + accessRestrictions: [{ plugin: ['a'] }], + }), + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'accessRestrictions[0].plugin' in 'mock-config', got array, wanted string"`, + ); // old style add expect(() => @@ -187,18 +261,28 @@ describe('LegacyTokenHandler', () => { ).not.toThrow(); expect(() => handler.addOld(new ConfigReader({ _missingsecret: true })), - ).toThrow(/secret/); - expect(() => handler.addOld(new ConfigReader({ secret: '' }))).toThrow( - /secret/, + ).toThrowErrorMatchingInlineSnapshot( + `"Missing required config value at 'secret' in 'mock-config'"`, + ); + expect(() => + handler.addOld(new ConfigReader({ secret: '' })), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'secret' in 'mock-config', got empty-string, wanted string"`, ); expect(() => handler.addOld(new ConfigReader({ secret: 'has spaces' })), - ).toThrow(/secret/); + ).toThrowErrorMatchingInlineSnapshot( + `"Illegal secret, must be a valid base64 string"`, + ); expect(() => handler.addOld(new ConfigReader({ secret: 'hasnewline\n' })), - ).toThrow(/secret/); - expect(() => handler.addOld(new ConfigReader({ secret: 3 }))).toThrow( - /secret/, + ).toThrowErrorMatchingInlineSnapshot( + `"Illegal secret, must be a valid base64 string"`, + ); + expect(() => + handler.addOld(new ConfigReader({ secret: 3 })), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'secret' in 'mock-config', got number, wanted string"`, ); }); }); diff --git a/packages/backend-app-api/src/services/implementations/auth/external/legacy.ts b/packages/backend-app-api/src/services/implementations/auth/external/legacy.ts index 8448f295a6..9c60e70707 100644 --- a/packages/backend-app-api/src/services/implementations/auth/external/legacy.ts +++ b/packages/backend-app-api/src/services/implementations/auth/external/legacy.ts @@ -16,7 +16,8 @@ import { Config } from '@backstage/config'; import { base64url, decodeJwt, decodeProtectedHeader, jwtVerify } from 'jose'; -import { TokenHandler } from './types'; +import { readAccessRestrictionsFromConfig } from './helpers'; +import { AccessRestriptionsMap, TokenHandler } from './types'; /** * Handles `type: legacy` access. @@ -24,21 +25,38 @@ import { TokenHandler } from './types'; * @internal */ export class LegacyTokenHandler implements TokenHandler { - #entries: Array<{ key: Uint8Array; subject: string }> = []; + #entries = new Array<{ + key: Uint8Array; + result: { + subject: string; + allAccessRestrictions?: AccessRestriptionsMap; + }; + }>(); - add(options: Config) { - this.#doAdd(options.getString('secret'), options.getString('subject')); + add(config: Config) { + const allAccessRestrictions = readAccessRestrictionsFromConfig(config); + this.#doAdd( + config.getString('options.secret'), + config.getString('options.subject'), + allAccessRestrictions, + ); } // used only for the old backend.auth.keys array - addOld(options: Config) { + addOld(config: Config) { // This choice of subject is for compatibility reasons - this.#doAdd(options.getString('secret'), 'external:backstage-plugin'); + this.#doAdd(config.getString('secret'), 'external:backstage-plugin'); } - #doAdd(secret: string, subject: string) { + #doAdd( + secret: string, + subject: string, + allAccessRestrictions?: AccessRestriptionsMap, + ) { if (!secret.match(/^\S+$/)) { throw new Error('Illegal secret, must be a valid base64 string'); + } else if (!subject.match(/^\S+$/)) { + throw new Error('Illegal subject, must be a set of non-space characters'); } let key: Uint8Array; @@ -48,11 +66,19 @@ export class LegacyTokenHandler implements TokenHandler { throw new Error('Illegal secret, must be a valid base64 string'); } - if (!subject.match(/^\S+$/)) { - throw new Error('Illegal subject, must be a set of non-space characters'); + if (this.#entries.some(e => e.key === key)) { + throw new Error( + 'Legacy externalAccess token was declared more than once', + ); } - this.#entries.push({ key, subject }); + this.#entries.push({ + key, + result: { + subject, + allAccessRestrictions, + }, + }); } async verifyToken(token: string) { @@ -76,10 +102,10 @@ export class LegacyTokenHandler implements TokenHandler { return undefined; } - for (const entry of this.#entries) { + for (const { key, result } of this.#entries) { try { - await jwtVerify(token, entry.key); - return { subject: entry.subject }; + await jwtVerify(token, key); + return result; } catch (e) { if (e.code !== 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') { throw e; diff --git a/packages/backend-app-api/src/services/implementations/auth/external/static.test.ts b/packages/backend-app-api/src/services/implementations/auth/external/static.test.ts index 86bdf0bdee..a5945daba9 100644 --- a/packages/backend-app-api/src/services/implementations/auth/external/static.test.ts +++ b/packages/backend-app-api/src/services/implementations/auth/external/static.test.ts @@ -20,14 +20,36 @@ import { StaticTokenHandler } from './static'; describe('StaticTokenHandler', () => { it('accepts any of the added list of tokens', async () => { const handler = new StaticTokenHandler(); - handler.add(new ConfigReader({ token: 'abcabcabc', subject: 'one' })); - handler.add(new ConfigReader({ token: 'defdefdef', subject: 'two' })); + handler.add( + new ConfigReader({ + options: { token: 'abcabcabc', subject: 'one' }, + accessRestrictions: [{ plugin: 'scaffolder' }], + }), + ); + handler.add( + new ConfigReader({ + options: { token: 'defdefdef', subject: 'two' }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }), + ); + const accessRestrictionsOne = new Map(Object.entries({ scaffolder: {} })); + const accessRestrictionsTwo = new Map( + Object.entries({ + catalog: { + permissionNames: ['catalog.entity.read'], + }, + }), + ); await expect(handler.verifyToken('abcabcabc')).resolves.toEqual({ subject: 'one', + allAccessRestrictions: accessRestrictionsOne, }); await expect(handler.verifyToken('defdefdef')).resolves.toEqual({ subject: 'two', + allAccessRestrictions: accessRestrictionsTwo, }); await expect(handler.verifyToken('ghighighi')).resolves.toBeUndefined(); }); @@ -41,71 +63,89 @@ describe('StaticTokenHandler', () => { const handler = new StaticTokenHandler(); expect(() => - handler.add(new ConfigReader({ _missingtoken: true, subject: 'ok' })), + handler.add( + new ConfigReader({ options: { _missingtoken: true, subject: 'ok' } }), + ), ).toThrowErrorMatchingInlineSnapshot( - `"Missing required config value at 'token' in 'mock-config'"`, + `"Missing required config value at 'options.token' in 'mock-config'"`, ); expect(() => - handler.add(new ConfigReader({ token: '', subject: 'ok' })), + handler.add(new ConfigReader({ options: { token: '', subject: 'ok' } })), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'token' in 'mock-config', got empty-string, wanted string"`, + `"Invalid type in config for key 'options.token' in 'mock-config', got empty-string, wanted string"`, ); expect(() => - handler.add(new ConfigReader({ token: 'has spaces', subject: 'ok' })), + handler.add( + new ConfigReader({ options: { token: 'has spaces', subject: 'ok' } }), + ), ).toThrowErrorMatchingInlineSnapshot( `"Illegal token, must be a set of non-space characters"`, ); expect(() => handler.add( new ConfigReader({ - token: 'hasnewlinebutislongenough\n', - subject: 'ok', + options: { + token: 'hasnewlinebutislongenough\n', + subject: 'ok', + }, }), ), ).toThrowErrorMatchingInlineSnapshot( `"Illegal token, must be a set of non-space characters"`, ); expect(() => - handler.add(new ConfigReader({ token: 'short', subject: 'ok' })), + handler.add( + new ConfigReader({ options: { token: 'short', subject: 'ok' } }), + ), ).toThrowErrorMatchingInlineSnapshot( `"Illegal token, must be at least 8 characters length"`, ); expect(() => - handler.add(new ConfigReader({ token: 3, subject: 'ok' })), + handler.add(new ConfigReader({ options: { token: 3, subject: 'ok' } })), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'token' in 'mock-config', got number, wanted string"`, + `"Invalid type in config for key 'options.token' in 'mock-config', got number, wanted string"`, ); expect(() => handler.add( - new ConfigReader({ token: 'validtoken', _missingsubject: true }), + new ConfigReader({ + options: { token: 'validtoken', _missingsubject: true }, + }), ), ).toThrowErrorMatchingInlineSnapshot( - `"Missing required config value at 'subject' in 'mock-config'"`, - ); - expect(() => - handler.add(new ConfigReader({ token: 'validtoken', subject: '' })), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'subject' in 'mock-config', got empty-string, wanted string"`, + `"Missing required config value at 'options.subject' in 'mock-config'"`, ); expect(() => handler.add( - new ConfigReader({ token: 'validtoken', subject: 'has spaces' }), + new ConfigReader({ options: { token: 'validtoken', subject: '' } }), + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'options.subject' in 'mock-config', got empty-string, wanted string"`, + ); + expect(() => + handler.add( + new ConfigReader({ + options: { token: 'validtoken', subject: 'has spaces' }, + }), ), ).toThrowErrorMatchingInlineSnapshot( `"Illegal subject, must be a set of non-space characters"`, ); expect(() => handler.add( - new ConfigReader({ token: 'validtoken', subject: 'hasnewline\n' }), + new ConfigReader({ + options: { token: 'validtoken', subject: 'hasnewline\n' }, + }), ), ).toThrowErrorMatchingInlineSnapshot( `"Illegal subject, must be a set of non-space characters"`, ); expect(() => - handler.add(new ConfigReader({ token: 'validtoken', subject: 3 })), + handler.add( + new ConfigReader({ options: { token: 'validtoken', subject: 3 } }), + ), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'subject' in 'mock-config', got number, wanted string"`, + `"Invalid type in config for key 'options.subject' in 'mock-config', got number, wanted string"`, ); }); }); diff --git a/packages/backend-app-api/src/services/implementations/auth/external/static.ts b/packages/backend-app-api/src/services/implementations/auth/external/static.ts index bae8e05f2b..1e89d8c6a1 100644 --- a/packages/backend-app-api/src/services/implementations/auth/external/static.ts +++ b/packages/backend-app-api/src/services/implementations/auth/external/static.ts @@ -15,7 +15,8 @@ */ import { Config } from '@backstage/config'; -import { TokenHandler } from './types'; +import { readAccessRestrictionsFromConfig } from './helpers'; +import { AccessRestriptionsMap, TokenHandler } from './types'; const MIN_TOKEN_LENGTH = 8; @@ -25,33 +26,37 @@ const MIN_TOKEN_LENGTH = 8; * @internal */ export class StaticTokenHandler implements TokenHandler { - #entries: Array<{ token: string; subject: string }> = []; + #entries = new Map< + string, + { + subject: string; + allAccessRestrictions?: AccessRestriptionsMap; + } + >(); + + add(config: Config) { + const token = config.getString('options.token'); + const subject = config.getString('options.subject'); + const allAccessRestrictions = readAccessRestrictionsFromConfig(config); - add(options: Config) { - const token = options.getString('token'); if (!token.match(/^\S+$/)) { throw new Error('Illegal token, must be a set of non-space characters'); - } - if (token.length < MIN_TOKEN_LENGTH) { + } else if (token.length < MIN_TOKEN_LENGTH) { throw new Error( `Illegal token, must be at least ${MIN_TOKEN_LENGTH} characters length`, ); - } - - const subject = options.getString('subject'); - if (!subject.match(/^\S+$/)) { + } else if (!subject.match(/^\S+$/)) { throw new Error('Illegal subject, must be a set of non-space characters'); + } else if (this.#entries.has(token)) { + throw new Error( + 'Static externalAccess token was declared more than once', + ); } - this.#entries.push({ token, subject }); + this.#entries.set(token, { subject, allAccessRestrictions }); } async verifyToken(token: string) { - const entry = this.#entries.find(e => e.token === token); - if (!entry) { - return undefined; - } - - return { subject: entry.subject }; + return this.#entries.get(token); } } diff --git a/packages/backend-app-api/src/services/implementations/auth/external/types.ts b/packages/backend-app-api/src/services/implementations/auth/external/types.ts index 5d33f09a22..6a1fd084ed 100644 --- a/packages/backend-app-api/src/services/implementations/auth/external/types.ts +++ b/packages/backend-app-api/src/services/implementations/auth/external/types.ts @@ -14,9 +14,21 @@ * limitations under the License. */ +import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; +export type AccessRestriptionsMap = Map< + string, // plugin ID + BackstagePrincipalAccessRestrictions +>; + export interface TokenHandler { add(options: Config): void; - verifyToken(token: string): Promise<{ subject: string } | undefined>; + verifyToken(token: string): Promise< + | { + subject: string; + allAccessRestrictions?: AccessRestriptionsMap; + } + | undefined + >; } diff --git a/packages/backend-app-api/src/services/implementations/auth/helpers.ts b/packages/backend-app-api/src/services/implementations/auth/helpers.ts index 01fb9537a1..eebe45eb76 100644 --- a/packages/backend-app-api/src/services/implementations/auth/helpers.ts +++ b/packages/backend-app-api/src/services/implementations/auth/helpers.ts @@ -17,6 +17,7 @@ import { BackstageCredentials, BackstageNonePrincipal, + BackstagePrincipalAccessRestrictions, BackstageServicePrincipal, BackstageUserPrincipal, } from '@backstage/backend-plugin-api'; @@ -25,6 +26,7 @@ import { InternalBackstageCredentials } from './types'; export function createCredentialsWithServicePrincipal( sub: string, token?: string, + accessRestrictions?: BackstagePrincipalAccessRestrictions, ): InternalBackstageCredentials { return { $$type: '@backstage/BackstageCredentials', @@ -33,6 +35,7 @@ export function createCredentialsWithServicePrincipal( principal: { type: 'service', subject: sub, + accessRestrictions, }, }; } diff --git a/packages/backend-app-api/src/services/implementations/cache/cacheServiceFactory.ts b/packages/backend-app-api/src/services/implementations/cache/cacheServiceFactory.ts index b91356a13a..9b5e5ab16a 100644 --- a/packages/backend-app-api/src/services/implementations/cache/cacheServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/cache/cacheServiceFactory.ts @@ -20,15 +20,19 @@ import { createServiceFactory, } from '@backstage/backend-plugin-api'; -/** @public */ +/** + * @public + * @deprecated Please import from `@backstage/backend-defaults/cache` instead. + */ export const cacheServiceFactory = createServiceFactory({ service: coreServices.cache, deps: { config: coreServices.rootConfig, + logger: coreServices.rootLogger, plugin: coreServices.pluginMetadata, }, - async createRootContext({ config }) { - return CacheManager.fromConfig(config); + async createRootContext({ config, logger }) { + return CacheManager.fromConfig(config, { logger }); }, async factory({ plugin }, manager) { return manager.forPlugin(plugin.getId()).getClient(); diff --git a/packages/backend-app-api/src/services/implementations/config/rootConfigServiceFactory.ts b/packages/backend-app-api/src/services/implementations/config/rootConfigServiceFactory.ts index 68103842bb..c74474e629 100644 --- a/packages/backend-app-api/src/services/implementations/config/rootConfigServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/config/rootConfigServiceFactory.ts @@ -23,7 +23,10 @@ import { RemoteConfigSourceOptions, } from '@backstage/config-loader'; -/** @public */ +/** + * @public + * @deprecated Please import from `@backstage/backend-defaults/rootConfig` instead. + */ export interface RootConfigFactoryOptions { /** * Process arguments to use instead of the default `process.argv()`. @@ -37,7 +40,10 @@ export interface RootConfigFactoryOptions { watch?: boolean; } -/** @public */ +/** + * @public + * @deprecated Please import from `@backstage/backend-defaults/rootConfig` instead. + */ export const rootConfigServiceFactory = createServiceFactory( (options?: RootConfigFactoryOptions) => ({ service: coreServices.rootConfig, diff --git a/packages/backend-app-api/src/services/implementations/database/databaseServiceFactory.ts b/packages/backend-app-api/src/services/implementations/database/databaseServiceFactory.ts index 139609b6c1..972d8dd4ec 100644 --- a/packages/backend-app-api/src/services/implementations/database/databaseServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/database/databaseServiceFactory.ts @@ -21,7 +21,10 @@ import { } from '@backstage/backend-plugin-api'; import { ConfigReader } from '@backstage/config'; -/** @public */ +/** + * @public + * @deprecated Please import from `@backstage/backend-defaults/database` instead. + */ export const databaseServiceFactory = createServiceFactory({ service: coreServices.database, deps: { diff --git a/packages/backend-app-api/src/services/implementations/deprecated.ts b/packages/backend-app-api/src/services/implementations/deprecated.ts new file mode 100644 index 0000000000..6f4f76e679 --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/deprecated.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './scheduler'; diff --git a/packages/backend-app-api/src/services/implementations/discovery/HostDiscovery.ts b/packages/backend-app-api/src/services/implementations/discovery/HostDiscovery.ts index 180c8706d5..d337da997a 100644 --- a/packages/backend-app-api/src/services/implementations/discovery/HostDiscovery.ts +++ b/packages/backend-app-api/src/services/implementations/discovery/HostDiscovery.ts @@ -29,6 +29,7 @@ type Target = string | { internal: string; external: string }; * resolved to the same host, so there won't be any balancing of internal traffic. * * @public + * @deprecated Please import from `@backstage/backend-defaults/discovery` instead. */ export class HostDiscovery implements DiscoveryService { /** diff --git a/packages/backend-app-api/src/services/implementations/discovery/discoveryServiceFactory.ts b/packages/backend-app-api/src/services/implementations/discovery/discoveryServiceFactory.ts index bfc5a6a489..b29a589438 100644 --- a/packages/backend-app-api/src/services/implementations/discovery/discoveryServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/discovery/discoveryServiceFactory.ts @@ -20,7 +20,10 @@ import { } from '@backstage/backend-plugin-api'; import { HostDiscovery } from './HostDiscovery'; -/** @public */ +/** + * @public + * @deprecated Please import from `@backstage/backend-defaults/discovery` instead. + */ export const discoveryServiceFactory = createServiceFactory({ service: coreServices.discovery, deps: { diff --git a/packages/backend-app-api/src/services/implementations/identity/identityServiceFactory.ts b/packages/backend-app-api/src/services/implementations/identity/identityServiceFactory.ts index 58da1c37d5..c2f1a61007 100644 --- a/packages/backend-app-api/src/services/implementations/identity/identityServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/identity/identityServiceFactory.ts @@ -24,16 +24,22 @@ import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; * An identity client options object which allows extra configurations * * @public + * @deprecated Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead */ export type IdentityFactoryOptions = { issuer?: string; - /** JWS "alg" (Algorithm) Header Parameter values. Defaults to an array containing just ES256. - * More info on supported algorithms: https://github.com/panva/jose */ + /** + * JWS "alg" (Algorithm) Header Parameter values. Defaults to an array containing just ES256. + * More info on supported algorithms: https://github.com/panva/jose + */ algorithms?: string[]; }; -/** @public */ +/** + * @public + * @deprecated Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead + */ export const identityServiceFactory = createServiceFactory( (options?: IdentityFactoryOptions) => ({ service: coreServices.identity, diff --git a/packages/backend-app-api/src/services/implementations/index.ts b/packages/backend-app-api/src/services/implementations/index.ts index a1114ab3e3..e6656801a9 100644 --- a/packages/backend-app-api/src/services/implementations/index.ts +++ b/packages/backend-app-api/src/services/implementations/index.ts @@ -28,7 +28,8 @@ export * from './permissions'; export * from './rootHttpRouter'; export * from './rootLifecycle'; export * from './rootLogger'; -export * from './scheduler'; export * from './tokenManager'; export * from './urlReader'; export * from './userInfo'; + +export * from './deprecated'; diff --git a/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleServiceFactory.ts b/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleServiceFactory.ts index 2b68a81807..b3b0135a7c 100644 --- a/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleServiceFactory.ts @@ -26,7 +26,10 @@ import { createServiceFactory, } from '@backstage/backend-plugin-api'; -/** @internal */ +/** + * @internal + * @deprecated + */ export class BackendPluginLifecycleImpl implements LifecycleService { constructor( private readonly logger: LoggerService, @@ -85,7 +88,9 @@ export class BackendPluginLifecycleImpl implements LifecycleService { /** * Allows plugins to register shutdown hooks that are run when the process is about to exit. + * * @public + * @deprecated Please import from `@backstage/backend-defaults/lifecycle` instead. */ export const lifecycleServiceFactory = createServiceFactory({ service: coreServices.lifecycle, diff --git a/packages/backend-app-api/src/services/implementations/permissions/permissionsServiceFactory.ts b/packages/backend-app-api/src/services/implementations/permissions/permissionsServiceFactory.ts index 9824eb2145..c8fa0e7bbf 100644 --- a/packages/backend-app-api/src/services/implementations/permissions/permissionsServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/permissions/permissionsServiceFactory.ts @@ -20,7 +20,10 @@ import { } from '@backstage/backend-plugin-api'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; -/** @public */ +/** + * @public + * @deprecated Please import from `@backstage/backend-defaults/permissions` instead. + */ export const permissionsServiceFactory = createServiceFactory({ service: coreServices.permissions, deps: { diff --git a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.test.ts index 91cb9031ae..5992e38d50 100644 --- a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.test.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { BackendLifecycleImpl } from './rootLifecycleServiceFactory'; +import { mockServices } from '@backstage/backend-test-utils'; describe('lifecycleService', () => { it('should execute registered shutdown hook', async () => { - const service = new BackendLifecycleImpl(getVoidLogger()); + const service = new BackendLifecycleImpl(mockServices.logger.mock()); const hook = jest.fn(); service.addShutdownHook(() => hook()); // should not execute the hook more than once. @@ -30,7 +30,7 @@ describe('lifecycleService', () => { }); it('should not throw errors', async () => { - const service = new BackendLifecycleImpl(getVoidLogger()); + const service = new BackendLifecycleImpl(mockServices.logger.mock()); service.addShutdownHook(() => { throw new Error('oh no'); }); @@ -38,7 +38,7 @@ describe('lifecycleService', () => { }); it('should not throw async errors', async () => { - const service = new BackendLifecycleImpl(getVoidLogger()); + const service = new BackendLifecycleImpl(mockServices.logger.mock()); service.addShutdownHook(async () => { throw new Error('oh no'); }); @@ -46,7 +46,7 @@ describe('lifecycleService', () => { }); it('should reject hooks after trigger', async () => { - const service = new BackendLifecycleImpl(getVoidLogger()); + const service = new BackendLifecycleImpl(mockServices.logger.mock()); await service.startup(); expect(() => { service.addStartupHook(() => {}); diff --git a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.ts b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.ts index 197f99b97a..bf5dc09b80 100644 --- a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.ts @@ -25,7 +25,10 @@ import { LoggerService, } from '@backstage/backend-plugin-api'; -/** @internal */ +/** + * @internal + * @deprecated + */ export class BackendLifecycleImpl implements RootLifecycleService { constructor(private readonly logger: LoggerService) {} @@ -108,6 +111,7 @@ export class BackendLifecycleImpl implements RootLifecycleService { * Allows plugins to register shutdown hooks that are run when the process is about to exit. * * @public + * @deprecated Please import from `@backstage/backend-defaults/rootLifecycle` instead. */ export const rootLifecycleServiceFactory = createServiceFactory({ service: coreServices.rootLifecycle, diff --git a/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.ts b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.ts index dc9b3a9864..b4370761d9 100644 --- a/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.ts @@ -20,7 +20,10 @@ import { } from '@backstage/backend-plugin-api'; import { TaskScheduler } from '@backstage/backend-tasks'; -/** @public */ +/** + * @public + * @deprecated Please import from `@backstage/backend-defaults/scheduler` instead. + */ export const schedulerServiceFactory = createServiceFactory({ service: coreServices.scheduler, deps: { diff --git a/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.ts b/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.ts index cad1b06035..e7c4ce7af0 100644 --- a/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.ts @@ -20,7 +20,10 @@ import { } from '@backstage/backend-plugin-api'; import { ServerTokenManager } from '@backstage/backend-common'; -/** @public */ +/** + * @public + * @deprecated Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead + */ export const tokenManagerServiceFactory = createServiceFactory({ service: coreServices.tokenManager, deps: { diff --git a/packages/backend-app-api/src/services/implementations/urlReader/urlReaderServiceFactory.ts b/packages/backend-app-api/src/services/implementations/urlReader/urlReaderServiceFactory.ts index 7f404a24b1..44caf25ad6 100644 --- a/packages/backend-app-api/src/services/implementations/urlReader/urlReaderServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/urlReader/urlReaderServiceFactory.ts @@ -20,7 +20,10 @@ import { createServiceFactory, } from '@backstage/backend-plugin-api'; -/** @public */ +/** + * @public + * @deprecated Please import from `@backstage/backend-defaults/urlReader` instead. + */ export const urlReaderServiceFactory = createServiceFactory({ service: coreServices.urlReader, deps: { diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 1bf38a8c40..ddad94de14 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -20,9 +20,9 @@ import { coreServices, ServiceRef, ServiceFactory, + LifecycleService, + RootLifecycleService, } from '@backstage/backend-plugin-api'; -import { BackendLifecycleImpl } from '../services/implementations/rootLifecycle/rootLifecycleServiceFactory'; -import { BackendPluginLifecycleImpl } from '../services/implementations/lifecycle/lifecycleServiceFactory'; import { ServiceOrExtensionPoint } from './types'; // Direct internal import to avoid duplication // eslint-disable-next-line @backstage/no-forbidden-package-imports @@ -345,27 +345,42 @@ export class BackendInitializer { } // Bit of a hacky way to grab the lifecycle services, potentially find a nicer way to do this - async #getRootLifecycleImpl(): Promise { + async #getRootLifecycleImpl(): Promise< + RootLifecycleService & { + startup(): Promise; + shutdown(): Promise; + } + > { const lifecycleService = await this.#serviceRegistry.get( coreServices.rootLifecycle, 'root', ); - if (lifecycleService instanceof BackendLifecycleImpl) { - return lifecycleService; + + const service = lifecycleService as any; + if ( + service && + typeof service.startup === 'function' && + typeof service.shutdown === 'function' + ) { + return service; } + throw new Error('Unexpected root lifecycle service implementation'); } async #getPluginLifecycleImpl( pluginId: string, - ): Promise { + ): Promise }> { const lifecycleService = await this.#serviceRegistry.get( coreServices.lifecycle, pluginId, ); - if (lifecycleService instanceof BackendPluginLifecycleImpl) { - return lifecycleService; + + const service = lifecycleService as any; + if (service && typeof service.startup === 'function') { + return service; } + throw new Error('Unexpected plugin lifecycle service implementation'); } } diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 70136051d0..ecf56c5f28 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,63 @@ # @backstage/backend-common +## 0.23.0-next.2 + +### Minor Changes + +- 395b973: Implemented `readTree` for Harness provider to support TechDocs functionality + +### Patch Changes + +- 0634fdc: Deprecated `dropDatabase` +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/backend-app-api@0.7.6-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/config-loader@1.8.0 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + +## 0.23.0-next.1 + +### Minor Changes + +- 02103be: Deprecated and moved over core services to `@backstage/backend-defaults` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/config-loader@1.8.0 + - @backstage/plugin-auth-node@0.4.14-next.1 + +## 0.22.1-next.0 + +### Patch Changes + +- c6c0919: Updated configuration schema to include the `useRedisSets` cache config option. +- 1779188: In preparation to the new backend system stable release, the `isDatabaseConflictError` helper have been moved to the `@backstage/backend-plugin-api` package and deprecated from `@backstage/backend-common`. +- 8869b8e: We are deprecating the legacy `createServiceBuilder` factory, so if you are still using it, please checkout the migration guide and [migrate](https://backstage.io/docs/backend-system/building-plugins-and-modules/migrating) your plugin to use the new backend system. +- 3bd04bb: We are deprecating the legacy router handlers and contexts in preparation for the new backend system stable release. +- 6a576dc: Deprecate legacy service logger helpers and stop using `getVoidLogger` in tests. +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + ## 0.22.0 ### Minor Changes diff --git a/packages/backend-common/api-report-alpha.md b/packages/backend-common/api-report-alpha.md index 9d60b68b6b..046e8264fb 100644 --- a/packages/backend-common/api-report-alpha.md +++ b/packages/backend-common/api-report-alpha.md @@ -5,14 +5,14 @@ ```ts import { Duration } from 'luxon'; -// @alpha +// @alpha @deprecated export interface Context { readonly abortSignal: AbortSignal; readonly deadline: Date | undefined; value(key: string): T | undefined; } -// @alpha +// @alpha @deprecated export class Contexts { static root(): Context; static withAbort( diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 42df32dc1d..2947741fe3 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -17,11 +17,12 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; import { BitbucketCloudIntegration } from '@backstage/integration'; import { BitbucketIntegration } from '@backstage/integration'; import { BitbucketServerIntegration } from '@backstage/integration'; -import { CacheService as CacheClient } from '@backstage/backend-plugin-api'; -import { CacheServiceOptions as CacheClientOptions } from '@backstage/backend-plugin-api'; -import { CacheServiceSetOptions as CacheClientSetOptions } from '@backstage/backend-plugin-api'; +import { CacheService } from '@backstage/backend-plugin-api'; +import { CacheServiceOptions } from '@backstage/backend-plugin-api'; +import type { CacheServiceSetOptions } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import cors from 'cors'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; import Docker from 'dockerode'; import { ErrorRequestHandler } from 'express'; import express from 'express'; @@ -35,6 +36,7 @@ import { HostDiscovery as HostDiscovery_2 } from '@backstage/backend-app-api'; import { HttpAuthService } from '@backstage/backend-plugin-api'; import { IdentityService } from '@backstage/backend-plugin-api'; import { isChildPath as isChildPath_2 } from '@backstage/backend-plugin-api'; +import { isDatabaseConflictError as isDatabaseConflictError_2 } from '@backstage/backend-plugin-api'; import { KubeConfig } from '@kubernetes/client-node'; import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoadConfigOptionsRemote } from '@backstage/config-loader'; @@ -43,7 +45,6 @@ import { LoggerService } from '@backstage/backend-plugin-api'; import { MergeResult } from 'isomorphic-git'; import { PermissionsService } from '@backstage/backend-plugin-api'; import { DatabaseService as PluginDatabaseManager } from '@backstage/backend-plugin-api'; -import { DiscoveryService as PluginEndpointDiscovery } from '@backstage/backend-plugin-api'; import { PluginMetadataService } from '@backstage/backend-plugin-api'; import { PushResult } from 'isomorphic-git'; import { Readable } from 'stream'; @@ -192,18 +193,26 @@ export class BitbucketUrlReader implements UrlReader { toString(): string; } -export { CacheClient }; +// @public @deprecated (undocumented) +export type CacheClient = CacheService; -export { CacheClientOptions }; +// @public @deprecated (undocumented) +export type CacheClientOptions = CacheServiceOptions; -export { CacheClientSetOptions }; +// @public @deprecated (undocumented) +export type CacheClientSetOptions = CacheServiceSetOptions; // @public export class CacheManager { - forPlugin(pluginId: string): PluginCacheManager; + forPlugin(pluginId: string): { + getClient(options?: CacheServiceOptions): CacheService; + }; static fromConfig( config: Config, - options?: CacheManagerOptions, + options?: { + logger?: LoggerService; + onError?: (err: Error) => void; + }, ): CacheManager; } @@ -213,12 +222,12 @@ export type CacheManagerOptions = { onError?: (err: Error) => void; }; -// @public (undocumented) -export function cacheToPluginCacheManager( - cache: CacheClient, -): PluginCacheManager; - // @public +export function cacheToPluginCacheManager(cache: CacheService): { + getClient(options?: CacheServiceOptions): CacheService; +}; + +// @public @deprecated export const coloredFormat: winston.Logform.Format; // @public @@ -259,13 +268,13 @@ export function createLegacyAuthAdapters< : {}), >(options: TOptions): TAdapters; -// @public +// @public @deprecated export function createRootLogger( options?: winston.LoggerOptions, env?: NodeJS.ProcessEnv, ): winston.Logger; -// @public +// @public @deprecated export function createServiceBuilder(_module: NodeModule): ServiceBuilder; // @public @@ -303,13 +312,13 @@ export class DockerContainerRunner implements ContainerRunner { runContainer(options: RunContainerOptions): Promise; } -// @public +// @public @deprecated export function dropDatabase( dbConfig: Config, ...databaseNames: string[] ): Promise; -// @public +// @public @deprecated export function errorHandler( options?: ErrorHandlerOptions, ): ErrorRequestHandler; @@ -366,10 +375,10 @@ export class GerritUrlReader implements UrlReader { toString(): string; } -// @public +// @public @deprecated export function getRootLogger(): winston.Logger; -// @public +// @public @deprecated export function getVoidLogger(): winston.Logger; // @public @deprecated @@ -517,13 +526,18 @@ export class GitlabUrlReader implements UrlReader { // @public export class HarnessUrlReader implements UrlReader { - constructor(integration: HarnessIntegration); + constructor( + integration: HarnessIntegration, + deps: { + treeResponseFactory: ReadTreeResponseFactory; + }, + ); // (undocumented) static factory: ReaderFactory; // (undocumented) read(url: string): Promise; // (undocumented) - readTree(): Promise; + readTree(url: string, options?: ReadTreeOptions): Promise; // (undocumented) readUrl(url: string, options?: ReadUrlOptions): Promise; // (undocumented) @@ -532,14 +546,14 @@ export class HarnessUrlReader implements UrlReader { toString(): string; } -// @public +// @public @deprecated export const HostDiscovery: typeof HostDiscovery_2; // @public @deprecated (undocumented) export const isChildPath: typeof isChildPath_2; -// @public -export function isDatabaseConflictError(e: unknown): boolean; +// @public @deprecated (undocumented) +export const isDatabaseConflictError: typeof isDatabaseConflictError_2; // @public export class KubernetesContainerRunner implements ContainerRunner { @@ -574,10 +588,10 @@ export const legacyPlugin: ( default: LegacyCreateRouter< TransformedEnv< { - cache: CacheClient; + cache: CacheService; config: RootConfigService; database: PluginDatabaseManager; - discovery: PluginEndpointDiscovery; + discovery: DiscoveryService; logger: LoggerService; permissions: PermissionsService; scheduler: SchedulerService; @@ -587,7 +601,9 @@ export const legacyPlugin: ( }, { logger: (log: LoggerService) => Logger; - cache: (cache: CacheClient) => PluginCacheManager; + cache: (cache: CacheService) => { + getClient(options?: CacheServiceOptions | undefined): CacheService; + }; } > >; @@ -599,7 +615,7 @@ export type LegacyRootDatabaseService = { forPlugin(pluginId: string): PluginDatabaseManager; }; -// @public +// @public @deprecated export function loadBackendConfig(options: { logger: LoggerService; remote?: LoadConfigOptionsRemote; @@ -638,12 +654,13 @@ export function notFoundHandler(): RequestHandler; // @public (undocumented) export interface PluginCacheManager { // (undocumented) - getClient(options?: CacheClientOptions): CacheClient; + getClient(options?: CacheServiceOptions): CacheService; } export { PluginDatabaseManager }; -export { PluginEndpointDiscovery }; +// @public @deprecated (undocumented) +export type PluginEndpointDiscovery = DiscoveryService; // @public export interface PullOptions { @@ -733,7 +750,7 @@ export function redactWinstonLogLine( info: winston.Logform.TransformableInfo, ): winston.Logform.TransformableInfo; -// @public +// @public @deprecated export function requestLoggingHandler(logger?: LoggerService): RequestHandler; // @public @@ -815,7 +832,7 @@ export type ServiceBuilder = { start(): Promise; }; -// @public +// @public @deprecated export function setRootLogger(newLogger: winston.Logger): void; // @public @deprecated diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index c6263cde97..ed253dd54d 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -191,6 +191,11 @@ export interface Config { connection: string; /** An optional default TTL (in milliseconds). */ defaultTtl?: number; + /** + * Whether or not [useRedisSets](https://github.com/jaredwray/keyv/tree/main/packages/redis#useredissets) should be configured to this redis cache. + * Defaults to true if unspecified. + */ + useRedisSets?: boolean; } | { store: 'memcache'; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 8e0d388352..e5b5806ddf 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-common", - "version": "0.22.0", + "version": "0.23.0-next.2", "description": "Common functionality library for Backstage backends", "backstage": { "role": "node-library" diff --git a/packages/backend-common/src/cache/CacheManager.test.ts b/packages/backend-common/src/cache/CacheManager.test.ts deleted file mode 100644 index 4b7bb05efd..0000000000 --- a/packages/backend-common/src/cache/CacheManager.test.ts +++ /dev/null @@ -1,384 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ConfigReader } from '@backstage/config'; -import Keyv from 'keyv'; -import KeyvMemcache from '@keyv/memcache'; -import KeyvRedis from '@keyv/redis'; -import { DefaultCacheClient } from './CacheClient'; -import { CacheManager } from './CacheManager'; - -jest.createMockFromModule('keyv'); -jest.mock('keyv'); -jest.createMockFromModule('@keyv/memcache'); -jest.mock('@keyv/memcache'); -jest.createMockFromModule('@keyv/redis'); -jest.mock('@keyv/redis'); -jest.mock('./CacheClient', () => { - return { - DefaultCacheClient: jest.fn(), - }; -}); - -const globalDefaultTtl = 1234; -describe('CacheManager', () => { - const defaultConfigOptions = { - backend: { - cache: { - store: 'memory', - defaultTtl: globalDefaultTtl, - }, - }, - }; - const defaultConfig = () => new ConfigReader(defaultConfigOptions); - - afterEach(() => jest.resetAllMocks()); - - describe('CacheManager.fromConfig', () => { - it('accesses the backend.cache key', () => { - const getOptionalString = jest.fn(); - const getOptionalBoolean = jest.fn(); - const getOptionalNumber = jest.fn(); - const config = defaultConfig(); - config.getOptionalString = getOptionalString; - config.getOptionalBoolean = getOptionalBoolean; - config.getOptionalNumber = getOptionalNumber; - - CacheManager.fromConfig(config); - - expect(getOptionalString.mock.calls[0][0]).toEqual('backend.cache.store'); - expect(getOptionalString.mock.calls[1][0]).toEqual( - 'backend.cache.connection', - ); - expect(getOptionalBoolean.mock.calls[0][0]).toEqual( - 'backend.cache.useRedisSets', - ); - expect(getOptionalNumber.mock.calls[0][0]).toEqual( - 'backend.cache.defaultTtl', - ); - }); - - it('does not require the backend.cache key', () => { - const config = new ConfigReader({ backend: {} }); - expect(() => { - CacheManager.fromConfig(config); - }).not.toThrow(); - }); - - it('throws on unknown cache store', () => { - const config = new ConfigReader({ - backend: { cache: { store: 'notreal' } }, - }); - expect(() => { - CacheManager.fromConfig(config); - }).toThrow(); - }); - }); - - describe('CacheManager.forPlugin', () => { - const manager = CacheManager.fromConfig(defaultConfig()); - - it('connects to a cache store scoped to the plugin', async () => { - const pluginId = 'test1'; - manager.forPlugin(pluginId).getClient(); - - const client = DefaultCacheClient as jest.Mock; - expect(client).toHaveBeenCalledTimes(1); - }); - - it('attaches error handler to client', () => { - const pluginId = 'error-test'; - manager.forPlugin(pluginId).getClient(); - - const client = DefaultCacheClient as jest.Mock; - const mockCalls = client.mock.calls.splice(-1); - const realClient = mockCalls[0][0] as Keyv; - expect(realClient.on).toHaveBeenCalledWith('error', expect.any(Function)); - }); - - it('provides different plugins different cache clients', async () => { - const plugin1Id = 'test1'; - const plugin2Id = 'test2'; - const expectedTtl = 3600; - manager.forPlugin(plugin1Id).getClient({ defaultTtl: expectedTtl }); - manager.forPlugin(plugin2Id).getClient({ defaultTtl: expectedTtl }); - - const client = DefaultCacheClient as jest.Mock; - const cache = Keyv as unknown as jest.Mock; - expect(cache).toHaveBeenCalledTimes(2); - expect(client).toHaveBeenCalledTimes(2); - - const plugin1CallArgs = cache.mock.calls[0]; - const plugin2CallArgs = cache.mock.calls[1]; - expect(plugin1CallArgs[0].namespace).not.toEqual( - plugin2CallArgs[0].namespace, - ); - }); - }); - - describe('CacheManager.forPlugin stores', () => { - it('returns memory client when no cache is configured', () => { - const manager = CacheManager.fromConfig( - new ConfigReader({ backend: {} }), - ); - const expectedTtl = 3600; - const expectedNamespace = 'test-plugin'; - manager - .forPlugin(expectedNamespace) - .getClient({ defaultTtl: expectedTtl }); - - const cache = Keyv as unknown as jest.Mock; - const mockCalls = cache.mock.calls.splice(-1); - const callArgs = mockCalls[0]; - expect(callArgs[0]).toMatchObject({ - ttl: expectedTtl, - namespace: expectedNamespace, - }); - }); - - it('returns memory client when explicitly configured', () => { - const manager = CacheManager.fromConfig(defaultConfig()); - const expectedTtl = 3600; - const expectedNamespace = 'test-plugin'; - manager - .forPlugin(expectedNamespace) - .getClient({ defaultTtl: expectedTtl }); - - const cache = Keyv as unknown as jest.Mock; - const mockCalls = cache.mock.calls.splice(-1); - const callArgs = mockCalls[0]; - expect(callArgs[0]).toMatchObject({ - ttl: expectedTtl, - namespace: expectedNamespace, - }); - }); - - it('returns memory client with a global defaultTtl when explicitly configured', () => { - const manager = CacheManager.fromConfig(defaultConfig()); - const expectedNamespace = 'test-plugin'; - manager.forPlugin(expectedNamespace).getClient(); - - const cache = Keyv as unknown as jest.Mock; - const mockCalls = cache.mock.calls.splice(-1); - const callArgs = mockCalls[0]; - expect(callArgs[0]).toMatchObject({ - ttl: globalDefaultTtl, - namespace: expectedNamespace, - }); - }); - - it('shares memory across multiple instances of the memory client', () => { - const manager = CacheManager.fromConfig(defaultConfig()); - const plugin = 'test-plugin'; - - // Instantiate two in-memory clients. - manager.forPlugin(plugin).getClient({ defaultTtl: 10 }); - manager.forPlugin(plugin).getClient({ defaultTtl: 10 }); - - const cache = Keyv as unknown as jest.Mock; - const mockCall2 = cache.mock.calls.splice(-1)[0][0]; - const mockCall1 = cache.mock.calls.splice(-1)[0][0]; - - // Note: .toBe() checks referential identity of object instances. - expect(mockCall1.store).toBe(mockCall2.store); - }); - - it('returns a memcache client when configured', () => { - const expectedHost = '127.0.0.1:11211'; - const manager = CacheManager.fromConfig( - new ConfigReader({ - backend: { - cache: { - store: 'memcache', - connection: expectedHost, - }, - }, - }), - ); - const expectedTtl = 3600; - manager.forPlugin('test').getClient({ defaultTtl: expectedTtl }); - - const cache = Keyv as unknown as jest.Mock; - const mockCacheCalls = cache.mock.calls.splice(-1); - expect(mockCacheCalls[0][0]).toMatchObject({ - ttl: expectedTtl, - }); - expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvMemcache); - const memcache = KeyvMemcache as unknown as jest.Mock; - const mockMemcacheCalls = memcache.mock.calls.splice(-1); - expect(mockMemcacheCalls[0][0]).toEqual(expectedHost); - }); - - it('returns a memcache client with a global defaultTtl when configured', () => { - const expectedHost = '127.0.0.1:11211'; - const manager = CacheManager.fromConfig( - new ConfigReader({ - backend: { - cache: { - store: 'memcache', - connection: expectedHost, - defaultTtl: globalDefaultTtl, - }, - }, - }), - ); - manager.forPlugin('test').getClient(); - - const cache = Keyv as unknown as jest.Mock; - const mockCacheCalls = cache.mock.calls.splice(-1); - expect(mockCacheCalls[0][0]).toMatchObject({ - ttl: globalDefaultTtl, - }); - expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvMemcache); - const memcache = KeyvMemcache as unknown as jest.Mock; - const mockMemcacheCalls = memcache.mock.calls.splice(-1); - expect(mockMemcacheCalls[0][0]).toEqual(expectedHost); - }); - - it('returns a Redis client when configured', () => { - const redisConnection = 'redis://127.0.0.1:6379'; - const manager = CacheManager.fromConfig( - new ConfigReader({ - backend: { - cache: { - store: 'redis', - connection: redisConnection, - }, - }, - }), - ); - const expectedTtl = 3600; - manager.forPlugin('test').getClient({ defaultTtl: expectedTtl }); - - const cache = Keyv as unknown as jest.Mock; - const mockCacheCalls = cache.mock.calls.splice(-1); - expect(mockCacheCalls[0][0]).toMatchObject({ - ttl: expectedTtl, - }); - expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvRedis); - const redis = KeyvRedis as unknown as jest.Mock; - const mockRedisCalls = redis.mock.calls.splice(-1); - expect(mockRedisCalls[0][0]).toEqual(redisConnection); - }); - - it('returns a Redis client with a global defaultTtl when configured', () => { - const redisConnection = 'redis://127.0.0.1:6379'; - const manager = CacheManager.fromConfig( - new ConfigReader({ - backend: { - cache: { - store: 'redis', - connection: redisConnection, - defaultTtl: globalDefaultTtl, - }, - }, - }), - ); - manager.forPlugin('test').getClient(); - - const cache = Keyv as unknown as jest.Mock; - const mockCacheCalls = cache.mock.calls.splice(-1); - expect(mockCacheCalls[0][0]).toMatchObject({ - ttl: globalDefaultTtl, - }); - expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvRedis); - const redis = KeyvRedis as unknown as jest.Mock; - const mockRedisCalls = redis.mock.calls.splice(-1); - expect(mockRedisCalls[0][0]).toEqual(redisConnection); - }); - - it('returns a Redis client when configured with useRedisSets flag', () => { - const redisConnection = 'redis://127.0.0.1:6379'; - const useRedisSets = false; - const manager = CacheManager.fromConfig( - new ConfigReader({ - backend: { - cache: { - store: 'redis', - connection: redisConnection, - useRedisSets: useRedisSets, - }, - }, - }), - ); - const expectedTtl = 3600; - manager.forPlugin('test').getClient({ defaultTtl: expectedTtl }); - - const cache = Keyv as unknown as jest.Mock; - const mockCacheCalls = cache.mock.calls.splice(-1); - expect(mockCacheCalls[0][0]).toMatchObject({ - ttl: expectedTtl, - useRedisSets: useRedisSets, - }); - expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvRedis); - const redis = KeyvRedis as unknown as jest.Mock; - const mockRedisCalls = redis.mock.calls.splice(-1); - expect(mockRedisCalls[0][0]).toEqual(redisConnection); - }); - }); - - describe('connection errors', () => { - it('uses provided logger', () => { - // Set up and inject mock logger. - const mockLogger = { child: jest.fn(), error: jest.fn() }; - mockLogger.child.mockImplementation(() => mockLogger as any); - const manager = CacheManager.fromConfig(defaultConfig(), { - logger: mockLogger as any, - }); - - // Set up a cache client using the configured manager. - manager.forPlugin('error-logger-test').getClient(); - - // Retrieve the error handler attached to the cache client. - const client = DefaultCacheClient as jest.Mock; - const mockCalls = client.mock.calls.splice(-1); - const realClient = mockCalls[0][0] as Keyv; - const realOnError = realClient.on as jest.Mock; - const realHandler = realOnError.mock.calls.splice(-1)[0][1]; - - // Invoke the actual error handler. - const expectedError = new Error('some error'); - realHandler(expectedError); - expect(mockLogger.error).toHaveBeenCalledWith( - 'Failed to create cache client', - expectedError, - ); - }); - - it('calls provided handler', () => { - // Set up and inject mock logger. - const mockHandler = jest.fn(); - const manager = CacheManager.fromConfig(defaultConfig(), { - onError: mockHandler, - }); - - // Set up a cache client using the configured manager. - manager.forPlugin('error-handler-test').getClient(); - - // Retrieve the error handler attached to the cache client. - const client = DefaultCacheClient as jest.Mock; - const mockCalls = client.mock.calls.splice(-1); - const realClient = mockCalls[0][0] as Keyv; - const realOnError = realClient.on as jest.Mock; - const realHandler = realOnError.mock.calls.splice(-1)[0][1]; - - // Invoke the actual error handler. - const expectedError = new Error('some error'); - realHandler(expectedError); - expect(mockHandler).toHaveBeenCalledWith(expectedError); - }); - }); -}); diff --git a/packages/backend-common/src/cache/cacheToPluginCacheManager.ts b/packages/backend-common/src/cache/cacheToPluginCacheManager.ts new file mode 100644 index 0000000000..2654934804 --- /dev/null +++ b/packages/backend-common/src/cache/cacheToPluginCacheManager.ts @@ -0,0 +1,34 @@ +/* + * 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 { + CacheService, + CacheServiceOptions, +} from '@backstage/backend-plugin-api'; + +/** + * Compatibility wrapper for going from a new-backend cache service to the + * old-backend plugin cache manager. + * + * @public + */ +export function cacheToPluginCacheManager(cache: CacheService): { + getClient(options?: CacheServiceOptions): CacheService; +} { + return { + getClient: (opts: CacheServiceOptions) => cache.withOptions(opts), + }; +} diff --git a/packages/backend-common/src/cache/index.ts b/packages/backend-common/src/cache/index.ts index 0187aa45d5..9d05745ebd 100644 --- a/packages/backend-common/src/cache/index.ts +++ b/packages/backend-common/src/cache/index.ts @@ -14,11 +14,6 @@ * limitations under the License. */ -export { CacheManager, cacheToPluginCacheManager } from './CacheManager'; -export type { - CacheClient, - CacheClientSetOptions, - PluginCacheManager, - CacheManagerOptions, - CacheClientOptions, -} from './types'; +export { cacheToPluginCacheManager } from './cacheToPluginCacheManager'; +export * from './reexport'; +export * from './types'; diff --git a/packages/backend-common/src/cache/reexport.ts b/packages/backend-common/src/cache/reexport.ts new file mode 100644 index 0000000000..9f6106de60 --- /dev/null +++ b/packages/backend-common/src/cache/reexport.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * NOTE(freben): This is a temporary hack. We use cross-package imports so that + * we do not have to maintain double implementations for the time being, until + * backend-common is properly removed. + */ + +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { CacheManager } from '../../../backend-defaults/src/entrypoints/cache/CacheManager'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { + type PluginCacheManager, + type CacheManagerOptions, +} from '../../../backend-defaults/src/entrypoints/cache/types'; diff --git a/packages/backend-common/src/cache/types.ts b/packages/backend-common/src/cache/types.ts index 93fa0ac77c..83b6916843 100644 --- a/packages/backend-common/src/cache/types.ts +++ b/packages/backend-common/src/cache/types.ts @@ -14,39 +14,26 @@ * limitations under the License. */ -import { LoggerService } from '@backstage/backend-plugin-api'; -import { +import type { CacheService, + CacheServiceSetOptions, CacheServiceOptions, } from '@backstage/backend-plugin-api'; -export type { - CacheService as CacheClient, - CacheServiceSetOptions as CacheClientSetOptions, - CacheServiceOptions as CacheClientOptions, -} from '@backstage/backend-plugin-api'; - /** - * Options given when constructing a {@link CacheManager}. - * * @public + * @deprecated Use `CacheService` from the `@backstage/backend-plugin-api` package instead */ -export type CacheManagerOptions = { - /** - * An optional logger for use by the PluginCacheManager. - */ - logger?: LoggerService; - - /** - * An optional handler for connection errors emitted from the underlying data - * store. - */ - onError?: (err: Error) => void; -}; +export type CacheClient = CacheService; /** * @public + * @deprecated Use `CacheServiceSetOptions` from the `@backstage/backend-plugin-api` package instead */ -export interface PluginCacheManager { - getClient(options?: CacheServiceOptions): CacheService; -} +export type CacheClientSetOptions = CacheServiceSetOptions; + +/** + * @public + * @deprecated Use `CacheServiceOptions` from the `@backstage/backend-plugin-api` package instead + */ +export type CacheClientOptions = CacheServiceOptions; diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index b452d8db70..1dfa9643e2 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -29,6 +29,7 @@ import { setRootLoggerRedactionList } from './logging/createRootLogger'; * This function should only be called once, during the initialization of the backend. * * @public + * @deprecated Use {@link @backstage/backend-app-api#loadBackendConfig} instead. */ export async function loadBackendConfig(options: { logger: LoggerService; diff --git a/packages/backend-common/src/context/Contexts.ts b/packages/backend-common/src/context/Contexts.ts index 4eeb56b029..026e3ce4e0 100644 --- a/packages/backend-common/src/context/Contexts.ts +++ b/packages/backend-common/src/context/Contexts.ts @@ -24,6 +24,7 @@ import { ValueContext } from './ValueContext'; * Common context decorators. * * @alpha + * @deprecated This class is not used in the new Backend system, so it is going to be removed in a near future. */ export class Contexts { /** diff --git a/packages/backend-common/src/context/types.ts b/packages/backend-common/src/context/types.ts index 3b53225163..d980f54e43 100644 --- a/packages/backend-common/src/context/types.ts +++ b/packages/backend-common/src/context/types.ts @@ -19,6 +19,7 @@ * to pass along scoped information and abort signals. * * @alpha + * @deprecated This type is not used in the new Backend system, so it is going to be removed in a near future. */ export interface Context { /** diff --git a/packages/backend-common/src/database/index.ts b/packages/backend-common/src/database/index.ts index a0a61ccdd0..0e95261d82 100644 --- a/packages/backend-common/src/database/index.ts +++ b/packages/backend-common/src/database/index.ts @@ -14,11 +14,5 @@ * limitations under the License. */ -export { DatabaseManager, dropDatabase } from './DatabaseManager'; -export type { - DatabaseManagerOptions, - LegacyRootDatabaseService, -} from './DatabaseManager'; - +export * from './reexport'; export type { PluginDatabaseManager } from './types'; -export { isDatabaseConflictError } from './util'; diff --git a/packages/backend-common/src/database/reexport.ts b/packages/backend-common/src/database/reexport.ts new file mode 100644 index 0000000000..1ecff9be15 --- /dev/null +++ b/packages/backend-common/src/database/reexport.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * NOTE(freben): This is a temporary hack. We use cross-package imports so that + * we do not have to maintain double implementations for the time being, until + * backend-common is properly removed. When it is, the impleemntation should be + * moved into this part of the repo instead. + */ + +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { + DatabaseManager, + dropDatabase, + type DatabaseManagerOptions, + type LegacyRootDatabaseService, +} from '../../../backend-defaults/src/entrypoints/database/DatabaseManager'; + +export { + DatabaseManager, + dropDatabase, + type DatabaseManagerOptions, + type LegacyRootDatabaseService, +}; diff --git a/packages/backend-common/src/deprecated/index.ts b/packages/backend-common/src/deprecated/index.ts index 708e358bfc..d9ca795c3a 100644 --- a/packages/backend-common/src/deprecated/index.ts +++ b/packages/backend-common/src/deprecated/index.ts @@ -17,6 +17,7 @@ export * from './scm'; import { + isDatabaseConflictError as _isDatabaseConflictError, resolvePackagePath as _resolvePackagePath, resolveSafeChildPath as _resolveSafeChildPath, isChildPath as _isChildPath, @@ -24,21 +25,28 @@ import { /** * @public - * @deprecated This type is deprecated and will be removed in a future release, see https://github.com/backstage/backstage/issues/24493. + * @deprecated This function is deprecated and will be removed in a future release, see https://github.com/backstage/backstage/issues/24493. + * Please use the `isDatabaseConflictError` function from the `@backstage/backend-plugin-api` package instead. + */ +export const isDatabaseConflictError = _isDatabaseConflictError; + +/** + * @public + * @deprecated This function is deprecated and will be removed in a future release, see https://github.com/backstage/backstage/issues/24493. * Please use the `resolvePackagePath` function from the `@backstage/backend-plugin-api` package instead. */ export const resolvePackagePath = _resolvePackagePath; /** * @public - * @deprecated This type is deprecated and will be removed in a future release, see + * @deprecated This function is deprecated and will be removed in a future release, see https://github.com/backstage/backstage/issues/24493. * Please use the `resolveSafeChildPath` function from the `@backstage/backend-plugin-api` package instead. */ export const resolveSafeChildPath = _resolveSafeChildPath; /** * @public - * @deprecated This type is deprecated and will be removed in a future release, see + * @deprecated This function is deprecated and will be removed in a future release, see https://github.com/backstage/backstage/issues/24493. * Please use the `isChildPath` function from the `@backstage/cli-common` package instead. */ export const isChildPath = _isChildPath; diff --git a/packages/backend-common/src/discovery/HostDiscovery.ts b/packages/backend-common/src/discovery/HostDiscovery.ts index 38ac975747..77810b7f6d 100644 --- a/packages/backend-common/src/discovery/HostDiscovery.ts +++ b/packages/backend-common/src/discovery/HostDiscovery.ts @@ -15,8 +15,13 @@ */ import { HostDiscovery as _HostDiscovery } from '@backstage/backend-app-api'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; -export type { DiscoveryService as PluginEndpointDiscovery } from '@backstage/backend-plugin-api'; +/** + * @public + * @deprecated Use `DiscoveryService` from `@backstage/backend-plugin-api` instead + */ +export type PluginEndpointDiscovery = DiscoveryService; /** * HostDiscovery is a basic PluginEndpointDiscovery implementation @@ -27,6 +32,7 @@ export type { DiscoveryService as PluginEndpointDiscovery } from '@backstage/bac * resolved to the same host, so there won't be any balancing of internal traffic. * * @public + * @deprecated Please import from `@backstage/backend-defaults/discovery` instead. */ export const HostDiscovery = _HostDiscovery; @@ -39,6 +45,6 @@ export const HostDiscovery = _HostDiscovery; * resolved to the same host, so there won't be any balancing of internal traffic. * * @public - * @deprecated Use {@link HostDiscovery} instead + * @deprecated Use `HostDiscovery` from `@backstage/backend-defaults/discovery` instead */ export const SingleHostDiscovery = _HostDiscovery; diff --git a/packages/backend-common/src/discovery/index.ts b/packages/backend-common/src/discovery/index.ts index bad721d4da..827fd059ad 100644 --- a/packages/backend-common/src/discovery/index.ts +++ b/packages/backend-common/src/discovery/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { HostDiscovery, SingleHostDiscovery, diff --git a/packages/backend-common/src/logging/createRootLogger.ts b/packages/backend-common/src/logging/createRootLogger.ts index f9a3ed4ba0..16554750ef 100644 --- a/packages/backend-common/src/logging/createRootLogger.ts +++ b/packages/backend-common/src/logging/createRootLogger.ts @@ -59,6 +59,8 @@ const colorizer = format.colorize(); * Creates a pretty printed winston log formatter. * * @public + * @deprecated As we are going to deprecate the legacy backend, this formatter utility will be removed in the future. + * If you need to format logs in the new system, please use the `WinstonLogger.colorFormat()` from `@backstage/backend-app-api` instead. */ export const coloredFormat = format.combine( format.timestamp(), @@ -96,6 +98,9 @@ export const coloredFormat = format.combine( * instances passed to plugins etc, in a given backend. * * @public + * @deprecated As we are going to deprecate the legacy backend, this function will be removed in the future. + * If you need to create the root logger in the new system, please check out this documentation: + * https://backstage.io/docs/backend-system/core-services/logger */ export function createRootLogger( options: winston.LoggerOptions = {}, diff --git a/packages/backend-common/src/logging/globalLoggers.ts b/packages/backend-common/src/logging/globalLoggers.ts index 8b473db211..cbd5279d29 100644 --- a/packages/backend-common/src/logging/globalLoggers.ts +++ b/packages/backend-common/src/logging/globalLoggers.ts @@ -21,6 +21,8 @@ import { createRootLogger } from './createRootLogger'; * A logger that just throws away all messages. * * @public + * @deprecated As we are going to deprecate the legacy backend, this function will be removed in the future. + * If you need to mock the root logger in the new system, please use `mockServices.logger.mock()` from `@backstage/test-utils` instead. */ export function getVoidLogger(): winston.Logger { return winston.createLogger({ @@ -34,6 +36,9 @@ let rootLogger: winston.Logger; * Gets the current root logger. * * @public + * @deprecated As we are going to deprecate the legacy backend, this function will be removed in the future. + * If you need to get the root logger in the new system, please check out this documentation: + * https://backstage.io/docs/backend-system/core-services/logger */ export function getRootLogger(): winston.Logger { if (!rootLogger) { @@ -55,6 +60,9 @@ export function getRootLogger(): winston.Logger { * behavior, you would instead call {@link createRootLogger}. * * @public + * @deprecated As we are going to deprecate the legacy backend, this function will be removed in the future. + * If you need to set the root logger in the new system, please check out this documentation: + * https://backstage.io/docs/backend-system/core-services/logger */ export function setRootLogger(newLogger: winston.Logger) { rootLogger = newLogger; diff --git a/packages/backend-common/src/middleware/errorHandler.ts b/packages/backend-common/src/middleware/errorHandler.ts index 99ede75ab0..110734b0a2 100644 --- a/packages/backend-common/src/middleware/errorHandler.ts +++ b/packages/backend-common/src/middleware/errorHandler.ts @@ -61,6 +61,7 @@ export type ErrorHandlerOptions = { * * @public * @returns An Express error request handler + * @deprecated Use {@link @backstage/backend-app-api#MiddlewareFactory.create.error} instead */ export function errorHandler( options: ErrorHandlerOptions = {}, diff --git a/packages/backend-common/src/middleware/requestLoggingHandler.ts b/packages/backend-common/src/middleware/requestLoggingHandler.ts index a03e3c31f6..81dfa1d345 100644 --- a/packages/backend-common/src/middleware/requestLoggingHandler.ts +++ b/packages/backend-common/src/middleware/requestLoggingHandler.ts @@ -26,6 +26,7 @@ import { ConfigReader } from '@backstage/config'; * @public * @param logger - An optional logger to use. If not specified, the root logger will be used. * @returns An Express request handler + * @deprecated @deprecated Use {@link @backstage/backend-app-api#MiddlewareFactory.create.logging} instead */ export function requestLoggingHandler(logger?: LoggerService): RequestHandler { return MiddlewareFactory.create({ diff --git a/packages/backend-common/src/reading/AwsCodeCommitUrlReader.test.ts b/packages/backend-common/src/reading/AwsCodeCommitUrlReader.test.ts index 44d785d9c4..6263d073fc 100644 --- a/packages/backend-common/src/reading/AwsCodeCommitUrlReader.test.ts +++ b/packages/backend-common/src/reading/AwsCodeCommitUrlReader.test.ts @@ -16,7 +16,6 @@ import { ConfigReader } from '@backstage/config'; import { JsonObject } from '@backstage/types'; -import { getVoidLogger } from '../logging'; import { DefaultReadTreeResponseFactory } from './tree'; import { AwsCodeCommitUrlReader, parseUrl } from './AwsCodeCommitUrlReader'; import { UrlReaderPredicateTuple } from './types'; @@ -34,6 +33,7 @@ import { AwsCodeCommitIntegration, readAwsCodeCommitIntegrationConfig, } from '@backstage/integration'; +import { mockServices } from '@backstage/backend-test-utils'; const AMAZON_AWS_CODECOMMIT_HOST = 'console.aws.amazon.com'; @@ -202,7 +202,7 @@ describe('AwsCodeCommitUrlReader', () => { const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => { return AwsCodeCommitUrlReader.factory({ config: new ConfigReader(config), - logger: getVoidLogger(), + logger: mockServices.logger.mock(), treeResponseFactory, }); }; diff --git a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts index e3247f190b..15f46199da 100644 --- a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts +++ b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts @@ -16,7 +16,6 @@ import { ConfigReader } from '@backstage/config'; import { JsonObject } from '@backstage/types'; -import { getVoidLogger } from '../logging'; import { DefaultReadTreeResponseFactory } from './tree'; import { DEFAULT_REGION, AwsS3UrlReader, parseUrl } from './AwsS3UrlReader'; import { @@ -37,6 +36,7 @@ import { } from '@aws-sdk/client-s3'; import { sdkStreamMixin } from '@aws-sdk/util-stream-node'; import fs from 'fs'; +import { mockServices } from '@backstage/backend-test-utils'; const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), @@ -166,7 +166,7 @@ describe('AwsS3UrlReader', () => { const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => { return AwsS3UrlReader.factory({ config: new ConfigReader(config), - logger: getVoidLogger(), + logger: mockServices.logger.mock(), treeResponseFactory, }); }; diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index 2b4bc84be7..ce2385507b 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -25,6 +25,7 @@ import { } from '@backstage/integration'; import { createMockDirectory, + mockServices, setupRequestMockHandlers, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; @@ -32,7 +33,6 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import path from 'path'; import { NotModifiedError } from '@backstage/errors'; -import { getVoidLogger } from '../logging'; import { AzureUrlReader } from './AzureUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; @@ -42,7 +42,7 @@ type AzureIntegrationConfigLike = Partial< credentials?: Partial[]; }; -const logger = getVoidLogger(); +const logger = mockServices.logger.mock(); const mockDir = createMockDirectory({ mockOsTmpDir: true }); diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index 4258ac9d5b..edd8d6d3fc 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -21,6 +21,7 @@ import { } from '@backstage/integration'; import { createMockDirectory, + mockServices, setupRequestMockHandlers, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; @@ -30,10 +31,9 @@ import path from 'path'; import { NotModifiedError } from '@backstage/errors'; import { BitbucketUrlReader } from './BitbucketUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; -import { getVoidLogger } from '../logging'; import getRawBody from 'raw-body'; -const logger = getVoidLogger(); +const logger = mockServices.logger.mock(); describe('BitbucketUrlReader.factory', () => { it('only apply integration configs not inherited from bitbucketCloud or bitbucketServer', () => { diff --git a/packages/backend-common/src/reading/FetchUrlReader.test.ts b/packages/backend-common/src/reading/FetchUrlReader.test.ts index 8a0b24002e..38936806c6 100644 --- a/packages/backend-common/src/reading/FetchUrlReader.test.ts +++ b/packages/backend-common/src/reading/FetchUrlReader.test.ts @@ -16,10 +16,12 @@ import { ConfigReader } from '@backstage/config'; import { NotFoundError, NotModifiedError } from '@backstage/errors'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + setupRequestMockHandlers, +} from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { getVoidLogger } from '../logging'; import { FetchUrlReader } from './FetchUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; @@ -105,7 +107,7 @@ describe('FetchUrlReader', () => { }, }, }), - logger: getVoidLogger(), + logger: mockServices.logger.mock(), treeResponseFactory: DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), }), @@ -155,7 +157,7 @@ describe('FetchUrlReader', () => { }, }, }), - logger: getVoidLogger(), + logger: mockServices.logger.mock(), treeResponseFactory: DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), }), diff --git a/packages/backend-common/src/reading/GerritUrlReader.test.ts b/packages/backend-common/src/reading/GerritUrlReader.test.ts index 14f96b529a..ca4a2707f3 100644 --- a/packages/backend-common/src/reading/GerritUrlReader.test.ts +++ b/packages/backend-common/src/reading/GerritUrlReader.test.ts @@ -16,6 +16,7 @@ import { createMockDirectory, + mockServices, setupRequestMockHandlers, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; @@ -29,7 +30,6 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import fs from 'fs-extra'; import path from 'path'; -import { getVoidLogger } from '../logging'; import { UrlReaderPredicateTuple } from './types'; import { DefaultReadTreeResponseFactory } from './tree'; import { @@ -86,7 +86,7 @@ const gerritProcessorWithGitiles = new GerritUrlReader( const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => { return GerritUrlReader.factory({ config: new ConfigReader(config), - logger: getVoidLogger(), + logger: mockServices.logger.mock(), treeResponseFactory, }); }; diff --git a/packages/backend-common/src/reading/GiteaUrlReader.test.ts b/packages/backend-common/src/reading/GiteaUrlReader.test.ts index f8658a2241..0b5983eefd 100644 --- a/packages/backend-common/src/reading/GiteaUrlReader.test.ts +++ b/packages/backend-common/src/reading/GiteaUrlReader.test.ts @@ -14,13 +14,15 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + setupRequestMockHandlers, +} from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { GiteaIntegration, readGiteaConfig } from '@backstage/integration'; import { JsonObject } from '@backstage/types'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { getVoidLogger } from '../logging'; import { UrlReaderPredicateTuple } from './types'; import { DefaultReadTreeResponseFactory } from './tree'; import getRawBody from 'raw-body'; @@ -55,7 +57,7 @@ const giteaProcessor = new GiteaUrlReader( const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => { return GiteaUrlReader.factory({ config: new ConfigReader(config), - logger: getVoidLogger(), + logger: mockServices.logger.mock(), treeResponseFactory, }); }; diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index 3c065e8b81..46276fcdda 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -17,13 +17,13 @@ import { ConfigReader } from '@backstage/config'; import { createMockDirectory, + mockServices, setupRequestMockHandlers, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import path from 'path'; -import { getVoidLogger } from '../logging'; import { GitlabUrlReader } from './GitlabUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; import { NotModifiedError, NotFoundError } from '@backstage/errors'; @@ -32,7 +32,7 @@ import { readGitLabIntegrationConfig, } from '@backstage/integration'; -const logger = getVoidLogger(); +const logger = mockServices.logger.mock(); const mockDir = createMockDirectory({ mockOsTmpDir: true }); diff --git a/packages/backend-common/src/reading/GoogleGcsUrlReader.test.ts b/packages/backend-common/src/reading/GoogleGcsUrlReader.test.ts index 25bce25686..bc31818d23 100644 --- a/packages/backend-common/src/reading/GoogleGcsUrlReader.test.ts +++ b/packages/backend-common/src/reading/GoogleGcsUrlReader.test.ts @@ -16,11 +16,11 @@ import { ConfigReader } from '@backstage/config'; import { JsonObject } from '@backstage/types'; -import { getVoidLogger } from '../logging'; import { DefaultReadTreeResponseFactory } from './tree'; import { GoogleGcsUrlReader } from './GoogleGcsUrlReader'; import { UrlReaderPredicateTuple } from './types'; import packageinfo from '../../package.json'; +import { mockServices } from '@backstage/backend-test-utils'; const bucketGetFilesMock = jest.fn(); jest.mock('@google-cloud/storage', () => { @@ -44,7 +44,7 @@ describe('GcsUrlReader', () => { const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => { return GoogleGcsUrlReader.factory({ config: new ConfigReader(config), - logger: getVoidLogger(), + logger: mockServices.logger.mock(), treeResponseFactory: DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), }), diff --git a/packages/backend-common/src/reading/HarnessUrlReader.test.ts b/packages/backend-common/src/reading/HarnessUrlReader.test.ts index 2741b75fd5..f1efbca108 100644 --- a/packages/backend-common/src/reading/HarnessUrlReader.test.ts +++ b/packages/backend-common/src/reading/HarnessUrlReader.test.ts @@ -14,17 +14,22 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + setupRequestMockHandlers, +} from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { HarnessIntegration, readHarnessConfig } from '@backstage/integration'; import { JsonObject } from '@backstage/types'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { getVoidLogger } from '../logging'; import { UrlReaderPredicateTuple } from './types'; import { DefaultReadTreeResponseFactory } from './tree'; import getRawBody from 'raw-body'; import { HarnessUrlReader } from './HarnessUrlReader'; +import { NotFoundError, NotModifiedError } from '@backstage/errors'; +import fs from 'fs-extra'; +import path from 'path'; const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), @@ -47,12 +52,13 @@ const harnessProcessor = new HarnessUrlReader( }), ), ), + { treeResponseFactory }, ); const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => { return HarnessUrlReader.factory({ config: new ConfigReader(config), - logger: getVoidLogger(), + logger: mockServices.logger.mock(), treeResponseFactory, }); }; @@ -60,6 +66,7 @@ const responseBuffer = Buffer.from('Apache License'); const harnessApiResponse = (content: any) => { return content; }; +const commitHash = '3bdd5457286abdf920db4b77bf2fef79a06190c2'; const handlers = [ rest.get( @@ -93,6 +100,22 @@ const handlers = [ ); }, ), + rest.get( + 'https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName2/projectName/repoName/:path+/content?routingId=accountId&include_commit=true&git_ref=refs/heads/branchName', + (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ latest_commit: { sha: commitHash } }), + ); + }, + ), + rest.get( + 'https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName3/projectName/repoName/:path+/content?routingId=accountId&include_commit=true&git_ref=refs/heads/branchName', + (_, res, ctx) => { + return res(ctx.status(404)); + }, + ), ]; describe('HarnessUrlReader', () => { @@ -168,7 +191,7 @@ describe('HarnessUrlReader', () => { 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repos/repoName/files/refMain/~/404error.yaml', ), ).rejects.toThrow( - 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repos/repoName/files/refMain/~/404error.yaml x https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projName/repoName/+/raw/404error.yaml?routingId=accountId&git_ref=refMain, 404 Not Found', + 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repos/repoName/files/refMain/~/404error.yaml x https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projName/repoName/+/raw/404error.yaml?routingId=accountId&git_ref=refs/heads/refMain, 404 Not Found', ); }); @@ -178,8 +201,60 @@ describe('HarnessUrlReader', () => { 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repos/repoName/files/refMain/~/all-apis.yaml', ), ).rejects.toThrow( - 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repos/repoName/files/refMain/~/all-apis.yaml x https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projName/repoName/+/raw/all-apis.yaml?routingId=accountId&git_ref=refMain, 500 Internal Server Error', + 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repos/repoName/files/refMain/~/all-apis.yaml x https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projName/repoName/+/raw/all-apis.yaml?routingId=accountId&git_ref=refs/heads/refMain, 500 Internal Server Error', ); }); }); + + describe('readTree', () => { + const repoBuffer = fs.readFileSync( + path.resolve(__dirname, '__fixtures__/mock-main.zip'), + ); + + it('should be able to get archive', async () => { + worker.use( + rest.get( + 'https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName2/projectName/repoName/:path+/archive/branchName.zip', + (_, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/gzip'), + ctx.set( + 'content-disposition', + 'attachment; filename=backstage-mock.zip', + ), + ctx.body(repoBuffer), + ); + }, + ), + ); + + const response = await harnessProcessor.readTree( + 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName2/projects/projectName/repos/repoName/files/branchName', + ); + expect(response.etag).toBe(commitHash); + + const files = await response.files(); + expect(files.length).toBe(2); + }); + + it('should return not modified', async () => { + await expect( + harnessProcessor.readTree( + 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName2/projects/projectName/repos/repoName/files/branchName2', + { + etag: commitHash, + }, + ), + ).rejects.toThrow(NotModifiedError); + }); + + it('should return not found', async () => { + await expect( + harnessProcessor.readTree( + 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName3/projects/projectName/repos/repoName/files/branchName3', + ), + ).rejects.toThrow(NotFoundError); + }); + }); }); diff --git a/packages/backend-common/src/reading/HarnessUrlReader.ts b/packages/backend-common/src/reading/HarnessUrlReader.ts index 09291c5176..76ad882d80 100644 --- a/packages/backend-common/src/reading/HarnessUrlReader.ts +++ b/packages/backend-common/src/reading/HarnessUrlReader.ts @@ -18,12 +18,18 @@ import { getHarnessFileContentsUrl, HarnessIntegration, ScmIntegrations, + getHarnessLatestCommitUrl, + getHarnessArchiveUrl, + parseHarnessUrl, } from '@backstage/integration'; -import { ReadUrlOptions, ReadUrlResponse } from './types'; import { ReaderFactory, + ReadTreeOptions, ReadTreeResponse, SearchResponse, + ReadTreeResponseFactory, + ReadUrlOptions, + ReadUrlResponse, UrlReader, } from './types'; import fetch, { Response } from 'node-fetch'; @@ -42,11 +48,13 @@ import { Readable } from 'stream'; * @public */ export class HarnessUrlReader implements UrlReader { - static factory: ReaderFactory = ({ config }) => { + static factory: ReaderFactory = ({ config, treeResponseFactory }) => { return ScmIntegrations.fromConfig(config) .harness.list() .map(integration => { - const reader = new HarnessUrlReader(integration); + const reader = new HarnessUrlReader(integration, { + treeResponseFactory, + }); const predicate = (url: URL) => { return url.host === integration.config.host; }; @@ -54,8 +62,12 @@ export class HarnessUrlReader implements UrlReader { }); }; - constructor(private readonly integration: HarnessIntegration) {} - + constructor( + private readonly integration: HarnessIntegration, + private readonly deps: { + treeResponseFactory: ReadTreeResponseFactory; + }, + ) {} async read(url: string): Promise { const response = await this.readUrl(url); return response.buffer(); @@ -109,8 +121,37 @@ export class HarnessUrlReader implements UrlReader { throw new Error(message); } - readTree(): Promise { - throw new Error('HarnessUrlReader readTree not implemented.'); + async readTree( + url: string, + options?: ReadTreeOptions, + ): Promise { + const lastCommitHash = await this.getLastCommitHash(url); + + if (options?.etag && options.etag === lastCommitHash) { + throw new NotModifiedError(); + } + + const archiveUri = getHarnessArchiveUrl(this.integration.config, url); + + let response: Response; + try { + response = await fetch(archiveUri, { + method: 'GET', + ...getHarnessRequestOptions(this.integration.config), + signal: options?.signal as any, + }); + } catch (e) { + throw new Error(`Unable to read ${archiveUri}, ${e}`); + } + + const parsedUri = parseHarnessUrl(this.integration.config, url); + + return this.deps.treeResponseFactory.fromZipArchive({ + stream: Readable.from(response.body), + subpath: parsedUri.path, + etag: lastCommitHash, + filter: options?.filter, + }); } search(): Promise { throw new Error('HarnessUrlReader search not implemented.'); @@ -122,4 +163,21 @@ export class HarnessUrlReader implements UrlReader { this.integration.config.token || this.integration.config.apiKey, )}}`; } + private async getLastCommitHash(url: string): Promise { + const commitUri = getHarnessLatestCommitUrl(this.integration.config, url); + + const response = await fetch( + commitUri, + getHarnessRequestOptions(this.integration.config), + ); + if (!response.ok) { + const message = `Failed to retrieve latest commit information from ${commitUri}, ${response.status} ${response.statusText}`; + if (response.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + return (await response.json()).latest_commit.sha; + } } diff --git a/packages/backend-common/src/service/createServiceBuilder.ts b/packages/backend-common/src/service/createServiceBuilder.ts index a6ca25415e..e731056f72 100644 --- a/packages/backend-common/src/service/createServiceBuilder.ts +++ b/packages/backend-common/src/service/createServiceBuilder.ts @@ -19,8 +19,8 @@ import { ServiceBuilder } from './types'; /** * Creates a new service builder. - * * @public + * @deprecated We are going to deprecated this old way of creating services in a near future, if you are using this service helper, please checkout the {@link https://backstage.io/docs/backend-system/building-backends/migrating | backend} and {@link https://backstage.io/docs/backend-system/building-plugins-and-modules/migrating | plugin} migration guides. */ export function createServiceBuilder(_module: NodeModule): ServiceBuilder { return new ServiceBuilderImpl(_module); diff --git a/packages/backend-common/src/tokens/ServerTokenManager.test.ts b/packages/backend-common/src/tokens/ServerTokenManager.test.ts index 8870b4ed1c..2f99164ec1 100644 --- a/packages/backend-common/src/tokens/ServerTokenManager.test.ts +++ b/packages/backend-common/src/tokens/ServerTokenManager.test.ts @@ -16,17 +16,17 @@ import { ConfigReader } from '@backstage/config'; import * as jose from 'jose'; -import { getVoidLogger } from '../logging'; import { ServerTokenManager } from './ServerTokenManager'; import { TokenManager } from './types'; import { DateTime } from 'luxon'; +import { mockServices } from '@backstage/backend-test-utils'; const emptyConfig = new ConfigReader({}); const configWithSecret = new ConfigReader({ backend: { auth: { keys: [{ secret: 'a-secret-key' }] } }, }); const env = process.env; -const logger = getVoidLogger(); +const logger = mockServices.logger.mock(); describe('ServerTokenManager', () => { beforeEach(() => { diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index b43a592ba3..7556cbbab8 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,58 @@ # @backstage/backend-defaults +## 0.3.0-next.2 + +### Patch Changes + +- 0634fdc: Deprecated `dropDatabase` +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-permission-node@0.7.30-next.2 + - @backstage/backend-app-api@0.7.6-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/config-loader@1.8.0 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.3.0-next.1 + +### Minor Changes + +- 02103be: Deprecated and moved over core services to `@backstage/backend-defaults` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-permission-node@0.7.30-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/config-loader@1.8.0 + - @backstage/plugin-events-node@0.3.5-next.0 + +## 0.2.19-next.0 + +### Patch Changes + +- 6551b3d: Added core service factories and implementations from + `@backstage/backend-app-api`. They are now available as subpath exports, e.g. + `@backstage/backend-defaults/scheduler` is where the service factory and default + implementation of `coreServices.scheduler` now lives. They have been marked as + deprecated in their old locations. +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.2.18 ### Patch Changes diff --git a/packages/backend-defaults/api-report-cache.md b/packages/backend-defaults/api-report-cache.md new file mode 100644 index 0000000000..2cd2f3efc9 --- /dev/null +++ b/packages/backend-defaults/api-report-cache.md @@ -0,0 +1,42 @@ +## API Report File for "@backstage/backend-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CacheService } from '@backstage/backend-plugin-api'; +import { CacheServiceOptions } from '@backstage/backend-plugin-api'; +import { Config } from '@backstage/config'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; + +// @public +export class CacheManager { + forPlugin(pluginId: string): { + getClient(options?: CacheServiceOptions): CacheService; + }; + static fromConfig( + config: Config, + options?: { + logger?: LoggerService; + onError?: (err: Error) => void; + }, + ): CacheManager; +} + +// @public +export type CacheManagerOptions = { + logger?: LoggerService; + onError?: (err: Error) => void; +}; + +// @public (undocumented) +export const cacheServiceFactory: () => ServiceFactory; + +// @public (undocumented) +export interface PluginCacheManager { + // (undocumented) + getClient(options?: CacheServiceOptions): CacheService; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-defaults/api-report-database.md b/packages/backend-defaults/api-report-database.md new file mode 100644 index 0000000000..598a3bd1b0 --- /dev/null +++ b/packages/backend-defaults/api-report-database.md @@ -0,0 +1,53 @@ +## API Report File for "@backstage/backend-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Config } from '@backstage/config'; +import { DatabaseService } from '@backstage/backend-plugin-api'; +import { LifecycleService } from '@backstage/backend-plugin-api'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { PluginDatabaseManager } from '@backstage/backend-common'; +import { PluginMetadataService } from '@backstage/backend-plugin-api'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; + +// @public +export class DatabaseManager implements LegacyRootDatabaseService { + forPlugin( + pluginId: string, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, + ): DatabaseService; + static fromConfig( + config: Config, + options?: DatabaseManagerOptions, + ): DatabaseManager; +} + +// @public +export type DatabaseManagerOptions = { + migrations?: DatabaseService['migrations']; + logger?: LoggerService; +}; + +// @public (undocumented) +export const databaseServiceFactory: () => ServiceFactory< + PluginDatabaseManager, + 'plugin' +>; + +// @public @deprecated +export function dropDatabase( + dbConfig: Config, + ...databaseNames: string[] +): Promise; + +// @public +export type LegacyRootDatabaseService = { + forPlugin(pluginId: string): DatabaseService; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-defaults/api-report-discovery.md b/packages/backend-defaults/api-report-discovery.md new file mode 100644 index 0000000000..0ff734b8a7 --- /dev/null +++ b/packages/backend-defaults/api-report-discovery.md @@ -0,0 +1,31 @@ +## API Report File for "@backstage/backend-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Config } from '@backstage/config'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; + +// @public (undocumented) +export const discoveryServiceFactory: () => ServiceFactory< + DiscoveryService, + 'plugin' +>; + +// @public +export class HostDiscovery implements DiscoveryService { + static fromConfig( + config: Config, + options?: { + basePath?: string; + }, + ): HostDiscovery; + // (undocumented) + getBaseUrl(pluginId: string): Promise; + // (undocumented) + getExternalBaseUrl(pluginId: string): Promise; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-defaults/api-report-lifecycle.md b/packages/backend-defaults/api-report-lifecycle.md new file mode 100644 index 0000000000..4233584664 --- /dev/null +++ b/packages/backend-defaults/api-report-lifecycle.md @@ -0,0 +1,16 @@ +## API Report File for "@backstage/backend-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { LifecycleService } from '@backstage/backend-plugin-api'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; + +// @public +export const lifecycleServiceFactory: () => ServiceFactory< + LifecycleService, + 'plugin' +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-defaults/api-report-permissions.md b/packages/backend-defaults/api-report-permissions.md new file mode 100644 index 0000000000..9006a62018 --- /dev/null +++ b/packages/backend-defaults/api-report-permissions.md @@ -0,0 +1,16 @@ +## API Report File for "@backstage/backend-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { PermissionsService } from '@backstage/backend-plugin-api'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; + +// @public (undocumented) +export const permissionsServiceFactory: () => ServiceFactory< + PermissionsService, + 'plugin' +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-defaults/api-report-rootConfig.md b/packages/backend-defaults/api-report-rootConfig.md new file mode 100644 index 0000000000..60c8fbcb57 --- /dev/null +++ b/packages/backend-defaults/api-report-rootConfig.md @@ -0,0 +1,24 @@ +## API Report File for "@backstage/backend-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { RemoteConfigSourceOptions } from '@backstage/config-loader'; +import { RootConfigService } from '@backstage/backend-plugin-api'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; + +// @public (undocumented) +export interface RootConfigFactoryOptions { + argv?: string[]; + remote?: Pick; + // (undocumented) + watch?: boolean; +} + +// @public (undocumented) +export const rootConfigServiceFactory: ( + options?: RootConfigFactoryOptions | undefined, +) => ServiceFactory; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-defaults/api-report-rootLifecycle.md b/packages/backend-defaults/api-report-rootLifecycle.md new file mode 100644 index 0000000000..00eb9c1ebb --- /dev/null +++ b/packages/backend-defaults/api-report-rootLifecycle.md @@ -0,0 +1,16 @@ +## API Report File for "@backstage/backend-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { RootLifecycleService } from '@backstage/backend-plugin-api'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; + +// @public +export const rootLifecycleServiceFactory: () => ServiceFactory< + RootLifecycleService, + 'root' +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-defaults/api-report-scheduler.md b/packages/backend-defaults/api-report-scheduler.md new file mode 100644 index 0000000000..a2dad43d60 --- /dev/null +++ b/packages/backend-defaults/api-report-scheduler.md @@ -0,0 +1,16 @@ +## API Report File for "@backstage/backend-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { SchedulerService } from '@backstage/backend-plugin-api'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; + +// @public +export const schedulerServiceFactory: () => ServiceFactory< + SchedulerService, + 'plugin' +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-defaults/api-report-urlReader.md b/packages/backend-defaults/api-report-urlReader.md new file mode 100644 index 0000000000..8c7147bd7c --- /dev/null +++ b/packages/backend-defaults/api-report-urlReader.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/backend-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { UrlReader } from '@backstage/backend-common'; + +// @public (undocumented) +export const urlReaderServiceFactory: () => ServiceFactory; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts new file mode 100644 index 0000000000..569ab436db --- /dev/null +++ b/packages/backend-defaults/config.d.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + /** + * Options used by the default discovery service. + */ + discovery?: { + /** + * A list of target baseUrls and the associated plugins. + */ + endpoints: Array<{ + /** + * The target base URL to use for the plugin. + * + * Can be either a string or an object with internal and external keys. + * Targets with `{{pluginId}}` or `{{ pluginId }} in the URL will be replaced with the plugin ID. + */ + target: string | { internal: string; external: string }; + /** + * Array of plugins which use the target base URL. + */ + plugins: string[]; + }>; + }; +} diff --git a/packages/backend-defaults/migrations/scheduler/20210928160613_init.js b/packages/backend-defaults/migrations/scheduler/20210928160613_init.js new file mode 100644 index 0000000000..f9900cab11 --- /dev/null +++ b/packages/backend-defaults/migrations/scheduler/20210928160613_init.js @@ -0,0 +1,64 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + // + // tasks + // + await knex.schema.createTable('backstage_backend_tasks__tasks', table => { + table.comment('Tasks used for scheduling work on multiple workers'); + table + .string('id') + .primary() + .notNullable() + .comment('The unique ID of this particular task'); + table + .text('settings_json') + .notNullable() + .comment('JSON serialized object with properties for this task'); + table + .dateTime('next_run_start_at') + .notNullable() + .comment('The next time that the task should be started'); + table + .text('current_run_ticket') + .nullable() + .comment('A unique ticket for the current task run'); + table + .dateTime('current_run_started_at') + .nullable() + .comment('The time that the current task run started'); + table + .dateTime('current_run_expires_at') + .nullable() + .comment('The time that the current task run will time out'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + // + // tasks + // + await knex.schema.dropTable('backstage_backend_tasks__tasks'); +}; diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index b5da7abbcf..505c8924ba 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,47 +1,120 @@ { "name": "@backstage/backend-defaults", + "version": "0.3.0-next.2", "description": "Backend defaults used by Backstage backend apps", - "version": "0.2.18", - "main": "src/index.ts", - "types": "src/index.ts", - "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" - }, "backstage": { "role": "node-library" }, + "publishConfig": { + "access": "public" + }, + "keywords": [ + "backstage" + ], "homepage": "https://backstage.io", "repository": { "type": "git", "url": "https://github.com/backstage/backstage", "directory": "packages/backend-defaults" }, - "keywords": [ - "backstage" - ], "license": "Apache-2.0", + "exports": { + ".": "./src/index.ts", + "./cache": "./src/entrypoints/cache/index.ts", + "./database": "./src/entrypoints/database/index.ts", + "./discovery": "./src/entrypoints/discovery/index.ts", + "./lifecycle": "./src/entrypoints/lifecycle/index.ts", + "./permissions": "./src/entrypoints/permissions/index.ts", + "./rootConfig": "./src/entrypoints/rootConfig/index.ts", + "./rootLifecycle": "./src/entrypoints/rootLifecycle/index.ts", + "./scheduler": "./src/entrypoints/scheduler/index.ts", + "./urlReader": "./src/entrypoints/urlReader/index.ts", + "./package.json": "./package.json" + }, + "main": "src/index.ts", + "types": "src/index.ts", + "typesVersions": { + "*": { + "cache": [ + "src/entrypoints/cache/index.ts" + ], + "database": [ + "src/entrypoints/database/index.ts" + ], + "discovery": [ + "src/entrypoints/discovery/index.ts" + ], + "lifecycle": [ + "src/entrypoints/lifecycle/index.ts" + ], + "permissions": [ + "src/entrypoints/permissions/index.ts" + ], + "rootConfig": [ + "src/entrypoints/rootConfig/index.ts" + ], + "rootLifecycle": [ + "src/entrypoints/rootLifecycle/index.ts" + ], + "scheduler": [ + "src/entrypoints/scheduler/index.ts" + ], + "urlReader": [ + "src/entrypoints/urlReader/index.ts" + ], + "package.json": [ + "package.json" + ] + } + }, + "files": [ + "config.d.ts", + "dist", + "migrations" + ], "scripts": { "build": "backstage-cli package build", + "clean": "backstage-cli package clean", "lint": "backstage-cli package lint", - "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack", - "clean": "backstage-cli package clean", - "start": "backstage-cli package start" + "start": "backstage-cli package start", + "test": "backstage-cli package test" }, "dependencies": { "@backstage/backend-app-api": "workspace:^", "@backstage/backend-common": "workspace:^", - "@backstage/plugin-events-node": "workspace:^" + "@backstage/backend-dev-utils": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/config-loader": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-events-node": "workspace:^", + "@backstage/plugin-permission-node": "workspace:^", + "@backstage/types": "workspace:^", + "@keyv/memcache": "^1.3.5", + "@keyv/redis": "^2.5.3", + "@opentelemetry/api": "^1.3.0", + "better-sqlite3": "^9.0.0", + "cron": "^3.0.0", + "fs-extra": "^11.2.0", + "keyv": "^4.5.2", + "knex": "^3.0.0", + "lodash": "^4.17.21", + "luxon": "^3.0.0", + "mysql2": "^3.0.0", + "p-limit": "^3.1.0", + "pg": "^8.11.3", + "pg-connection-string": "^2.3.0", + "uuid": "^9.0.0", + "yn": "^4.0.0", + "zod": "^3.22.4" }, "devDependencies": { "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-test-utils": "workspace:^", - "@backstage/cli": "workspace:^" + "@backstage/cli": "workspace:^", + "wait-for-expect": "^3.0.2" }, - "files": [ - "dist" - ] + "configSchema": "config.d.ts" } diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index e9bdc03262..823b3c2833 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -16,26 +16,26 @@ import { Backend, - cacheServiceFactory, - rootConfigServiceFactory, - createSpecializedBackend, - databaseServiceFactory, - discoveryServiceFactory, - httpRouterServiceFactory, - rootHttpRouterServiceFactory, - lifecycleServiceFactory, - rootLifecycleServiceFactory, - loggerServiceFactory, - permissionsServiceFactory, - rootLoggerServiceFactory, - schedulerServiceFactory, - tokenManagerServiceFactory, - urlReaderServiceFactory, - identityServiceFactory, authServiceFactory, + createSpecializedBackend, httpAuthServiceFactory, + httpRouterServiceFactory, + identityServiceFactory, + loggerServiceFactory, + rootHttpRouterServiceFactory, + rootLoggerServiceFactory, + tokenManagerServiceFactory, userInfoServiceFactory, } from '@backstage/backend-app-api'; +import { cacheServiceFactory } from '@backstage/backend-defaults/cache'; +import { databaseServiceFactory } from '@backstage/backend-defaults/database'; +import { discoveryServiceFactory } from '@backstage/backend-defaults/discovery'; +import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle'; +import { permissionsServiceFactory } from '@backstage/backend-defaults/permissions'; +import { rootConfigServiceFactory } from '@backstage/backend-defaults/rootConfig'; +import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle'; +import { schedulerServiceFactory } from '@backstage/backend-defaults/scheduler'; +import { urlReaderServiceFactory } from '@backstage/backend-defaults/urlReader'; import { eventsServiceFactory } from '@backstage/plugin-events-node'; export const defaultServiceFactories = [ diff --git a/packages/backend-common/src/cache/CacheClient.test.ts b/packages/backend-defaults/src/entrypoints/cache/CacheClient.test.ts similarity index 100% rename from packages/backend-common/src/cache/CacheClient.test.ts rename to packages/backend-defaults/src/entrypoints/cache/CacheClient.test.ts diff --git a/packages/backend-common/src/cache/CacheClient.ts b/packages/backend-defaults/src/entrypoints/cache/CacheClient.ts similarity index 100% rename from packages/backend-common/src/cache/CacheClient.ts rename to packages/backend-defaults/src/entrypoints/cache/CacheClient.ts diff --git a/packages/backend-common/src/cache/CacheManager.integration.test.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts similarity index 57% rename from packages/backend-common/src/cache/CacheManager.integration.test.ts rename to packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts index 7dc60a9051..6eb907764d 100644 --- a/packages/backend-common/src/cache/CacheManager.integration.test.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts @@ -14,33 +14,41 @@ * limitations under the License. */ -import { ConfigReader } from '@backstage/config'; -import { CacheManager } from './CacheManager'; +import { mockServices, TestCaches } from '@backstage/backend-test-utils'; import KeyvRedis from '@keyv/redis'; +import KeyvMemcache from '@keyv/memcache'; +import { CacheManager } from './CacheManager'; // This test is in a separate file because the main test file uses other mocking // that might interfere with this one. // Contrived code because it's hard to spy on a default export jest.mock('@keyv/redis', () => { - const ActualKeyvRedis = jest.requireActual('@keyv/redis'); - return jest - .fn() - .mockImplementation((...args: any[]) => new ActualKeyvRedis(...args)); + const Actual = jest.requireActual('@keyv/redis'); + return jest.fn((...args: any[]) => { + return new Actual(...args); + }); +}); +jest.mock('@keyv/memcache', () => { + const Actual = jest.requireActual('@keyv/memcache'); + return jest.fn((...args: any[]) => { + return new Actual(...args); + }); }); describe('CacheManager integration', () => { - describe('redis', () => { - it('only creates one underlying connection', async () => { + const caches = TestCaches.create(); + + afterEach(jest.clearAllMocks); + + it.each(caches.eachSupportedId())( + 'only creates one underlying connection, %p', + async cacheId => { + const { store, connection } = await caches.init(cacheId); + const manager = CacheManager.fromConfig( - new ConfigReader({ - backend: { - cache: { - store: 'redis', - // no actual connection errors will be seen since we don't interact with it - connection: 'redis://localhost:6379', - }, - }, + mockServices.rootConfig({ + data: { backend: { cache: { store, connection } } }, }), ); @@ -49,25 +57,25 @@ describe('CacheManager integration', () => { manager.forPlugin('p2').getClient(); manager.forPlugin('p3').getClient({}); - expect(KeyvRedis).toHaveBeenCalledTimes(1); - }); - - it('interacts correctly with redis', async () => { - // TODO(freben): This could be frameworkified as TestCaches just like - // TestDatabases, but that will have to come some other day - const connection = - process.env.BACKSTAGE_TEST_CACHE_REDIS_CONNECTION_STRING; - if (!connection) { - return; + if (store === 'redis') { + // eslint-disable-next-line jest/no-conditional-expect + expect(KeyvRedis).toHaveBeenCalledTimes(1); + } else if (store === 'memcache') { + // eslint-disable-next-line jest/no-conditional-expect + expect(KeyvMemcache).toHaveBeenCalledTimes(1); } + }, + ); + + it.each(caches.eachSupportedId())( + 'interacts correctly with store, %p', + async cacheId => { + const { store, connection } = await caches.init(cacheId); const manager = CacheManager.fromConfig( - new ConfigReader({ - backend: { - cache: { - store: 'redis', - connection, - }, + mockServices.rootConfig({ + data: { + backend: { cache: { store, connection } }, }, }), ); @@ -83,6 +91,6 @@ describe('CacheManager integration', () => { await expect(plugin1.get('a')).resolves.toBe('plugin1'); await expect(plugin2a.get('a')).resolves.toBe('plugin2b'); await expect(plugin2b.get('a')).resolves.toBe('plugin2b'); - }); - }); + }, + ); }); diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts similarity index 82% rename from packages/backend-common/src/cache/CacheManager.ts rename to packages/backend-defaults/src/entrypoints/cache/CacheManager.ts index ec8a1d93f7..70f952ea46 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts @@ -14,21 +14,25 @@ * limitations under the License. */ -import { Config } from '@backstage/config'; -import Keyv from 'keyv'; -import KeyvMemcache from '@keyv/memcache'; -import KeyvRedis from '@keyv/redis'; import { CacheService, CacheServiceOptions, LoggerService, } from '@backstage/backend-plugin-api'; -import { getRootLogger } from '../logging'; +import { Config } from '@backstage/config'; +import Keyv from 'keyv'; import { DefaultCacheClient } from './CacheClient'; -import { CacheManagerOptions, PluginCacheManager } from './types'; +import { CacheManagerOptions } from './types'; type StoreFactory = (pluginId: string, defaultTtl: number | undefined) => Keyv; +/* + * TODO(freben): This class intentionally inlines the CacheManagerOptions and + * PluginCacheManager types, to not break the api reports in backend-common + * which re-exports it. When backend-common is deprecated, we can stop inlining + * those types. + */ + /** * Implements a Cache Manager which will automatically create new cache clients * for plugins when requested. All requested cache clients are created with the @@ -47,7 +51,7 @@ export class CacheManager { memory: this.createMemoryStoreFactory(), }; - private readonly logger: LoggerService; + private readonly logger?: LoggerService; private readonly store: keyof CacheManager['storeFactories']; private readonly connection: string; private readonly useRedisSets: boolean; @@ -62,7 +66,18 @@ export class CacheManager { */ static fromConfig( config: Config, - options: CacheManagerOptions = {}, + options: { + /** + * An optional logger for use by the PluginCacheManager. + */ + logger?: LoggerService; + + /** + * An optional handler for connection errors emitted from the underlying data + * store. + */ + onError?: (err: Error) => void; + } = {}, ): CacheManager { // If no `backend.cache` config is provided, instantiate the CacheManager // with an in-memory cache client. @@ -72,25 +87,26 @@ export class CacheManager { config.getOptionalString('backend.cache.connection') || ''; const useRedisSets = config.getOptionalBoolean('backend.cache.useRedisSets') ?? true; - const logger = (options.logger || getRootLogger()).child({ + const logger = options.logger?.child({ type: 'cacheManager', }); return new CacheManager( store, connectionString, useRedisSets, - logger, options.onError, + logger, defaultTtl, ); } - private constructor( + /** @internal */ + constructor( store: string, connectionString: string, useRedisSets: boolean, - logger: LoggerService, errorHandler: CacheManagerOptions['onError'], + logger?: LoggerService, defaultTtl?: number, ) { if (!this.storeFactories.hasOwnProperty(store)) { @@ -110,7 +126,9 @@ export class CacheManager { * @param pluginId - The plugin that the cache manager should be created for. * Plugin names should be unique. */ - forPlugin(pluginId: string): PluginCacheManager { + forPlugin(pluginId: string): { + getClient(options?: CacheServiceOptions): CacheService; + } { return { getClient: (defaultOptions = {}) => { const clientFactory = (options: CacheServiceOptions) => { @@ -122,7 +140,7 @@ export class CacheManager { // Always provide an error handler to avoid stopping the process. concreteClient.on('error', (err: Error) => { // In all cases, just log the error. - this.logger.error('Failed to create cache client', err); + this.logger?.error('Failed to create cache client', err); // Invoke any custom error handler if provided. if (typeof this.errorHandler === 'function') { @@ -147,7 +165,8 @@ export class CacheManager { } private createRedisStoreFactory(): StoreFactory { - let store: KeyvRedis | undefined; + const KeyvRedis = require('@keyv/redis'); + let store: typeof KeyvRedis | undefined; return (pluginId, defaultTtl) => { if (!store) { store = new KeyvRedis(this.connection); @@ -162,7 +181,8 @@ export class CacheManager { } private createMemcacheStoreFactory(): StoreFactory { - let store: KeyvMemcache | undefined; + const KeyvMemcache = require('@keyv/memcache'); + let store: typeof KeyvMemcache | undefined; return (pluginId, defaultTtl) => { if (!store) { store = new KeyvMemcache(this.connection); @@ -185,12 +205,3 @@ export class CacheManager { }); } } - -/** @public */ -export function cacheToPluginCacheManager( - cache: CacheService, -): PluginCacheManager { - return { - getClient: (opts: CacheServiceOptions) => cache.withOptions(opts), - }; -} diff --git a/packages/backend-defaults/src/entrypoints/cache/cacheServiceFactory.ts b/packages/backend-defaults/src/entrypoints/cache/cacheServiceFactory.ts new file mode 100644 index 0000000000..f60d770644 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/cache/cacheServiceFactory.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { CacheManager } from './CacheManager'; + +/** + * @public + */ +export const cacheServiceFactory = createServiceFactory({ + service: coreServices.cache, + deps: { + config: coreServices.rootConfig, + plugin: coreServices.pluginMetadata, + logger: coreServices.rootLogger, + }, + async createRootContext({ config, logger }) { + return CacheManager.fromConfig(config, { logger }); + }, + async factory({ plugin }, manager) { + return manager.forPlugin(plugin.getId()).getClient(); + }, +}); diff --git a/packages/backend-defaults/src/entrypoints/cache/index.ts b/packages/backend-defaults/src/entrypoints/cache/index.ts new file mode 100644 index 0000000000..b16fa56bd2 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/cache/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { cacheServiceFactory } from './cacheServiceFactory'; +export { CacheManager } from './CacheManager'; +export type { CacheManagerOptions, PluginCacheManager } from './types'; diff --git a/packages/backend-defaults/src/entrypoints/cache/types.ts b/packages/backend-defaults/src/entrypoints/cache/types.ts new file mode 100644 index 0000000000..ccb3ed5f6d --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/cache/types.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LoggerService } from '@backstage/backend-plugin-api'; +import { + CacheService, + CacheServiceOptions, +} from '@backstage/backend-plugin-api'; + +/** + * Options given when constructing a {@link CacheManager}. + * + * @public + */ +export type CacheManagerOptions = { + /** + * An optional logger for use by the PluginCacheManager. + */ + logger?: LoggerService; + + /** + * An optional handler for connection errors emitted from the underlying data + * store. + */ + onError?: (err: Error) => void; +}; + +/** + * @public + */ +export interface PluginCacheManager { + getClient(options?: CacheServiceOptions): CacheService; +} diff --git a/packages/backend-common/src/database/DatabaseManager.test.ts b/packages/backend-defaults/src/entrypoints/database/DatabaseManager.test.ts similarity index 100% rename from packages/backend-common/src/database/DatabaseManager.test.ts rename to packages/backend-defaults/src/entrypoints/database/DatabaseManager.test.ts diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-defaults/src/entrypoints/database/DatabaseManager.ts similarity index 98% rename from packages/backend-common/src/database/DatabaseManager.ts rename to packages/backend-defaults/src/entrypoints/database/DatabaseManager.ts index dd36cc3b4a..7e3adf17df 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-defaults/src/entrypoints/database/DatabaseManager.ts @@ -41,7 +41,7 @@ function pluginPath(pluginId: string): string { * @public */ export type DatabaseManagerOptions = { - migrations?: PluginDatabaseManager['migrations']; + migrations?: DatabaseService['migrations']; logger?: LoggerService; }; @@ -234,9 +234,10 @@ export class DatabaseManager implements LegacyRootDatabaseService { } /** - * Helper for deleting databases, only exists for backend-test-utils for now. + * Helper for deleting databases. * * @public + * @deprecated Will be removed in a future release. */ export async function dropDatabase( dbConfig: Config, diff --git a/packages/backend-common/src/database/connectors/defaultNameOverride.test.ts b/packages/backend-defaults/src/entrypoints/database/connectors/defaultNameOverride.test.ts similarity index 100% rename from packages/backend-common/src/database/connectors/defaultNameOverride.test.ts rename to packages/backend-defaults/src/entrypoints/database/connectors/defaultNameOverride.test.ts diff --git a/packages/backend-common/src/database/connectors/defaultNameOverride.ts b/packages/backend-defaults/src/entrypoints/database/connectors/defaultNameOverride.ts similarity index 100% rename from packages/backend-common/src/database/connectors/defaultNameOverride.ts rename to packages/backend-defaults/src/entrypoints/database/connectors/defaultNameOverride.ts diff --git a/packages/backend-common/src/database/connectors/defaultSchemaOverride.test.ts b/packages/backend-defaults/src/entrypoints/database/connectors/defaultSchemaOverride.test.ts similarity index 100% rename from packages/backend-common/src/database/connectors/defaultSchemaOverride.test.ts rename to packages/backend-defaults/src/entrypoints/database/connectors/defaultSchemaOverride.test.ts diff --git a/packages/backend-common/src/database/connectors/defaultSchemaOverride.ts b/packages/backend-defaults/src/entrypoints/database/connectors/defaultSchemaOverride.ts similarity index 100% rename from packages/backend-common/src/database/connectors/defaultSchemaOverride.ts rename to packages/backend-defaults/src/entrypoints/database/connectors/defaultSchemaOverride.ts diff --git a/packages/backend-common/src/database/connectors/index.ts b/packages/backend-defaults/src/entrypoints/database/connectors/index.ts similarity index 100% rename from packages/backend-common/src/database/connectors/index.ts rename to packages/backend-defaults/src/entrypoints/database/connectors/index.ts diff --git a/packages/backend-common/src/database/connectors/mergeDatabaseConfig.test.ts b/packages/backend-defaults/src/entrypoints/database/connectors/mergeDatabaseConfig.test.ts similarity index 100% rename from packages/backend-common/src/database/connectors/mergeDatabaseConfig.test.ts rename to packages/backend-defaults/src/entrypoints/database/connectors/mergeDatabaseConfig.test.ts diff --git a/packages/backend-common/src/database/connectors/mergeDatabaseConfig.ts b/packages/backend-defaults/src/entrypoints/database/connectors/mergeDatabaseConfig.ts similarity index 100% rename from packages/backend-common/src/database/connectors/mergeDatabaseConfig.ts rename to packages/backend-defaults/src/entrypoints/database/connectors/mergeDatabaseConfig.ts diff --git a/packages/backend-common/src/database/connectors/mysql.test.ts b/packages/backend-defaults/src/entrypoints/database/connectors/mysql.test.ts similarity index 100% rename from packages/backend-common/src/database/connectors/mysql.test.ts rename to packages/backend-defaults/src/entrypoints/database/connectors/mysql.test.ts diff --git a/packages/backend-common/src/database/connectors/mysql.ts b/packages/backend-defaults/src/entrypoints/database/connectors/mysql.ts similarity index 100% rename from packages/backend-common/src/database/connectors/mysql.ts rename to packages/backend-defaults/src/entrypoints/database/connectors/mysql.ts diff --git a/packages/backend-common/src/database/connectors/postgres.test.ts b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts similarity index 100% rename from packages/backend-common/src/database/connectors/postgres.test.ts rename to packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts diff --git a/packages/backend-common/src/database/connectors/postgres.ts b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts similarity index 100% rename from packages/backend-common/src/database/connectors/postgres.ts rename to packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts diff --git a/packages/backend-common/src/database/connectors/sqlite3.test.ts b/packages/backend-defaults/src/entrypoints/database/connectors/sqlite3.test.ts similarity index 100% rename from packages/backend-common/src/database/connectors/sqlite3.test.ts rename to packages/backend-defaults/src/entrypoints/database/connectors/sqlite3.test.ts diff --git a/packages/backend-common/src/database/connectors/sqlite3.ts b/packages/backend-defaults/src/entrypoints/database/connectors/sqlite3.ts similarity index 100% rename from packages/backend-common/src/database/connectors/sqlite3.ts rename to packages/backend-defaults/src/entrypoints/database/connectors/sqlite3.ts diff --git a/packages/backend-defaults/src/entrypoints/database/databaseServiceFactory.ts b/packages/backend-defaults/src/entrypoints/database/databaseServiceFactory.ts new file mode 100644 index 0000000000..12e4e569bd --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/database/databaseServiceFactory.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DatabaseManager } from '@backstage/backend-common'; +import { + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { ConfigReader } from '@backstage/config'; + +/** + * @public + */ +export const databaseServiceFactory = createServiceFactory({ + service: coreServices.database, + deps: { + config: coreServices.rootConfig, + lifecycle: coreServices.lifecycle, + pluginMetadata: coreServices.pluginMetadata, + }, + async createRootContext({ config }) { + return config.getOptional('backend.database') + ? DatabaseManager.fromConfig(config) + : DatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { client: 'better-sqlite3', connection: ':memory:' }, + }, + }), + ); + }, + async factory({ pluginMetadata, lifecycle }, databaseManager) { + return databaseManager.forPlugin(pluginMetadata.getId(), { + pluginMetadata, + lifecycle, + }); + }, +}); diff --git a/packages/backend-defaults/src/entrypoints/database/index.ts b/packages/backend-defaults/src/entrypoints/database/index.ts new file mode 100644 index 0000000000..7d6221b856 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/database/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { databaseServiceFactory } from './databaseServiceFactory'; +export { + DatabaseManager, + type DatabaseManagerOptions, + type LegacyRootDatabaseService, + dropDatabase, +} from './DatabaseManager'; diff --git a/packages/backend-defaults/src/entrypoints/database/types.ts b/packages/backend-defaults/src/entrypoints/database/types.ts new file mode 100644 index 0000000000..a9cceaa1f9 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/database/types.ts @@ -0,0 +1,100 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + LifecycleService, + PluginMetadataService, +} from '@backstage/backend-plugin-api'; +import { Config } from '@backstage/config'; +import { Knex } from 'knex'; + +export type { DatabaseService as PluginDatabaseManager } from '@backstage/backend-plugin-api'; + +/** + * Manages an underlying Knex database driver. + */ +export interface DatabaseConnector { + /** + * Provides an instance of a knex database connector. + */ + createClient( + dbConfig: Config, + overrides?: Partial, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, + ): Knex; + + /** + * Provides a partial knex config sufficient to override a database name. + */ + createNameOverride(name: string): Partial; + + /** + * Provides a partial knex config sufficient to override a PostgreSQL schema + * name within utilizing the `searchPath` knex configuration. + */ + createSchemaOverride?(name: string): Partial; + + /** + * Produces a knex connection config object representing a database connection + * string. + */ + parseConnectionString( + connectionString: string, + client?: string, + ): Knex.StaticConnectionConfig; + + /** + * Performs a side-effect to ensure database names passed in are present. + * + * Calling this function on databases which already exist should do nothing. + * Missing databases should be created if needed. + */ + ensureDatabaseExists?( + dbConfig: Config, + ...databases: Array + ): Promise; + + /** + * Performs a side-effect to ensure schema names passed in are present. + * + * Calling this function on schemas which already exist should do nothing. + * Missing schemas should be created if needed. + */ + ensureSchemaExists?( + dbConfig: Config, + ...schemas: Array + ): Promise; + + /** + * Deletes databases. + */ + dropDatabase?(dbConfig: Config, ...databases: Array): Promise; +} + +export interface Connector { + getClient( + pluginId: string, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, + ): Promise; + + dropDatabase(...databaseNames: string[]): Promise; +} diff --git a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts new file mode 100644 index 0000000000..4e6aff5853 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts @@ -0,0 +1,257 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { HostDiscovery } from './HostDiscovery'; + +describe('HostDiscovery', () => { + it('is created from config', async () => { + const discovery = HostDiscovery.fromConfig( + new ConfigReader({ + backend: { + baseUrl: 'http://localhost:40', + listen: { port: 80, host: 'localhost' }, + }, + }), + ); + + await expect(discovery.getBaseUrl('catalog')).resolves.toBe( + 'http://localhost:80/api/catalog', + ); + await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe( + 'http://localhost:40/api/catalog', + ); + }); + + it('strips trailing slashes in config', async () => { + const discovery = HostDiscovery.fromConfig( + new ConfigReader({ + backend: { + baseUrl: 'http://localhost:40//', + listen: { port: 80, host: 'localhost' }, + }, + }), + ); + + await expect(discovery.getBaseUrl('catalog')).resolves.toBe( + 'http://localhost:80/api/catalog', + ); + await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe( + 'http://localhost:40/api/catalog', + ); + }); + + it('can configure the base path', async () => { + const discovery = HostDiscovery.fromConfig( + new ConfigReader({ + backend: { + baseUrl: 'http://localhost:40', + listen: { port: 80, host: 'localhost' }, + }, + }), + { basePath: '/service' }, + ); + + await expect(discovery.getBaseUrl('catalog')).resolves.toBe( + 'http://localhost:80/service/catalog', + ); + await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe( + 'http://localhost:40/service/catalog', + ); + }); + + it.each([ + [{ listen: ':80' }, 'http://localhost:80'], + [{ listen: ':40', https: true }, 'https://localhost:40'], + [{ listen: '127.0.0.1:80' }, 'http://127.0.0.1:80'], + [{ listen: '127.0.0.1:80', https: true }, 'https://127.0.0.1:80'], + [{ listen: '0.0.0.0:40' }, 'http://127.0.0.1:40'], + [{ listen: { port: 80 } }, 'http://localhost:80'], + [{ listen: { port: 8000 } }, 'http://localhost:8000'], + [{ listen: { port: 80, host: '0.0.0.0' } }, 'http://127.0.0.1:80'], + [{ listen: { port: 80, host: '::' } }, 'http://localhost:80'], + [{ listen: { port: 80, host: '::1' } }, 'http://[::1]:80'], + [{ listen: { port: 90, host: '::2' }, https: true }, 'https://[::2]:90'], + ])('resolves internal baseUrl for %j as %s', async (config, expected) => { + const discovery = HostDiscovery.fromConfig( + new ConfigReader({ + backend: { + baseUrl: 'http://localhost:40', + ...config, + }, + }), + ); + + await expect(discovery.getBaseUrl('catalog')).resolves.toBe( + `${expected}/api/catalog`, + ); + }); + + it('uses plugin specific targets from config if provided', async () => { + const discovery = HostDiscovery.fromConfig( + new ConfigReader({ + backend: { + baseUrl: 'http://localhost:40', + listen: { port: 80, host: 'localhost' }, + }, + discovery: { + endpoints: [ + { + target: { + internal: 'http://catalog-backend-internal:8080/api/catalog', + external: 'http://catalog-backend-external:8080/api/catalog', + }, + plugins: ['catalog'], + }, + ], + }, + }), + ); + + await expect(discovery.getBaseUrl('catalog')).resolves.toBe( + 'http://catalog-backend-internal:8080/api/catalog', + ); + await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe( + 'http://catalog-backend-external:8080/api/catalog', + ); + }); + + it('uses a single target for internal and external for a plugin', async () => { + const discovery = HostDiscovery.fromConfig( + new ConfigReader({ + backend: { + baseUrl: 'http://localhost:40', + listen: { port: 80, host: 'localhost' }, + }, + discovery: { + endpoints: [ + { + target: 'http://catalog-backend:8080/api/catalog', + plugins: ['catalog'], + }, + ], + }, + }), + ); + + await expect(discovery.getBaseUrl('catalog')).resolves.toBe( + 'http://catalog-backend:8080/api/catalog', + ); + await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe( + 'http://catalog-backend:8080/api/catalog', + ); + }); + + it('defaults to the backend baseUrl when there is not an endpoint for a plugin', async () => { + const discovery = HostDiscovery.fromConfig( + new ConfigReader({ + backend: { + baseUrl: 'http://localhost:40', + listen: { port: 80, host: 'localhost' }, + }, + discovery: { + endpoints: [ + { + target: 'http://catalog-backend:8080/api/catalog', + plugins: ['catalog'], + }, + ], + }, + }), + ); + + await expect(discovery.getBaseUrl('scaffolder')).resolves.toBe( + 'http://localhost:80/api/scaffolder', + ); + await expect(discovery.getExternalBaseUrl('scaffolder')).resolves.toBe( + 'http://localhost:40/api/scaffolder', + ); + }); + + it('replaces {{pluginId}} or {{ pluginId }} in the target', async () => { + const discovery = HostDiscovery.fromConfig( + new ConfigReader({ + backend: { + baseUrl: 'http://localhost:40', + listen: { port: 80, host: 'localhost' }, + }, + discovery: { + endpoints: [ + { + target: 'http://common-backend:8080/api/{{pluginId}}', + plugins: ['catalog', 'docs'], + }, + { + target: { + internal: 'http://scaffolder-internal:8080/api/{{ pluginId }}', + external: 'http://scaffolder-external:8080/api/{{ pluginId }}', + }, + plugins: ['scaffolder'], + }, + ], + }, + }), + ); + + await expect(discovery.getBaseUrl('catalog')).resolves.toBe( + 'http://common-backend:8080/api/catalog', + ); + await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe( + 'http://common-backend:8080/api/catalog', + ); + await expect(discovery.getBaseUrl('docs')).resolves.toBe( + 'http://common-backend:8080/api/docs', + ); + await expect(discovery.getExternalBaseUrl('docs')).resolves.toBe( + 'http://common-backend:8080/api/docs', + ); + await expect(discovery.getBaseUrl('scaffolder')).resolves.toBe( + 'http://scaffolder-internal:8080/api/scaffolder', + ); + await expect(discovery.getExternalBaseUrl('scaffolder')).resolves.toBe( + 'http://scaffolder-external:8080/api/scaffolder', + ); + }); + + it('encodes the pluginId', async () => { + const discovery = HostDiscovery.fromConfig( + new ConfigReader({ + backend: { + baseUrl: 'http://localhost:40', + listen: { port: 80, host: 'localhost' }, + }, + discovery: { + endpoints: [ + { + target: 'http://common-backend:8080/api/{{pluginId}}', + plugins: ['plugin/beta'], + }, + ], + }, + }), + ); + + await expect(discovery.getBaseUrl('plugin/beta')).resolves.toBe( + 'http://common-backend:8080/api/plugin%2Fbeta', + ); + await expect(discovery.getBaseUrl('plugin/alpha')).resolves.toBe( + 'http://localhost:80/api/plugin%2Falpha', + ); + await expect(discovery.getExternalBaseUrl('plugin/alpha')).resolves.toBe( + 'http://localhost:40/api/plugin%2Falpha', + ); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts new file mode 100644 index 0000000000..180c8706d5 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts @@ -0,0 +1,132 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import { readHttpServerOptions } from '@backstage/backend-app-api'; +import { DiscoveryService } from '@backstage/backend-plugin-api'; + +type Target = string | { internal: string; external: string }; + +/** + * HostDiscovery is a basic PluginEndpointDiscovery implementation + * that can handle plugins that are hosted in a single or multiple deployments. + * + * The deployment may be scaled horizontally, as long as the external URL + * is the same for all instances. However, internal URLs will always be + * resolved to the same host, so there won't be any balancing of internal traffic. + * + * @public + */ +export class HostDiscovery implements DiscoveryService { + /** + * Creates a new HostDiscovery discovery instance by reading + * from the `backend` config section, specifically the `.baseUrl` for + * discovering the external URL, and the `.listen` and `.https` config + * for the internal one. + * + * Can be overridden in config by providing a target and corresponding plugins in `discovery.endpoints`. + * eg. + * ```yaml + * discovery: + * endpoints: + * - target: https://internal.example.com/internal-catalog + * plugins: [catalog] + * - target: https://internal.example.com/secure/api/{{pluginId}} + * plugins: [auth, permission] + * - target: + * internal: https://internal.example.com/search + * external: https://example.com/search + * plugins: [search] + * ``` + * + * The basePath defaults to `/api`, meaning the default full internal + * path for the `catalog` plugin will be `http://localhost:7007/api/catalog`. + */ + static fromConfig(config: Config, options?: { basePath?: string }) { + const basePath = options?.basePath ?? '/api'; + const externalBaseUrl = config + .getString('backend.baseUrl') + .replace(/\/+$/, ''); + + const { + listen: { host: listenHost = '::', port: listenPort }, + } = readHttpServerOptions(config.getConfig('backend')); + const protocol = config.has('backend.https') ? 'https' : 'http'; + + // Translate bind-all to localhost, and support IPv6 + let host = listenHost; + if (host === '::' || host === '') { + // We use localhost instead of ::1, since IPv6-compatible systems should default + // to using IPv6 when they see localhost, but if the system doesn't support IPv6 + // things will still work. + host = 'localhost'; + } else if (host === '0.0.0.0') { + host = '127.0.0.1'; + } + if (host.includes(':')) { + host = `[${host}]`; + } + + const internalBaseUrl = `${protocol}://${host}:${listenPort}`; + + return new HostDiscovery( + internalBaseUrl + basePath, + externalBaseUrl + basePath, + config.getOptionalConfig('discovery'), + ); + } + + private constructor( + private readonly internalBaseUrl: string, + private readonly externalBaseUrl: string, + private readonly discoveryConfig: Config | undefined, + ) {} + + private getTargetFromConfig(pluginId: string, type: 'internal' | 'external') { + const endpoints = this.discoveryConfig?.getOptionalConfigArray('endpoints'); + + const target = endpoints + ?.find(endpoint => endpoint.getStringArray('plugins').includes(pluginId)) + ?.get('target'); + + if (!target) { + const baseUrl = + type === 'external' ? this.externalBaseUrl : this.internalBaseUrl; + + return `${baseUrl}/${encodeURIComponent(pluginId)}`; + } + + if (typeof target === 'string') { + return target.replace( + /\{\{\s*pluginId\s*\}\}/g, + encodeURIComponent(pluginId), + ); + } + + return target[type].replace( + /\{\{\s*pluginId\s*\}\}/g, + encodeURIComponent(pluginId), + ); + } + + async getBaseUrl(pluginId: string): Promise { + return this.getTargetFromConfig(pluginId, 'internal'); + } + + async getExternalBaseUrl(pluginId: string): Promise { + return this.getTargetFromConfig(pluginId, 'external'); + } +} diff --git a/plugins/devtools-backend/src/run.ts b/packages/backend-defaults/src/entrypoints/discovery/discoveryServiceFactory.ts similarity index 53% rename from plugins/devtools-backend/src/run.ts rename to packages/backend-defaults/src/entrypoints/discovery/discoveryServiceFactory.ts index d945aa13f0..bfc5a6a489 100644 --- a/plugins/devtools-backend/src/run.ts +++ b/packages/backend-defaults/src/entrypoints/discovery/discoveryServiceFactory.ts @@ -14,20 +14,19 @@ * limitations under the License. */ -import { getRootLogger } from '@backstage/backend-common'; -import yn from 'yn'; -import { startStandaloneServer } from './service/standaloneServer'; +import { + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { HostDiscovery } from './HostDiscovery'; -const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; -const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); -const logger = getRootLogger(); - -startStandaloneServer({ port, enableCors, logger }).catch(err => { - logger.error(err); - process.exit(1); -}); - -process.on('SIGINT', () => { - logger.info('CTRL+C pressed; exiting.'); - process.exit(0); +/** @public */ +export const discoveryServiceFactory = createServiceFactory({ + service: coreServices.discovery, + deps: { + config: coreServices.rootConfig, + }, + async factory({ config }) { + return HostDiscovery.fromConfig(config); + }, }); diff --git a/packages/backend-defaults/src/entrypoints/discovery/index.ts b/packages/backend-defaults/src/entrypoints/discovery/index.ts new file mode 100644 index 0000000000..ee4851271a --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/discovery/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { discoveryServiceFactory } from './discoveryServiceFactory'; +export { HostDiscovery } from './HostDiscovery'; diff --git a/packages/backend-defaults/src/entrypoints/lifecycle/index.ts b/packages/backend-defaults/src/entrypoints/lifecycle/index.ts new file mode 100644 index 0000000000..8dac4c26b4 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/lifecycle/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { lifecycleServiceFactory } from './lifecycleServiceFactory'; diff --git a/packages/backend-defaults/src/entrypoints/lifecycle/lifecycleServiceFactory.ts b/packages/backend-defaults/src/entrypoints/lifecycle/lifecycleServiceFactory.ts new file mode 100644 index 0000000000..3eb43d6c6e --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/lifecycle/lifecycleServiceFactory.ts @@ -0,0 +1,106 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + LifecycleService, + LifecycleServiceShutdownHook, + LifecycleServiceShutdownOptions, + LifecycleServiceStartupHook, + LifecycleServiceStartupOptions, + LoggerService, + PluginMetadataService, + RootLifecycleService, + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; + +/** @internal */ +export class BackendPluginLifecycleImpl implements LifecycleService { + constructor( + private readonly logger: LoggerService, + private readonly rootLifecycle: RootLifecycleService, + private readonly pluginMetadata: PluginMetadataService, + ) {} + + #hasStarted = false; + #startupTasks: Array<{ + hook: LifecycleServiceStartupHook; + options?: LifecycleServiceStartupOptions; + }> = []; + + addStartupHook( + hook: LifecycleServiceStartupHook, + options?: LifecycleServiceStartupOptions, + ): void { + if (this.#hasStarted) { + throw new Error('Attempted to add startup hook after startup'); + } + this.#startupTasks.push({ hook, options }); + } + + async startup(): Promise { + if (this.#hasStarted) { + return; + } + this.#hasStarted = true; + + this.logger.debug( + `Running ${this.#startupTasks.length} plugin startup tasks...`, + ); + await Promise.all( + this.#startupTasks.map(async ({ hook, options }) => { + const logger = options?.logger ?? this.logger; + try { + await hook(); + logger.debug(`Plugin startup hook succeeded`); + } catch (error) { + logger.error(`Plugin startup hook failed, ${error}`); + } + }), + ); + } + + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void { + const plugin = this.pluginMetadata.getId(); + this.rootLifecycle.addShutdownHook(hook, { + logger: options?.logger?.child({ plugin }) ?? this.logger, + }); + } +} + +/** + * Allows plugins to register shutdown hooks that are run when the process is about to exit. + * + * @public + */ +export const lifecycleServiceFactory = createServiceFactory({ + service: coreServices.lifecycle, + deps: { + logger: coreServices.logger, + rootLifecycle: coreServices.rootLifecycle, + pluginMetadata: coreServices.pluginMetadata, + }, + async factory({ rootLifecycle, logger, pluginMetadata }) { + return new BackendPluginLifecycleImpl( + logger, + rootLifecycle, + pluginMetadata, + ); + }, +}); diff --git a/packages/backend-defaults/src/entrypoints/permissions/index.ts b/packages/backend-defaults/src/entrypoints/permissions/index.ts new file mode 100644 index 0000000000..781dda31a0 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/permissions/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { permissionsServiceFactory } from './permissionsServiceFactory'; diff --git a/packages/backend-defaults/src/entrypoints/permissions/permissionsServiceFactory.ts b/packages/backend-defaults/src/entrypoints/permissions/permissionsServiceFactory.ts new file mode 100644 index 0000000000..f675dd6719 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/permissions/permissionsServiceFactory.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { ServerPermissionClient } from '@backstage/plugin-permission-node'; + +/** + * @public + */ +export const permissionsServiceFactory = createServiceFactory({ + service: coreServices.permissions, + deps: { + auth: coreServices.auth, + config: coreServices.rootConfig, + discovery: coreServices.discovery, + tokenManager: coreServices.tokenManager, + }, + async factory({ auth, config, discovery, tokenManager }) { + return ServerPermissionClient.fromConfig(config, { + auth, + discovery, + tokenManager, + }); + }, +}); diff --git a/packages/backend-defaults/src/entrypoints/rootConfig/index.ts b/packages/backend-defaults/src/entrypoints/rootConfig/index.ts new file mode 100644 index 0000000000..1775ef2efc --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootConfig/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { rootConfigServiceFactory } from './rootConfigServiceFactory'; +export type { RootConfigFactoryOptions } from './rootConfigServiceFactory'; diff --git a/packages/backend-defaults/src/entrypoints/rootConfig/rootConfigServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootConfig/rootConfigServiceFactory.ts new file mode 100644 index 0000000000..92d1a89c0f --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootConfig/rootConfigServiceFactory.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { + ConfigSources, + RemoteConfigSourceOptions, +} from '@backstage/config-loader'; + +/** + * @public + */ +export interface RootConfigFactoryOptions { + /** + * Process arguments to use instead of the default `process.argv()`. + */ + argv?: string[]; + + /** + * Enables and sets options for remote configuration loading. + */ + remote?: Pick; + watch?: boolean; +} + +/** + * @public + */ +export const rootConfigServiceFactory = createServiceFactory( + (options?: RootConfigFactoryOptions) => ({ + service: coreServices.rootConfig, + deps: {}, + async factory() { + const source = ConfigSources.default({ + argv: options?.argv, + remote: options?.remote, + watch: options?.watch, + }); + console.log(`Loading config from ${source}`); + return await ConfigSources.toConfig(source); + }, + }), +); diff --git a/packages/backend-defaults/src/entrypoints/rootLifecycle/index.ts b/packages/backend-defaults/src/entrypoints/rootLifecycle/index.ts new file mode 100644 index 0000000000..86589cd23e --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLifecycle/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { rootLifecycleServiceFactory } from './rootLifecycleServiceFactory'; diff --git a/packages/backend-defaults/src/entrypoints/rootLifecycle/rootLifecycleServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/rootLifecycle/rootLifecycleServiceFactory.test.ts new file mode 100644 index 0000000000..91cb9031ae --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLifecycle/rootLifecycleServiceFactory.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { BackendLifecycleImpl } from './rootLifecycleServiceFactory'; + +describe('lifecycleService', () => { + it('should execute registered shutdown hook', async () => { + const service = new BackendLifecycleImpl(getVoidLogger()); + const hook = jest.fn(); + service.addShutdownHook(() => hook()); + // should not execute the hook more than once. + await service.shutdown(); + await service.shutdown(); + await service.shutdown(); + expect(hook).toHaveBeenCalledTimes(1); + }); + + it('should not throw errors', async () => { + const service = new BackendLifecycleImpl(getVoidLogger()); + service.addShutdownHook(() => { + throw new Error('oh no'); + }); + await expect(service.shutdown()).resolves.toBeUndefined(); + }); + + it('should not throw async errors', async () => { + const service = new BackendLifecycleImpl(getVoidLogger()); + service.addShutdownHook(async () => { + throw new Error('oh no'); + }); + await expect(service.shutdown()).resolves.toBeUndefined(); + }); + + it('should reject hooks after trigger', async () => { + const service = new BackendLifecycleImpl(getVoidLogger()); + await service.startup(); + expect(() => { + service.addStartupHook(() => {}); + }).toThrow('Attempted to add startup hook after startup'); + + await service.shutdown(); + expect(() => { + service.addShutdownHook(() => {}); + }).toThrow('Attempted to add shutdown hook after shutdown'); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/rootLifecycle/rootLifecycleServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootLifecycle/rootLifecycleServiceFactory.ts new file mode 100644 index 0000000000..197f99b97a --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLifecycle/rootLifecycleServiceFactory.ts @@ -0,0 +1,120 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createServiceFactory, + coreServices, + LifecycleServiceStartupHook, + LifecycleServiceStartupOptions, + LifecycleServiceShutdownHook, + LifecycleServiceShutdownOptions, + RootLifecycleService, + LoggerService, +} from '@backstage/backend-plugin-api'; + +/** @internal */ +export class BackendLifecycleImpl implements RootLifecycleService { + constructor(private readonly logger: LoggerService) {} + + #hasStarted = false; + #startupTasks: Array<{ + hook: LifecycleServiceStartupHook; + options?: LifecycleServiceStartupOptions; + }> = []; + + addStartupHook( + hook: LifecycleServiceStartupHook, + options?: LifecycleServiceStartupOptions, + ): void { + if (this.#hasStarted) { + throw new Error('Attempted to add startup hook after startup'); + } + this.#startupTasks.push({ hook, options }); + } + + async startup(): Promise { + if (this.#hasStarted) { + return; + } + this.#hasStarted = true; + + this.logger.debug(`Running ${this.#startupTasks.length} startup tasks...`); + await Promise.all( + this.#startupTasks.map(async ({ hook, options }) => { + const logger = options?.logger ?? this.logger; + try { + await hook(); + logger.debug(`Startup hook succeeded`); + } catch (error) { + logger.error(`Startup hook failed, ${error}`); + } + }), + ); + } + + #hasShutdown = false; + #shutdownTasks: Array<{ + hook: LifecycleServiceShutdownHook; + options?: LifecycleServiceShutdownOptions; + }> = []; + + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void { + if (this.#hasShutdown) { + throw new Error('Attempted to add shutdown hook after shutdown'); + } + this.#shutdownTasks.push({ hook, options }); + } + + async shutdown(): Promise { + if (this.#hasShutdown) { + return; + } + this.#hasShutdown = true; + + this.logger.debug( + `Running ${this.#shutdownTasks.length} shutdown tasks...`, + ); + await Promise.all( + this.#shutdownTasks.map(async ({ hook, options }) => { + const logger = options?.logger ?? this.logger; + try { + await hook(); + logger.debug(`Shutdown hook succeeded`); + } catch (error) { + logger.error(`Shutdown hook failed, ${error}`); + } + }), + ); + } +} + +/** + * Allows plugins to register shutdown hooks that are run when the process is about to exit. + * + * @public + */ +export const rootLifecycleServiceFactory = createServiceFactory({ + service: coreServices.rootLifecycle, + deps: { + logger: coreServices.rootLogger, + }, + async factory({ logger }) { + return new BackendLifecycleImpl(logger); + }, +}); diff --git a/plugins/search-backend/src/run.ts b/packages/backend-defaults/src/entrypoints/scheduler/database/migrateBackendTasks.ts similarity index 53% rename from plugins/search-backend/src/run.ts rename to packages/backend-defaults/src/entrypoints/scheduler/database/migrateBackendTasks.ts index 53d4e4334a..959e6097f4 100644 --- a/plugins/search-backend/src/run.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/database/migrateBackendTasks.ts @@ -14,20 +14,18 @@ * limitations under the License. */ -import { getRootLogger } from '@backstage/backend-common'; -import yn from 'yn'; -import { startStandaloneServer } from './service/standaloneServer'; +import { resolvePackagePath } from '@backstage/backend-common'; +import { Knex } from 'knex'; +import { DB_MIGRATIONS_TABLE } from './tables'; -const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; -const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); -const logger = getRootLogger(); +export async function migrateBackendTasks(knex: Knex): Promise { + const migrationsDir = resolvePackagePath( + '@backstage/backend-defaults', + 'migrations/scheduler', + ); -startStandaloneServer({ port, enableCors, logger }).catch(err => { - logger.error(err); - process.exit(1); -}); - -process.on('SIGINT', () => { - logger.info('CTRL+C pressed; exiting.'); - process.exit(0); -}); + await knex.migrate.latest({ + directory: migrationsDir, + tableName: DB_MIGRATIONS_TABLE, + }); +} diff --git a/packages/backend-defaults/src/entrypoints/scheduler/database/tables.ts b/packages/backend-defaults/src/entrypoints/scheduler/database/tables.ts new file mode 100644 index 0000000000..63aad6e42a --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/database/tables.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +export const DB_MIGRATIONS_TABLE = 'backstage_backend_tasks__knex_migrations'; +export const DB_TASKS_TABLE = 'backstage_backend_tasks__tasks'; + +export type DbTasksRow = { + id: string; + settings_json: string; + next_run_start_at: Date; + current_run_ticket?: string; + current_run_started_at?: Date | string; + current_run_expires_at?: Date | string; +}; diff --git a/packages/backend-defaults/src/entrypoints/scheduler/index.ts b/packages/backend-defaults/src/entrypoints/scheduler/index.ts new file mode 100644 index 0000000000..77d4ea561d --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { schedulerServiceFactory } from './schedulerServiceFactory'; diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/LocalTaskWorker.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/LocalTaskWorker.test.ts new file mode 100644 index 0000000000..4fd4949478 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/LocalTaskWorker.test.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { LocalTaskWorker } from './LocalTaskWorker'; + +describe('LocalTaskWorker', () => { + const logger = getVoidLogger(); + + it('runs the happy path (with iso duration) and handles cancellation', async () => { + const fn = jest.fn(); + const controller = new AbortController(); + + const worker = new LocalTaskWorker('a', fn, logger); + worker.start( + { + version: 2, + initialDelayDuration: 'PT0.2S', + cadence: 'PT0.2S', + timeoutAfterDuration: 'PT1S', + }, + { signal: controller.signal }, + ); + + // TODO(freben): Rewrite to fake timers - tried, but it wouldn't work + expect(fn).toHaveBeenCalledTimes(0); + await new Promise(r => setTimeout(r, 100)); + expect(fn).toHaveBeenCalledTimes(0); + await new Promise(r => setTimeout(r, 200)); + expect(fn).toHaveBeenCalledTimes(1); + await new Promise(r => setTimeout(r, 200)); + expect(fn).toHaveBeenCalledTimes(2); + controller.abort(); + await new Promise(r => setTimeout(r, 200)); + expect(fn).toHaveBeenCalledTimes(2); + }); + + it('runs the happy path (with a cron expression) and handles cancellation', async () => { + const fn = jest.fn(); + const controller = new AbortController(); + + // Await until system time is just past a second boundary (since cron is + // wall clock based) + await new Promise(r => setTimeout(r, 1000 - (Date.now() % 1000) + 10)); + + const worker = new LocalTaskWorker('a', fn, logger); + worker.start( + { + version: 2, + initialDelayDuration: 'PT0.2S', + cadence: '* * * * * *', + timeoutAfterDuration: 'PT1S', + }, + { signal: controller.signal }, + ); + + // TODO(freben): Rewrite to fake timers - tried, but it wouldn't work + expect(fn).toHaveBeenCalledTimes(0); + await new Promise(r => setTimeout(r, 100)); + expect(fn).toHaveBeenCalledTimes(0); + await new Promise(r => setTimeout(r, 200)); + expect(fn).toHaveBeenCalledTimes(1); + await new Promise(r => setTimeout(r, 1000)); + expect(fn).toHaveBeenCalledTimes(2); + controller.abort(); + await new Promise(r => setTimeout(r, 1000)); + expect(fn).toHaveBeenCalledTimes(2); + }); + + it('can trigger to abort wait', async () => { + const fn = jest.fn(); + const controller = new AbortController(); + + const worker = new LocalTaskWorker('a', fn, logger); + worker.start( + { + version: 2, + initialDelayDuration: 'PT0.2S', + cadence: 'PT0.2S', + timeoutAfterDuration: 'PT1S', + }, + { signal: controller.signal }, + ); + + // TODO(freben): Rewrite to fake timers - tried, but it wouldn't work + expect(fn).toHaveBeenCalledTimes(0); + await new Promise(r => setTimeout(r, 100)); + expect(fn).toHaveBeenCalledTimes(0); + await new Promise(r => setTimeout(r, 200)); + expect(fn).toHaveBeenCalledTimes(1); + worker.trigger(); + await new Promise(r => setTimeout(r, 10)); + expect(fn).toHaveBeenCalledTimes(2); + controller.abort(); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/LocalTaskWorker.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/LocalTaskWorker.ts new file mode 100644 index 0000000000..3510a855e9 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/LocalTaskWorker.ts @@ -0,0 +1,155 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LoggerService } from '@backstage/backend-plugin-api'; +import { SchedulerServiceTaskFunction } from '@backstage/backend-plugin-api'; +import { ConflictError } from '@backstage/errors'; +import { CronTime } from 'cron'; +import { DateTime, Duration } from 'luxon'; +import { TaskSettingsV2 } from './types'; +import { delegateAbortController, sleep } from './util'; + +/** + * Implements tasks that run locally without cross-host collaboration. + * + * @private + */ +export class LocalTaskWorker { + private abortWait: AbortController | undefined; + + constructor( + private readonly taskId: string, + private readonly fn: SchedulerServiceTaskFunction, + private readonly logger: LoggerService, + ) {} + + start(settings: TaskSettingsV2, options?: { signal?: AbortSignal }) { + this.logger.info( + `Task worker starting: ${this.taskId}, ${JSON.stringify(settings)}`, + ); + + (async () => { + let attemptNum = 1; + for (;;) { + try { + if (settings.initialDelayDuration) { + await this.sleep( + Duration.fromISO(settings.initialDelayDuration), + options?.signal, + ); + } + + while (!options?.signal?.aborted) { + const startTime = process.hrtime(); + await this.runOnce(settings, options?.signal); + const timeTaken = process.hrtime(startTime); + await this.waitUntilNext( + settings, + (timeTaken[0] + timeTaken[1] / 1e9) * 1000, + options?.signal, + ); + } + + this.logger.info(`Task worker finished: ${this.taskId}`); + attemptNum = 0; + break; + } catch (e) { + attemptNum += 1; + this.logger.warn( + `Task worker failed unexpectedly, attempt number ${attemptNum}, ${e}`, + ); + await sleep(Duration.fromObject({ seconds: 1 })); + } + } + })(); + } + + trigger(): void { + if (!this.abortWait) { + throw new ConflictError(`Task ${this.taskId} is currently running`); + } + this.abortWait.abort(); + } + + /** + * Makes a single attempt at running the task to completion. + */ + private async runOnce( + settings: TaskSettingsV2, + signal?: AbortSignal, + ): Promise { + // Abort the task execution either if the worker is stopped, or if the + // task timeout is hit + const taskAbortController = delegateAbortController(signal); + const timeoutHandle = setTimeout(() => { + taskAbortController.abort(); + }, Duration.fromISO(settings.timeoutAfterDuration).as('milliseconds')); + + try { + await this.fn(taskAbortController.signal); + } catch (e) { + // ignore intentionally + } + + // release resources + clearTimeout(timeoutHandle); + taskAbortController.abort(); + } + + /** + * Sleeps until it's time to run the task again. + */ + private async waitUntilNext( + settings: TaskSettingsV2, + lastRunMillis: number, + signal?: AbortSignal, + ) { + if (signal?.aborted) { + return; + } + + const isCron = !settings.cadence.startsWith('P'); + let dt: number; + + if (isCron) { + const nextRun = +new CronTime(settings.cadence).sendAt().toJSDate(); + dt = nextRun - Date.now(); + } else { + dt = + Duration.fromISO(settings.cadence).as('milliseconds') - lastRunMillis; + } + + dt = Math.max(dt, 0); + + this.logger.debug( + `task: ${this.taskId} will next occur around ${DateTime.now().plus( + Duration.fromMillis(dt), + )}`, + ); + + await this.sleep(Duration.fromMillis(dt), signal); + } + + private async sleep( + duration: Duration, + abortSignal?: AbortSignal, + ): Promise { + this.abortWait = delegateAbortController(abortSignal); + await sleep(duration, this.abortWait.signal); + this.abortWait.abort(); // cleans up resources + this.abortWait = undefined; + } +} diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts new file mode 100644 index 0000000000..57faa6faf8 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts @@ -0,0 +1,349 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { ConflictError, NotFoundError } from '@backstage/errors'; +import { Duration } from 'luxon'; +import { migrateBackendTasks } from '../database/migrateBackendTasks'; +import { + PluginTaskSchedulerImpl, + parseDuration, +} from './PluginTaskSchedulerImpl'; + +function defer() { + let resolve = () => {}; + const promise = new Promise(_resolve => { + resolve = _resolve; + }); + return { promise, resolve }; +} + +jest.setTimeout(60_000); + +describe('PluginTaskManagerImpl', () => { + const databases = TestDatabases.create({ + ids: ['POSTGRES_16', 'POSTGRES_12', 'SQLITE_3'], + }); + + beforeAll(async () => { + // Make sure all databases are running before mocking timers, in case of testcontainers + await Promise.all( + databases.eachSupportedId().map(([id]) => databases.init(id)), + ); + + jest.useFakeTimers(); + }, 60_000); + + async function init(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + const manager = new PluginTaskSchedulerImpl( + async () => knex, + getVoidLogger(), + ); + return { knex, manager }; + } + + // This is just to test the wrapper code; most of the actual tests are in + // TaskWorker.test.ts + describe('scheduleTask with global scope', () => { + it.each(databases.eachSupportedId())( + 'can run the v1 happy path, %p', + async databaseId => { + const { manager } = await init(databaseId); + + const fn = jest.fn(); + const promise = new Promise(resolve => fn.mockImplementation(resolve)); + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromMillis(5000), + fn, + scope: 'global', + }); + + await promise; + expect(fn).toHaveBeenCalledWith(expect.any(AbortSignal)); + }, + ); + + it.each(databases.eachSupportedId())( + 'can run the v2 happy path, %p', + async databaseId => { + const { manager } = await init(databaseId); + + const fn = jest.fn(); + const promise = new Promise(resolve => fn.mockImplementation(resolve)); + await manager.scheduleTask({ + id: 'task2', + timeout: Duration.fromMillis(5000), + frequency: { cron: '* * * * * *' }, + fn, + scope: 'global', + }); + + await promise; + expect(fn).toHaveBeenCalledWith(expect.any(AbortSignal)); + }, + ); + }); + + describe('triggerTask with global scope', () => { + it.each(databases.eachSupportedId())( + 'can manually trigger a task, %p', + async databaseId => { + const { manager } = await init(databaseId); + + const fn = jest.fn(); + const promise = new Promise(resolve => fn.mockImplementation(resolve)); + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromObject({ years: 1 }), + initialDelay: Duration.fromObject({ years: 1 }), + fn, + scope: 'global', + }); + + await manager.triggerTask('task1'); + jest.advanceTimersByTime(5000); + + await promise; + expect(fn).toHaveBeenCalledWith(expect.any(AbortSignal)); + }, + ); + + it.each(databases.eachSupportedId())( + 'cant trigger a non-existent task, %p', + async databaseId => { + const { manager } = await init(databaseId); + + const fn = jest.fn(); + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromObject({ years: 1 }), + fn, + scope: 'global', + }); + + await expect(() => manager.triggerTask('task2')).rejects.toThrow( + NotFoundError, + ); + }, + ); + + it.each(databases.eachSupportedId())( + 'cant trigger a running task, %p', + async databaseId => { + const { manager } = await init(databaseId); + + const { promise, resolve } = defer(); + + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromObject({ years: 1 }), + fn: async () => { + resolve(); + await new Promise(r => setTimeout(r, 20000)); + }, + scope: 'global', + }); + + await promise; + await expect(() => manager.triggerTask('task1')).rejects.toThrow( + ConflictError, + ); + }, + ); + }); + + // This is just to test the wrapper code; most of the actual tests are in + // TaskWorker.test.ts + describe('scheduleTask with local scope', () => { + it('can run the v1 happy path', async () => { + const { manager } = await init('SQLITE_3'); + + const fn = jest.fn(); + const promise = new Promise(resolve => fn.mockImplementation(resolve)); + await manager.scheduleTask({ + id: 'task1', + timeout: { milliseconds: 5000 }, + frequency: { milliseconds: 5000 }, + fn, + scope: 'local', + }); + + await promise; + expect(fn).toHaveBeenCalledWith(expect.any(AbortSignal)); + }, 60_000); + + it('can run the v2 happy path', async () => { + const { manager } = await init('SQLITE_3'); + + const fn = jest.fn(); + const promise = new Promise(resolve => fn.mockImplementation(resolve)); + await manager.scheduleTask({ + id: 'task2', + timeout: Duration.fromMillis(5000), + frequency: { cron: '* * * * * *' }, + fn, + scope: 'local', + }); + + await promise; + expect(fn).toHaveBeenCalledWith(expect.any(AbortSignal)); + }, 60_000); + }); + + describe('triggerTask with local scope', () => { + it('can manually trigger a task', async () => { + const { manager } = await init('SQLITE_3'); + + const fn = jest.fn(); + const promise = new Promise(resolve => fn.mockImplementation(resolve)); + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromObject({ years: 1 }), + initialDelay: Duration.fromObject({ years: 1 }), + fn, + scope: 'local', + }); + + await manager.triggerTask('task1'); + jest.advanceTimersByTime(5000); + + await promise; + expect(fn).toHaveBeenCalledWith(expect.any(AbortSignal)); + }, 60_000); + + it('cant trigger a non-existent task', async () => { + const { manager } = await init('SQLITE_3'); + + const fn = jest.fn(); + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromObject({ years: 1 }), + fn, + scope: 'local', + }); + + await expect(() => manager.triggerTask('task2')).rejects.toThrow( + NotFoundError, + ); + }, 60_000); + + it('cant trigger a running task', async () => { + const { manager } = await init('SQLITE_3'); + + const { promise, resolve } = defer(); + + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromObject({ years: 1 }), + fn: async () => { + resolve(); + await new Promise(r => setTimeout(r, 20000)); + }, + scope: 'local', + }); + + await promise; + await expect(() => manager.triggerTask('task1')).rejects.toThrow( + ConflictError, + ); + }, 60_000); + }); + + // This is just to test the wrapper code; most of the actual tests are in + // TaskWorker.test.ts + describe('createScheduledTaskRunner', () => { + it.each(databases.eachSupportedId())( + 'can run the happy path, %p', + async databaseId => { + const { manager } = await init(databaseId); + + const fn = jest.fn(); + const promise = new Promise(resolve => fn.mockImplementation(resolve)); + await manager + .createScheduledTaskRunner({ + timeout: Duration.fromMillis(5000), + frequency: Duration.fromMillis(5000), + scope: 'global', + }) + .run({ + id: 'task1', + fn, + }); + + await promise; + expect(fn).toHaveBeenCalledWith(expect.any(AbortSignal)); + }, + ); + }); + + describe('can fetch task ids', () => { + it.each(databases.eachSupportedId())( + 'can fetch both global and local task ids, %p', + async databaseId => { + const { manager } = await init(databaseId); + const fn = jest.fn(); + + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromMillis(5000), + fn, + scope: 'global', + }); + + await manager.scheduleTask({ + id: 'task2', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromMillis(5000), + fn, + scope: 'local', + }); + + await expect(manager.getScheduledTasks()).resolves.toEqual([ + { + id: 'task1', + scope: 'global', + settings: expect.objectContaining({ cadence: 'PT5S' }), + }, + { + id: 'task2', + scope: 'local', + settings: expect.objectContaining({ cadence: 'PT5S' }), + }, + ]); + }, + ); + }); + + describe('parseDuration', () => { + it('should parse durations', () => { + expect(parseDuration({ milliseconds: 5000 })).toEqual('PT5S'); + expect(parseDuration(Duration.fromMillis(5000))).toEqual('PT5S'); + expect(parseDuration({ cron: '1 * * * *' })).toEqual('1 * * * *'); + }); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts new file mode 100644 index 0000000000..62b36e024c --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts @@ -0,0 +1,170 @@ +/* + * 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 { + LoggerService, + SchedulerService, + SchedulerServiceTaskDescriptor, + SchedulerServiceTaskFunction, + SchedulerServiceTaskInvocationDefinition, + SchedulerServiceTaskRunner, + SchedulerServiceTaskScheduleDefinition, +} from '@backstage/backend-plugin-api'; +import { Counter, Histogram, metrics } from '@opentelemetry/api'; +import { Knex } from 'knex'; +import { Duration } from 'luxon'; +import { LocalTaskWorker } from './LocalTaskWorker'; +import { TaskWorker } from './TaskWorker'; +import { TaskSettingsV2 } from './types'; +import { validateId } from './util'; + +/** + * Implements the actual task management. + */ +export class PluginTaskSchedulerImpl implements SchedulerService { + private readonly localTasksById = new Map(); + private readonly allScheduledTasks: SchedulerServiceTaskDescriptor[] = []; + + private readonly counter: Counter; + private readonly duration: Histogram; + + constructor( + private readonly databaseFactory: () => Promise, + private readonly logger: LoggerService, + ) { + const meter = metrics.getMeter('default'); + this.counter = meter.createCounter('backend_tasks.task.runs.count', { + description: 'Total number of times a task has been run', + }); + this.duration = meter.createHistogram('backend_tasks.task.runs.duration', { + description: 'Histogram of task run durations', + unit: 'seconds', + }); + } + + async triggerTask(id: string): Promise { + const localTask = this.localTasksById.get(id); + if (localTask) { + localTask.trigger(); + return; + } + + const knex = await this.databaseFactory(); + await TaskWorker.trigger(knex, id); + } + + async scheduleTask( + task: SchedulerServiceTaskScheduleDefinition & + SchedulerServiceTaskInvocationDefinition, + ): Promise { + validateId(task.id); + const scope = task.scope ?? 'global'; + + const settings: TaskSettingsV2 = { + version: 2, + cadence: parseDuration(task.frequency), + initialDelayDuration: + task.initialDelay && parseDuration(task.initialDelay), + timeoutAfterDuration: parseDuration(task.timeout), + }; + + if (scope === 'global') { + const knex = await this.databaseFactory(); + const worker = new TaskWorker( + task.id, + this.wrapInMetrics(task.fn, { labels: { taskId: task.id, scope } }), + knex, + this.logger.child({ task: task.id }), + ); + await worker.start(settings, { signal: task.signal }); + } else { + const worker = new LocalTaskWorker( + task.id, + this.wrapInMetrics(task.fn, { labels: { taskId: task.id, scope } }), + this.logger.child({ task: task.id }), + ); + worker.start(settings, { signal: task.signal }); + this.localTasksById.set(task.id, worker); + } + + this.allScheduledTasks.push({ + id: task.id, + scope: scope, + settings: settings, + }); + } + + createScheduledTaskRunner( + schedule: SchedulerServiceTaskScheduleDefinition, + ): SchedulerServiceTaskRunner { + return { + run: async task => { + await this.scheduleTask({ ...task, ...schedule }); + }, + }; + } + + async getScheduledTasks(): Promise { + return this.allScheduledTasks; + } + + private wrapInMetrics( + fn: SchedulerServiceTaskFunction, + opts: { labels: Record }, + ): SchedulerServiceTaskFunction { + return async abort => { + const labels = { + ...opts.labels, + }; + this.counter.add(1, { ...labels, result: 'started' }); + + const startTime = process.hrtime(); + + try { + await fn(abort); + labels.result = 'completed'; + } catch (ex) { + labels.result = 'failed'; + throw ex; + } finally { + const delta = process.hrtime(startTime); + const endTime = delta[0] + delta[1] / 1e9; + this.counter.add(1, labels); + this.duration.record(endTime, labels); + } + }; + } +} + +export function parseDuration( + frequency: SchedulerServiceTaskScheduleDefinition['frequency'], +): string { + if ('cron' in frequency) { + return frequency.cron; + } + + const parsed = Duration.isDuration(frequency) + ? frequency + : Duration.fromObject(frequency); + + if (!parsed.isValid) { + throw new Error( + `Invalid duration, ${parsed.invalidReason}: ${parsed.invalidExplanation}`, + ); + } + + return parsed.toISO()!; +} diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerJanitor.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerJanitor.test.ts new file mode 100644 index 0000000000..82133a9e54 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerJanitor.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { TestDatabases } from '@backstage/backend-test-utils'; +import { Knex } from 'knex'; +import { Duration } from 'luxon'; +import waitForExpect from 'wait-for-expect'; +import { migrateBackendTasks } from '../database/migrateBackendTasks'; +import { DB_TASKS_TABLE, DbTasksRow } from '../database/tables'; +import { PluginTaskSchedulerJanitor } from './PluginTaskSchedulerJanitor'; +import { createTestScopedSignal } from './__testUtils__/createTestScopedSignal'; + +const insertTask = async (knex: Knex, task: DbTasksRow) => { + return knex(DB_TASKS_TABLE) + .insert(task) + .onConflict('id') + .merge(['settings_json']); +}; + +const getTask = async (knex: Knex): Promise => { + return (await knex(DB_TASKS_TABLE))[0]; +}; + +describe('PluginTaskSchedulerJanitor', () => { + const logger = getVoidLogger(); + const databases = TestDatabases.create({ + ids: [ + /* 'MYSQL_8' not supported yet */ + 'POSTGRES_16', + 'POSTGRES_12', + 'SQLITE_3', + 'MYSQL_8', + ], + }); + const testScopedSignal = createTestScopedSignal(); + + jest.setTimeout(60_000); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it.each(databases.eachSupportedId())( + 'Should update date if current_run_expires_at expires, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const dateYesterday = new Date( + new Date().setDate(new Date().getDate() - 1), + ); + + await insertTask(knex, { + id: 'task1', + settings_json: '', + next_run_start_at: new Date('2023-03-01 00:00:00'), + current_run_ticket: 'ticket', + current_run_started_at: dateYesterday, + current_run_expires_at: dateYesterday, + }); + + const worker = new PluginTaskSchedulerJanitor({ + waitBetweenRuns: Duration.fromObject({ milliseconds: 20 }), + knex, + logger, + }); + + worker.start(testScopedSignal()); + + await waitForExpect(async () => { + await expect(getTask(knex)).resolves.toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }), + ); + }); + }, + ); +}); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerJanitor.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerJanitor.ts new file mode 100644 index 0000000000..b0cd4572cf --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerJanitor.ts @@ -0,0 +1,96 @@ +/* + * 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 { LoggerService } from '@backstage/backend-plugin-api'; +import { Knex } from 'knex'; +import { Duration } from 'luxon'; +import { DB_TASKS_TABLE, DbTasksRow } from '../database/tables'; +import { sleep } from './util'; + +/** + * Makes sure to auto-expire and clean up things that time out or for other + * reasons should not be left lingering. + */ +export class PluginTaskSchedulerJanitor { + private readonly knex: Knex; + private readonly waitBetweenRuns: Duration; + private readonly logger: LoggerService; + + constructor(options: { + knex: Knex; + waitBetweenRuns: Duration; + logger: LoggerService; + }) { + this.knex = options.knex; + this.waitBetweenRuns = options.waitBetweenRuns; + this.logger = options.logger; + } + + async start(abortSignal?: AbortSignal) { + while (!abortSignal?.aborted) { + try { + await this.runOnce(); + } catch (e) { + this.logger.warn(`Error while performing janitorial tasks, ${e}`); + } + + await sleep(this.waitBetweenRuns, abortSignal); + } + } + + private async runOnce() { + const dbNull = this.knex.raw('null'); + const configClient = this.knex.client.config.client; + + let tasks: Array<{ id: string }>; + if (configClient.includes('sqlite3') || configClient.includes('mysql')) { + tasks = await this.knex(DB_TASKS_TABLE) + .select('id') + .where('current_run_expires_at', '<', this.knex.fn.now()); + await this.knex(DB_TASKS_TABLE) + .whereIn( + 'id', + tasks.map(t => t.id), + ) + .update({ + current_run_ticket: dbNull, + current_run_started_at: dbNull, + current_run_expires_at: dbNull, + }); + } else { + tasks = await this.knex(DB_TASKS_TABLE) + .where('current_run_expires_at', '<', this.knex.fn.now()) + .update({ + current_run_ticket: dbNull, + current_run_started_at: dbNull, + current_run_expires_at: dbNull, + }) + .returning(['id']); + } + + // In rare cases, knex drivers may ignore "returning", and return the number + // of rows changed instead + if (typeof tasks === 'number') { + if (tasks > 0) { + this.logger.warn(`${tasks} tasks timed out and were lost`); + } + } else { + for (const { id } of tasks) { + this.logger.warn(`Task timed out and was lost: ${id}`); + } + } + } +} diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskScheduler.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskScheduler.test.ts new file mode 100644 index 0000000000..d286eaee38 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskScheduler.test.ts @@ -0,0 +1,84 @@ +/* + * 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 { DatabaseManager, getVoidLogger } from '@backstage/backend-common'; +import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { Duration } from 'luxon'; +import waitForExpect from 'wait-for-expect'; +import { TaskScheduler } from './TaskScheduler'; +import { createTestScopedSignal } from './__testUtils__/createTestScopedSignal'; + +jest.setTimeout(60_000); + +describe('TaskScheduler', () => { + const logger = getVoidLogger(); + const databases = TestDatabases.create(); + const testScopedSignal = createTestScopedSignal(); + + async function createDatabase( + databaseId: TestDatabaseId, + ): Promise { + const knex = await databases.init(databaseId); + const databaseManager: Partial = { + forPlugin: () => ({ + getClient: async () => knex, + }), + }; + return databaseManager as DatabaseManager; + } + + it.each(databases.eachSupportedId())( + 'can return a working v1 plugin impl, %p', + async databaseId => { + const database = await createDatabase(databaseId); + const manager = new TaskScheduler(database, logger).forPlugin('test'); + const fn = jest.fn(); + + await manager.scheduleTask({ + id: 'task1', + timeout: Duration.fromMillis(5000), + frequency: Duration.fromMillis(5000), + signal: testScopedSignal(), + fn, + }); + + await waitForExpect(() => { + expect(fn).toHaveBeenCalled(); + }); + }, + ); + + it.each(databases.eachSupportedId())( + 'can return a working v2 plugin impl, %p', + async databaseId => { + const database = await createDatabase(databaseId); + const manager = new TaskScheduler(database, logger).forPlugin('test'); + const fn = jest.fn(); + + await manager.scheduleTask({ + id: 'task2', + timeout: Duration.fromMillis(5000), + frequency: { cron: '* * * * * *' }, + signal: testScopedSignal(), + fn, + }); + + await waitForExpect(() => { + expect(fn).toHaveBeenCalled(); + }); + }, + ); +}); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskScheduler.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskScheduler.ts new file mode 100644 index 0000000000..21a19ce700 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskScheduler.ts @@ -0,0 +1,98 @@ +/* + * 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 { + DatabaseManager, + getRootLogger, + LegacyRootDatabaseService, + PluginDatabaseManager, +} from '@backstage/backend-common'; +import { + LoggerService, + RootConfigService, + SchedulerService, +} from '@backstage/backend-plugin-api'; +import { once } from 'lodash'; +import { Duration } from 'luxon'; +import { migrateBackendTasks } from '../database/migrateBackendTasks'; +import { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl'; +import { PluginTaskSchedulerJanitor } from './PluginTaskSchedulerJanitor'; + +/** + * Deals with the scheduling of distributed tasks. + */ +export class TaskScheduler { + static fromConfig( + config: RootConfigService, + options?: { + databaseManager?: LegacyRootDatabaseService; + logger?: LoggerService; + }, + ): TaskScheduler { + const databaseManager = + options?.databaseManager ?? DatabaseManager.fromConfig(config); + const logger = (options?.logger || getRootLogger()).child({ + type: 'taskManager', + }); + return new TaskScheduler(databaseManager, logger); + } + + constructor( + private readonly databaseManager: LegacyRootDatabaseService, + private readonly logger: LoggerService, + ) {} + + /** + * Instantiates a task manager instance for the given plugin. + * + * @param pluginId - The unique ID of the plugin, for example "catalog" + * @returns A {@link SchedulerService} instance + */ + forPlugin(pluginId: string): SchedulerService { + return TaskScheduler.forPlugin({ + pluginId, + databaseManager: this.databaseManager.forPlugin(pluginId), + logger: this.logger, + }); + } + + static forPlugin(opts: { + pluginId: string; + databaseManager: PluginDatabaseManager; + logger: LoggerService; + }): SchedulerService { + const databaseFactory = once(async () => { + const knex = await opts.databaseManager.getClient(); + + if (!opts.databaseManager.migrations?.skip) { + await migrateBackendTasks(knex); + } + + if (process.env.NODE_ENV !== 'test') { + const janitor = new PluginTaskSchedulerJanitor({ + knex, + waitBetweenRuns: Duration.fromObject({ minutes: 1 }), + logger: opts.logger, + }); + janitor.start(); + } + + return knex; + }); + + return new PluginTaskSchedulerImpl(databaseFactory, opts.logger); + } +} diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts new file mode 100644 index 0000000000..20d9b63859 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts @@ -0,0 +1,507 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { TestDatabases } from '@backstage/backend-test-utils'; +import { DateTime, Duration } from 'luxon'; +import waitForExpect from 'wait-for-expect'; +import { migrateBackendTasks } from '../database/migrateBackendTasks'; +import { DB_TASKS_TABLE, DbTasksRow } from '../database/tables'; +import { TaskWorker } from './TaskWorker'; +import { createTestScopedSignal } from './__testUtils__/createTestScopedSignal'; +import { TaskSettingsV2 } from './types'; + +jest.setTimeout(60_000); + +describe('TaskWorker', () => { + const logger = getVoidLogger(); + const databases = TestDatabases.create(); + const testScopedSignal = createTestScopedSignal(); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it.each(databases.eachSupportedId())( + 'goes through the expected states, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn( + async () => new Promise(resolve => setTimeout(resolve, 50)), + ); + const settings: TaskSettingsV2 = { + version: 2, + cadence: '*/2 * * * * *', + initialDelayDuration: Duration.fromObject({ seconds: 1 }).toISO()!, + timeoutAfterDuration: Duration.fromObject({ minutes: 1 }).toISO()!, + }; + + const worker = new TaskWorker('task1', fn, knex, logger); + await worker.persistTask(settings); + + let row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }), + ); + expect(JSON.parse(row.settings_json)).toEqual({ + version: 2, + cadence: '*/2 * * * * *', + initialDelayDuration: 'PT1S', + timeoutAfterDuration: 'PT1M', + }); + + await expect(worker.findReadyTask()).resolves.toEqual({ + result: 'not-ready-yet', + }); + + await waitForExpect(async () => { + await expect(worker.findReadyTask()).resolves.toEqual({ + result: 'ready', + settings, + }); + }); + + row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }), + ); + + await expect(worker.tryClaimTask('ticket', settings)).resolves.toBe(true); + + row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: 'ticket', + current_run_started_at: expect.anything(), + current_run_expires_at: expect.anything(), + }), + ); + + await expect(worker.tryReleaseTask('ticket', settings)).resolves.toBe( + true, + ); + + row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }), + ); + }, + ); + + it.each(databases.eachSupportedId())( + 'logs error when the task throws, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + jest.spyOn(logger, 'error'); + const fn = jest.fn().mockRejectedValue(new Error('failed')); + const settings: TaskSettingsV2 = { + version: 2, + initialDelayDuration: undefined, + cadence: '* * * * * *', + timeoutAfterDuration: Duration.fromMillis(60000).toISO()!, + }; + const checkFrequency = Duration.fromObject({ milliseconds: 100 }); + const worker = new TaskWorker('task1', fn, knex, logger, checkFrequency); + worker.start(settings, { signal: testScopedSignal() }); + + await waitForExpect(() => { + expect(logger.error).toHaveBeenCalled(); + }); + }, + ); + + it.each(databases.eachSupportedId())( + 'runs tasks more than once even when the task throws, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn().mockRejectedValue(new Error('failed')); + const settings: TaskSettingsV2 = { + version: 2, + initialDelayDuration: undefined, + cadence: '* * * * * *', + timeoutAfterDuration: Duration.fromMillis(60000).toISO()!, + }; + const checkFrequency = Duration.fromObject({ milliseconds: 100 }); + const worker = new TaskWorker('task1', fn, knex, logger, checkFrequency); + worker.start(settings, { signal: testScopedSignal() }); + + await waitForExpect(() => { + expect(fn).toHaveBeenCalledTimes(3); + }); + }, + ); + + it.each(databases.eachSupportedId())( + 'does not clobber ticket lock when stolen, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn( + async () => new Promise(resolve => setTimeout(resolve, 50)), + ); + const settings: TaskSettingsV2 = { + version: 2, + initialDelayDuration: undefined, + cadence: '* * * * * *', + timeoutAfterDuration: Duration.fromMillis(60000).toISO()!, + }; + + const worker = new TaskWorker('task1', fn, knex, logger); + await worker.persistTask(settings); + + await waitForExpect(async () => { + await expect(worker.findReadyTask()).resolves.toEqual({ + result: 'ready', + settings, + }); + }); + + await expect(worker.tryClaimTask('ticket', settings)).resolves.toBe(true); + + let row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: 'ticket', + current_run_started_at: expect.anything(), + current_run_expires_at: expect.anything(), + }), + ); + + await knex(DB_TASKS_TABLE) + .where('id', '=', 'task1') + .update({ current_run_ticket: 'stolen' }); + + await expect(worker.tryReleaseTask('ticket', settings)).resolves.toBe( + false, + ); + + row = (await knex(DB_TASKS_TABLE))[0]; + expect(row).toEqual( + expect.objectContaining({ + id: 'task1', + current_run_ticket: 'stolen', + current_run_started_at: expect.anything(), + current_run_expires_at: expect.anything(), + }), + ); + }, + ); + + it.each(databases.eachSupportedId())( + 'gracefully handles a disappeared task row, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn(async () => {}); + const settings: TaskSettingsV2 = { + version: 2, + initialDelayDuration: undefined, + cadence: '* * * * * *', + timeoutAfterDuration: Duration.fromMillis(60000).toISO()!, + }; + + const worker1 = new TaskWorker('task1', fn, knex, logger); + await worker1.persistTask(settings); + await knex(DB_TASKS_TABLE).where('id', '=', 'task1').delete(); + await expect(worker1.findReadyTask()).resolves.toEqual({ + result: 'abort', + }); + + const worker2 = new TaskWorker('task2', fn, knex, logger); + await worker2.persistTask(settings); + + await waitForExpect(async () => { + await expect(worker2.findReadyTask()).resolves.toEqual({ + result: 'ready', + settings, + }); + }); + + await knex(DB_TASKS_TABLE).where('id', '=', 'task2').delete(); + await expect(worker2.tryClaimTask('ticket', settings)).resolves.toBe( + false, + ); + + const worker3 = new TaskWorker('task3', fn, knex, logger); + await worker3.persistTask(settings); + + await waitForExpect(async () => { + await expect(worker3.findReadyTask()).resolves.toEqual({ + result: 'ready', + settings, + }); + }); + + await expect(worker3.tryClaimTask('ticket', settings)).resolves.toBe( + true, + ); + await knex(DB_TASKS_TABLE).where('id', '=', 'task3').delete(); + await expect(worker3.tryReleaseTask('ticket', settings)).resolves.toBe( + false, + ); + }, + ); + + it.each(databases.eachSupportedId())( + 'respects initialDelayDuration per worker, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const abortFirst = new AbortController(); + const settings: TaskSettingsV2 = { + version: 2, + initialDelayDuration: 'PT0.3S', + cadence: 'PT0.1S', + timeoutAfterDuration: 'PT10S', + }; + + // Start a single worker and make sure it waits and then goes to work + const fn1 = jest.fn(async () => {}); + const worker1 = new TaskWorker( + 'task1', + fn1, + knex, + logger, + Duration.fromMillis(10), + ); + await worker1.start(settings, { signal: abortFirst.signal }); + + expect(fn1).toHaveBeenCalledTimes(0); + await new Promise(resolve => setTimeout(resolve, 250)); + expect(fn1).toHaveBeenCalledTimes(0); + await new Promise(resolve => setTimeout(resolve, 100)); + expect(fn1.mock.calls.length).toBeGreaterThan(0); + + // Start a second worker and make sure it waits but the first worker still works along + const fn2 = jest.fn(); + const promise2 = new Promise(resolve => fn2.mockImplementation(resolve)); + const worker2 = new TaskWorker( + 'task1', + fn2, + knex, + logger, + Duration.fromMillis(10), + ); + await worker2.start(settings, { signal: testScopedSignal() }); + + // We eventually abort the first worker just to make sure that the second + // one for sure will get a go at running the task + setTimeout(() => abortFirst.abort(), 1000); + + const before = fn1.mock.calls.length; + await promise2; + expect(fn1.mock.calls.length).toBeGreaterThan(before); + + await knex.destroy(); + }, + ); + + it.each(databases.eachSupportedId())( + 'next_run_start_at is always the min between schedule changes from cron frequency, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn( + async () => new Promise(resolve => setTimeout(resolve, 50)), + ); + const settings: TaskSettingsV2 = { + version: 2, + cadence: '*/15 * * * *', + initialDelayDuration: 'PT2M', + timeoutAfterDuration: 'PT1M', + }; + + const worker = new TaskWorker('task99', fn, knex, logger); + await worker.persistTask(settings); + const row1 = (await knex(DB_TASKS_TABLE))[0]; + + const settings2 = { + ...settings, + cadence: '*/2 * * * *', + initialDelayDuration: 'PT1M', + }; + await worker.persistTask(settings2); + const row2 = (await knex(DB_TASKS_TABLE))[0]; + + expect(row2.next_run_start_at).not.toStrictEqual(row1.next_run_start_at); + + const settings3 = { ...settings }; + await worker.persistTask(settings3); + const row3 = (await knex(DB_TASKS_TABLE))[0]; + + // The new timestamp can basically be 0 or a minute depending on how the + // initialDelayDuration falls right on a cron boundary. This kinda + // contrived check removes a test flakiness based on wall clock time. + expect( + Math.abs( + +new Date(row3.next_run_start_at) - +new Date(row2.next_run_start_at), + ), + ).toBeLessThanOrEqual(60_000); + + await knex.destroy(); + }, + ); + + it.each(databases.eachSupportedId())( + 'next_run_start_at is always the min between schedule changes when using human duration frequency, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn( + async () => new Promise(resolve => setTimeout(resolve, 50)), + ); + + const initialSettings: TaskSettingsV2 = { + version: 2, + cadence: 'PT120M', + timeoutAfterDuration: 'PT1M', + }; + + const worker = new TaskWorker('task99', fn, knex, logger); + await worker.persistTask(initialSettings); + // replicate task running, sets next_run_start_at based on cadence + await worker.tryClaimTask('ticket', initialSettings); + await worker.tryReleaseTask('ticket', initialSettings); + + // grab initial row for comparisons later + const rowAfterClaimAndRelease = ( + await knex(DB_TASKS_TABLE) + )[0]; + + const settings: TaskSettingsV2 = { + ...initialSettings, + cadence: 'PT60M', + }; + await worker.persistTask(settings); + const row1 = (await knex(DB_TASKS_TABLE))[0]; + + const rowAfterClaimAndReleaseNextStartAt = DateTime.fromJSDate( + new Date(rowAfterClaimAndRelease.next_run_start_at), + ); + const row1NextStartAt = DateTime.fromJSDate( + new Date(row1.next_run_start_at), + ); + const now = DateTime.now(); + expect( + rowAfterClaimAndReleaseNextStartAt.diff(row1NextStartAt).as('minutes'), + ).toBeCloseTo(60, 1); // ensure that next start at is sooner than initial by one hour + expect(row1NextStartAt.diff(now).as('minutes')).toBeCloseTo(60, 1); // ensure that next start at is later than now by one hour + expect( + rowAfterClaimAndReleaseNextStartAt.diff(now).as('minutes'), + ).toBeCloseTo(120, 1); + + const settings2 = { + ...settings, + }; + await worker.persistTask(settings2); + const row2 = (await knex(DB_TASKS_TABLE))[0]; + + expect(row2.next_run_start_at).toStrictEqual(row1.next_run_start_at); + + await knex.destroy(); + }, + ); + + it.each(databases.eachSupportedId())( + 'next_run_start_at is always the min between schedule changes when using human duration frequency with initial start delay, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn( + async () => new Promise(resolve => setTimeout(resolve, 50)), + ); + + const initialSettings: TaskSettingsV2 = { + version: 2, + cadence: 'PT120M', + initialDelayDuration: 'PT2M', + timeoutAfterDuration: 'PT1M', + }; + + const worker = new TaskWorker('task99', fn, knex, logger); + await worker.persistTask(initialSettings); + // replicate task running, sets next_run_start_at based on cadence + await worker.tryClaimTask('ticket', initialSettings); + await worker.tryReleaseTask('ticket', initialSettings); + + // grab initial row for comparisons later + const rowAfterClaimAndRelease = ( + await knex(DB_TASKS_TABLE) + )[0]; + + const settings: TaskSettingsV2 = { + ...initialSettings, + cadence: 'PT60M', + }; + await worker.persistTask(settings); + const row1 = (await knex(DB_TASKS_TABLE))[0]; + + const rowAfterClaimAndReleaseNextStartAt = DateTime.fromJSDate( + new Date(rowAfterClaimAndRelease.next_run_start_at), + ); + const row1NextStartAt = DateTime.fromJSDate( + new Date(row1.next_run_start_at), + ); + const now = DateTime.now(); + expect( + rowAfterClaimAndReleaseNextStartAt.diff(row1NextStartAt).as('minutes'), + ).toBeCloseTo(62, 1); // ensure that next start at is sooner than initial by one hour, plus the 2 minute delay (set my tryReleaseTask) + expect(row1NextStartAt.diff(now).as('minutes')).toBeCloseTo(60, 1); // ensure that next start at is later than now by one hour (2 minute delay doesn't take effect here) + expect( + rowAfterClaimAndReleaseNextStartAt.diff(now).as('minutes'), + ).toBeCloseTo(122, 1); // includes 2 minute start delay (which is persisted from tryReleaseTask) + + const settings2 = { + ...settings, + }; + await worker.persistTask(settings2); + const row2 = (await knex(DB_TASKS_TABLE))[0]; + + expect(row2.next_run_start_at).toStrictEqual(row1.next_run_start_at); + + await knex.destroy(); + }, + ); +}); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts new file mode 100644 index 0000000000..a1c4ec44fd --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts @@ -0,0 +1,373 @@ +/* + * 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 { LoggerService } from '@backstage/backend-plugin-api'; +import { ConflictError, NotFoundError } from '@backstage/errors'; +import { CronTime } from 'cron'; +import { Knex } from 'knex'; +import { DateTime, Duration } from 'luxon'; +import { v4 as uuid } from 'uuid'; +import { DB_TASKS_TABLE, DbTasksRow } from '../database/tables'; +import { TaskSettingsV2, taskSettingsV2Schema } from './types'; +import { delegateAbortController, nowPlus, sleep } from './util'; +import { SchedulerServiceTaskFunction } from '@backstage/backend-plugin-api'; + +const DEFAULT_WORK_CHECK_FREQUENCY = Duration.fromObject({ seconds: 5 }); + +/** + * Implements tasks that run across worker hosts, with collaborative locking. + * + * @private + */ +export class TaskWorker { + constructor( + private readonly taskId: string, + private readonly fn: SchedulerServiceTaskFunction, + private readonly knex: Knex, + private readonly logger: LoggerService, + private readonly workCheckFrequency: Duration = DEFAULT_WORK_CHECK_FREQUENCY, + ) {} + + async start(settings: TaskSettingsV2, options?: { signal?: AbortSignal }) { + try { + await this.persistTask(settings); + } catch (e) { + throw new Error(`Failed to persist task, ${e}`); + } + + this.logger.info( + `Task worker starting: ${this.taskId}, ${JSON.stringify(settings)}`, + ); + + let workCheckFrequency = this.workCheckFrequency; + const isCron = !settings?.cadence.startsWith('P'); + if (!isCron) { + const cadence = Duration.fromISO(settings.cadence); + if (cadence < workCheckFrequency) { + workCheckFrequency = cadence; + } + } + + let attemptNum = 1; + (async () => { + for (;;) { + try { + if (settings.initialDelayDuration) { + await sleep( + Duration.fromISO(settings.initialDelayDuration), + options?.signal, + ); + } + + while (!options?.signal?.aborted) { + const runResult = await this.runOnce(options?.signal); + + if (runResult.result === 'abort') { + break; + } + + await sleep(workCheckFrequency, options?.signal); + } + + this.logger.info(`Task worker finished: ${this.taskId}`); + attemptNum = 0; + break; + } catch (e) { + attemptNum += 1; + this.logger.warn( + `Task worker failed unexpectedly, attempt number ${attemptNum}, ${e}`, + ); + await sleep(Duration.fromObject({ seconds: 1 })); + } + } + })(); + } + + static async trigger(knex: Knex, taskId: string): Promise { + // check if task exists + const rows = await knex(DB_TASKS_TABLE) + .select(knex.raw(1)) + .where('id', '=', taskId); + if (rows.length !== 1) { + throw new NotFoundError(`Task ${taskId} does not exist`); + } + + const updatedRows = await knex(DB_TASKS_TABLE) + .where('id', '=', taskId) + .whereNull('current_run_ticket') + .update({ + next_run_start_at: knex.fn.now(), + }); + if (updatedRows < 1) { + throw new ConflictError(`Task ${taskId} is currently running`); + } + } + + /** + * Makes a single attempt at running the task to completion, if ready. + * + * @returns The outcome of the attempt + */ + private async runOnce( + signal?: AbortSignal, + ): Promise< + | { result: 'not-ready-yet' } + | { result: 'abort' } + | { result: 'failed' } + | { result: 'completed' } + > { + const findResult = await this.findReadyTask(); + if ( + findResult.result === 'not-ready-yet' || + findResult.result === 'abort' + ) { + return findResult; + } + + const taskSettings = findResult.settings; + const ticket = uuid(); + + const claimed = await this.tryClaimTask(ticket, taskSettings); + if (!claimed) { + return { result: 'not-ready-yet' }; + } + + // Abort the task execution either if the worker is stopped, or if the + // task timeout is hit + const taskAbortController = delegateAbortController(signal); + const timeoutHandle = setTimeout(() => { + taskAbortController.abort(); + }, Duration.fromISO(taskSettings.timeoutAfterDuration).as('milliseconds')); + + try { + await this.fn(taskAbortController.signal); + taskAbortController.abort(); // releases resources + } catch (e) { + this.logger.error(e); + await this.tryReleaseTask(ticket, taskSettings); + return { result: 'failed' }; + } finally { + clearTimeout(timeoutHandle); + } + + await this.tryReleaseTask(ticket, taskSettings); + return { result: 'completed' }; + } + + /** + * Perform the initial store of the task info + */ + async persistTask(settings: TaskSettingsV2) { + // Perform an initial parse to ensure that we will definitely be able to + // read it back again. + taskSettingsV2Schema.parse(settings); + + const isCron = !settings?.cadence.startsWith('P'); + + let startAt: Knex.Raw | undefined; + let nextStartAt: Knex.Raw | undefined; + if (settings.initialDelayDuration) { + startAt = nowPlus( + Duration.fromISO(settings.initialDelayDuration), + this.knex, + ); + } + + if (isCron) { + const time = new CronTime(settings.cadence) + .sendAt() + .minus({ seconds: 1 }) // immediately, if "* * * * * *" + .toUTC(); + + nextStartAt = this.nextRunAtRaw(time); + startAt ||= nextStartAt; + } else { + startAt ||= this.knex.fn.now(); + nextStartAt = nowPlus(Duration.fromISO(settings.cadence), this.knex); + } + + this.logger.debug(`task: ${this.taskId} configured to run at: ${startAt}`); + + // It's OK if the task already exists; if it does, just replace its + // settings with the new value and start the loop as usual. + const settingsJson = JSON.stringify(settings); + await this.knex(DB_TASKS_TABLE) + .insert({ + id: this.taskId, + settings_json: settingsJson, + next_run_start_at: startAt, + }) + .onConflict('id') + .merge( + this.knex.client.config.client.includes('mysql') + ? { + settings_json: settingsJson, + next_run_start_at: this.knex.raw( + `CASE WHEN ?? < ?? THEN ?? ELSE ?? END`, + [ + nextStartAt, + 'next_run_start_at', + nextStartAt, + 'next_run_start_at', + ], + ), + } + : { + settings_json: this.knex.ref('excluded.settings_json'), + next_run_start_at: this.knex.raw( + `CASE WHEN ?? < ?? THEN ?? ELSE ?? END`, + [ + nextStartAt, + `${DB_TASKS_TABLE}.next_run_start_at`, + nextStartAt, + `${DB_TASKS_TABLE}.next_run_start_at`, + ], + ), + }, + ); + } + + /** + * Check if the task is ready to run + */ + async findReadyTask(): Promise< + | { result: 'not-ready-yet' } + | { result: 'abort' } + | { result: 'ready'; settings: TaskSettingsV2 } + > { + const [row] = await this.knex(DB_TASKS_TABLE) + .where('id', '=', this.taskId) + .select({ + settingsJson: 'settings_json', + ready: this.knex.raw( + `CASE + WHEN next_run_start_at <= ? AND current_run_ticket IS NULL THEN TRUE + ELSE FALSE + END`, + [this.knex.fn.now()], + ), + }); + + if (!row) { + this.logger.info( + 'No longer able to find task; aborting and assuming that it has been unregistered or expired', + ); + return { result: 'abort' }; + } else if (!row.ready) { + return { result: 'not-ready-yet' }; + } + + try { + const obj = JSON.parse(row.settingsJson); + const settings = taskSettingsV2Schema.parse(obj); + return { result: 'ready', settings }; + } catch (e) { + this.logger.info( + `Task "${this.taskId}" is no longer able to parse task settings; aborting and assuming that a ` + + `newer version of the task has been issued and being handled by other workers, ${e}`, + ); + return { result: 'abort' }; + } + } + + /** + * Attempts to claim a task that's ready for execution, on this worker's + * behalf. We should not attempt to perform the work unless the claim really + * goes through. + * + * @param ticket - A globally unique string that changes for each invocation + * @param settings - The settings of the task to claim + * @returns True if it was successfully claimed + */ + async tryClaimTask( + ticket: string, + settings: TaskSettingsV2, + ): Promise { + const startedAt = this.knex.fn.now(); + const expiresAt = settings.timeoutAfterDuration + ? nowPlus(Duration.fromISO(settings.timeoutAfterDuration), this.knex) + : this.knex.raw('null'); + + const rows = await this.knex(DB_TASKS_TABLE) + .where('id', '=', this.taskId) + .whereNull('current_run_ticket') + .update({ + current_run_ticket: ticket, + current_run_started_at: startedAt, + current_run_expires_at: expiresAt, + }); + + return rows === 1; + } + + async tryReleaseTask( + ticket: string, + settings: TaskSettingsV2, + ): Promise { + const isCron = !settings?.cadence.startsWith('P'); + + let nextRun: Knex.Raw; + if (isCron) { + const time = new CronTime(settings.cadence).sendAt().toUTC(); + this.logger.debug(`task: ${this.taskId} will next occur around ${time}`); + + nextRun = this.nextRunAtRaw(time); + } else { + const dt = Duration.fromISO(settings.cadence).as('seconds'); + this.logger.debug( + `task: ${this.taskId} will next occur around ${DateTime.now().plus({ + seconds: dt, + })}`, + ); + + if (this.knex.client.config.client.includes('sqlite3')) { + nextRun = this.knex.raw( + `max(datetime(next_run_start_at, ?), datetime('now'))`, + [`+${dt} seconds`], + ); + } else if (this.knex.client.config.client.includes('mysql')) { + nextRun = this.knex.raw( + `greatest(next_run_start_at + interval ${dt} second, now())`, + ); + } else { + nextRun = this.knex.raw( + `greatest(next_run_start_at + interval '${dt} seconds', now())`, + ); + } + } + + const rows = await this.knex(DB_TASKS_TABLE) + .where('id', '=', this.taskId) + .where('current_run_ticket', '=', ticket) + .update({ + next_run_start_at: nextRun, + current_run_ticket: this.knex.raw('null'), + current_run_started_at: this.knex.raw('null'), + current_run_expires_at: this.knex.raw('null'), + }); + + return rows === 1; + } + + private nextRunAtRaw(time: DateTime): Knex.Raw { + if (this.knex.client.config.client.includes('sqlite3')) { + return this.knex.raw('datetime(?)', [time.toISO()]); + } else if (this.knex.client.config.client.includes('mysql')) { + return this.knex.raw(`?`, [time.toSQL({ includeOffset: false })]); + } + return this.knex.raw(`?`, [time.toISO()]); + } +} diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/__testUtils__/createTestScopedSignal.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/__testUtils__/createTestScopedSignal.ts new file mode 100644 index 0000000000..6a497ea266 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/__testUtils__/createTestScopedSignal.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function createTestScopedSignal(): () => AbortSignal { + let testAbortController = new AbortController(); + + beforeEach(() => { + testAbortController = new AbortController(); + }); + afterEach(() => { + testAbortController.abort(); + }); + + return () => testAbortController.signal; +} diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts new file mode 100644 index 0000000000..61e6c1d9a8 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts @@ -0,0 +1,90 @@ +/* + * 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 { CronTime } from 'cron'; +import { Duration } from 'luxon'; +import { z } from 'zod'; + +function isValidOptionalDurationString(d: string | undefined): boolean { + try { + return !d || Duration.fromISO(d).isValid; + } catch { + return false; + } +} + +function isValidCronFormat(c: string | undefined): boolean { + try { + if (!c) { + return false; + } + // parse cron format to ensure it's a valid format. + // eslint-disable-next-line no-new + new CronTime(c); + return true; + } catch { + return false; + } +} + +export const taskSettingsV1Schema = z.object({ + version: z.literal(1), + initialDelayDuration: z + .string() + .optional() + .refine(isValidOptionalDurationString, { + message: 'Invalid duration, expecting ISO Period', + }), + recurringAtMostEveryDuration: z + .string() + .refine(isValidOptionalDurationString, { + message: 'Invalid duration, expecting ISO Period', + }), + timeoutAfterDuration: z.string().refine(isValidOptionalDurationString, { + message: 'Invalid duration, expecting ISO Period', + }), +}); + +/** + * The properties that control a scheduled task (version 1). + */ +export type TaskSettingsV1 = z.infer; + +export const taskSettingsV2Schema = z.object({ + version: z.literal(2), + cadence: z + .string() + .refine(isValidCronFormat, { message: 'Invalid cron' }) + .or( + z.string().refine(isValidOptionalDurationString, { + message: 'Invalid duration, expecting ISO Period', + }), + ), + timeoutAfterDuration: z.string().refine(isValidOptionalDurationString, { + message: 'Invalid duration, expecting ISO Period', + }), + initialDelayDuration: z + .string() + .optional() + .refine(isValidOptionalDurationString, { + message: 'Invalid duration, expecting ISO Period', + }), +}); + +/** + * The properties that control a scheduled task (version 2). + */ +export type TaskSettingsV2 = z.infer; diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/util.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/util.test.ts new file mode 100644 index 0000000000..e2536abb88 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/util.test.ts @@ -0,0 +1,113 @@ +/* + * 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 knexFactory, { Knex } from 'knex'; +import { Duration } from 'luxon'; +import { delegateAbortController, nowPlus, sleep, validateId } from './util'; + +class KnexBuilder { + public build(client: string): Knex { + return knexFactory({ client, useNullAsDefault: true }); + } +} + +describe('util', () => { + describe('validateId', () => { + it.each(['a', 'a_b', 'ab123c_2', 'a!', 'A', 'a-b', 'a.b', '_a', 'a_'])( + 'accepts valid inputs, %p', + async input => { + expect(validateId(input)).toBeUndefined(); + }, + ); + + it.each(['', null, Symbol('a')])( + 'rejects invalid inputs, %p', + async input => { + expect(() => validateId(input as any)).toThrow(); + }, + ); + }); + + describe('sleep', () => { + it('finishes the wait as expected with no signal', async () => { + const ac = new AbortController(); + const start = Date.now(); + await sleep(Duration.fromObject({ seconds: 1 }), ac.signal); + expect(Date.now() - start).toBeGreaterThan(800); + }, 5_000); + + it('aborts properly on the signal', async () => { + const ac = new AbortController(); + const promise = sleep(Duration.fromObject({ seconds: 10 }), ac.signal); + ac.abort(); + await promise; + expect(true).toBe(true); + }, 1_000); + }); + + describe('delegateAbortController', () => { + it('inherits parent abort state', () => { + const parent = new AbortController(); + const child = delegateAbortController(parent.signal); + expect(parent.signal.aborted).toBe(false); + expect(child.signal.aborted).toBe(false); + parent.abort(); + expect(parent.signal.aborted).toBe(true); + expect(child.signal.aborted).toBe(true); + }); + + it('does not inherit from child to parent', () => { + const parent = new AbortController(); + const child = delegateAbortController(parent.signal); + expect(parent.signal.aborted).toBe(false); + expect(child.signal.aborted).toBe(false); + child.abort(); + expect(parent.signal.aborted).toBe(false); + expect(child.signal.aborted).toBe(true); + }); + }); + + describe('nowPlus', () => { + describe('without duration', () => { + const databases = [ + { client: 'sqlite3', expected: 'CURRENT_TIMESTAMP' }, + { client: 'mysql2', expected: 'CURRENT_TIMESTAMP' }, + { client: 'pg', expected: 'CURRENT_TIMESTAMP' }, + ]; + + it.each(databases)('for client $client', ({ client, expected }) => { + const knex = new KnexBuilder().build(client); + const result = nowPlus(undefined, knex); + + expect(result.toString()).toBe(expected); + }); + }); + describe('With duration', () => { + const databases = [ + { client: 'sqlite3', expected: "datetime('now', '20 seconds')" }, + { client: 'mysql2', expected: 'now() + interval 20 second' }, + { client: 'pg', expected: "now() + interval '20 seconds'" }, + ]; + it.each(databases)('for client $client', ({ client, expected }) => { + const duration = Duration.fromObject({ seconds: 20 }); + const knex = new KnexBuilder().build(client); + const result = nowPlus(duration, knex); + + expect(result.toString()).toBe(expected); + }); + }); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/util.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/util.ts new file mode 100644 index 0000000000..70d67a9fbe --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/util.ts @@ -0,0 +1,111 @@ +/* + * 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 { InputError } from '@backstage/errors'; +import { Knex } from 'knex'; +import { DateTime, Duration } from 'luxon'; + +// Keep the IDs compatible with e.g. Prometheus labels +export function validateId(id: string) { + if (typeof id !== 'string' || !id.trim()) { + throw new InputError(`${id} is not a valid ID, expected non-empty string`); + } +} + +export function dbTime(t: Date | string): DateTime { + if (typeof t === 'string') { + return DateTime.fromSQL(t); + } + return DateTime.fromJSDate(t); +} + +export function nowPlus(duration: Duration | undefined, knex: Knex) { + const seconds = duration?.as('seconds') ?? 0; + if (!seconds) { + return knex.fn.now(); + } + + if (knex.client.config.client.includes('sqlite3')) { + return knex.raw(`datetime('now', ?)`, [`${seconds} seconds`]); + } + + if (knex.client.config.client.includes('mysql')) { + return knex.raw(`now() + interval ${seconds} second`); + } + + return knex.raw(`now() + interval '${seconds} seconds'`); +} + +/** + * Sleep for the given duration, but return sooner if the abort signal + * triggers. + * + * @param duration - The amount of time to sleep, at most + * @param abortSignal - An optional abort signal that short circuits the wait + */ +export async function sleep( + duration: Duration, + abortSignal?: AbortSignal, +): Promise { + if (abortSignal?.aborted) { + return; + } + + await new Promise(resolve => { + let timeoutHandle: NodeJS.Timeout | undefined = undefined; + + const done = () => { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + abortSignal?.removeEventListener('abort', done); + resolve(); + }; + + timeoutHandle = setTimeout(done, duration.as('milliseconds')); + abortSignal?.addEventListener('abort', done); + }); +} + +/** + * Creates a new AbortController that, in addition to working as a regular + * standalone controller, also gets aborted if the given parent signal + * reaches aborted state. + * + * @param parent - The "parent" signal that can trigger the delegate + */ +export function delegateAbortController(parent?: AbortSignal): AbortController { + const delegate = new AbortController(); + + if (parent) { + if (parent.aborted) { + delegate.abort(); + } else { + const onParentAborted = () => { + delegate.abort(); + }; + + const onChildAborted = () => { + parent.removeEventListener('abort', onParentAborted); + }; + + parent.addEventListener('abort', onParentAborted, { once: true }); + delegate.signal.addEventListener('abort', onChildAborted, { once: true }); + } + } + + return delegate; +} diff --git a/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.test.ts new file mode 100644 index 0000000000..7b889b3a0e --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { coreServices } from '@backstage/backend-plugin-api'; +import { ServiceFactoryTester } from '@backstage/backend-test-utils'; +import { schedulerServiceFactory } from './schedulerServiceFactory'; + +describe('schedulerFactory', () => { + it('creates sidecar database features', async () => { + const tester = ServiceFactoryTester.from(schedulerServiceFactory()); + + const scheduler = await tester.get(); + await scheduler.scheduleTask({ + id: 'task1', + timeout: { seconds: 1 }, + frequency: { seconds: 1 }, + fn: async () => {}, + }); + + const database = await tester.getService(coreServices.database); + + const client = await database.getClient(); + await expect( + client.from('backstage_backend_tasks__tasks').count(), + ).resolves.toEqual([{ 'count(*)': 1 }]); + await expect( + client.from('backstage_backend_tasks__knex_migrations').count(), + ).resolves.toEqual([{ 'count(*)': expect.any(Number) }]); + await expect( + client.from('backstage_backend_tasks__knex_migrations_lock').count(), + ).resolves.toEqual([{ 'count(*)': expect.any(Number) }]); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.ts b/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.ts new file mode 100644 index 0000000000..55c0d58e7b --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { TaskScheduler } from './lib/TaskScheduler'; + +/** + * The default service factory for {@link @backstage/backend-plugin-api#coreServices.scheduler}. + * + * @public + */ +export const schedulerServiceFactory = createServiceFactory({ + service: coreServices.scheduler, + deps: { + plugin: coreServices.pluginMetadata, + database: coreServices.database, + logger: coreServices.logger, + }, + async factory({ plugin, database, logger }) { + return TaskScheduler.forPlugin({ + pluginId: plugin.getId(), + databaseManager: database, + logger, + }); + }, +}); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/index.ts b/packages/backend-defaults/src/entrypoints/urlReader/index.ts new file mode 100644 index 0000000000..6a4b9f65be --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/urlReader/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { urlReaderServiceFactory } from './urlReaderServiceFactory'; diff --git a/plugins/user-settings-backend/src/run.ts b/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts similarity index 53% rename from plugins/user-settings-backend/src/run.ts rename to packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts index 178de0f786..7f404a24b1 100644 --- a/plugins/user-settings-backend/src/run.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts @@ -14,21 +14,23 @@ * limitations under the License. */ -import { getRootLogger } from '@backstage/backend-common'; -import yn from 'yn'; +import { UrlReaders } from '@backstage/backend-common'; +import { + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; -import { startStandaloneServer } from './service/standaloneServer'; - -const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; -const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); -const logger = getRootLogger(); - -startStandaloneServer({ port, enableCors, logger }).catch(err => { - logger.error(err); - process.exit(1); -}); - -process.on('SIGINT', () => { - logger.info('CTRL+C pressed; exiting.'); - process.exit(0); +/** @public */ +export const urlReaderServiceFactory = createServiceFactory({ + service: coreServices.urlReader, + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, + }, + async factory({ config, logger }) { + return UrlReaders.default({ + config, + logger, + }); + }, }); diff --git a/plugins/auth-backend/src/run.ts b/packages/backend-defaults/src/setupTests.ts similarity index 62% rename from plugins/auth-backend/src/run.ts rename to packages/backend-defaults/src/setupTests.ts index 7732bbd41a..76619a2542 100644 --- a/plugins/auth-backend/src/run.ts +++ b/packages/backend-defaults/src/setupTests.ts @@ -14,19 +14,12 @@ * limitations under the License. */ -import { getRootLogger } from '@backstage/backend-common'; -import { startStandaloneServer } from './service/standaloneServer'; +import { TestDatabases } from '@backstage/backend-test-utils'; +import { Settings } from 'luxon'; -const logger = getRootLogger(); +// TS still thinks that methods can return null / placeholders, but we still want to throw as soon as possible when things go wrong +Settings.throwOnInvalid = true; -startStandaloneServer({ logger }).catch(err => { - logger.error(err); - process.exit(1); +TestDatabases.setDefaults({ + ids: ['MYSQL_8', 'POSTGRES_16', 'POSTGRES_12', 'SQLITE_3'], }); - -process.on('SIGINT', () => { - logger.info('CTRL+C pressed; exiting.'); - process.exit(0); -}); - -module.hot?.accept(); diff --git a/packages/backend-dynamic-feature-service/CHANGELOG.md b/packages/backend-dynamic-feature-service/CHANGELOG.md index ad620ee060..007951a732 100644 --- a/packages/backend-dynamic-feature-service/CHANGELOG.md +++ b/packages/backend-dynamic-feature-service/CHANGELOG.md @@ -1,5 +1,76 @@ # @backstage/backend-dynamic-feature-service +## 0.2.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-node@0.2.6-next.1 + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-permission-node@0.7.30-next.2 + - @backstage/backend-app-api@0.7.6-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-app-node@0.1.19-next.1 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-catalog-backend@1.23.0-next.2 + - @backstage/plugin-events-backend@0.3.6-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/plugin-scaffolder-node@0.4.5-next.2 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/config-loader@1.8.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + +## 0.2.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.1 + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-permission-node@0.7.30-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-backend@1.23.0-next.1 + - @backstage/cli-node@0.2.6-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/plugin-auth-node@0.4.14-next.1 + - @backstage/plugin-events-backend@0.3.6-next.1 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + +## 0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.0 + - @backstage/cli-node@0.2.6-next.0 + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-catalog-backend@1.23.0-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-events-backend@0.3.6-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/plugin-scaffolder-node@0.4.5-next.0 + - @backstage/plugin-app-node@0.1.19-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + ## 0.2.10 ### Patch Changes diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index 4c7a7bbaf5..54a76df87a 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-dynamic-feature-service", "description": "Backstage dynamic feature service", - "version": "0.2.10", + "version": "0.2.11-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-legacy/CHANGELOG.md b/packages/backend-legacy/CHANGELOG.md index 9a5a720491..33d3a78ffd 100644 --- a/packages/backend-legacy/CHANGELOG.md +++ b/packages/backend-legacy/CHANGELOG.md @@ -1,5 +1,126 @@ # example-backend-legacy +## 0.2.99-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-backend@1.10.6-next.2 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.1-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-permission-node@0.7.30-next.2 + - example-app@0.2.98-next.2 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.2 + - @backstage/plugin-scaffolder-backend@1.22.8-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-app-backend@0.3.68-next.2 + - @backstage/plugin-auth-backend@0.22.6-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-catalog-backend@1.23.0-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.6-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/plugin-devtools-backend@0.3.5-next.2 + - @backstage/plugin-events-backend@0.3.6-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/plugin-kubernetes-backend@0.18.0-next.2 + - @backstage/plugin-permission-backend@0.5.43-next.2 + - @backstage/plugin-proxy-backend@0.5.0-next.2 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.20-next.2 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.36-next.2 + - @backstage/plugin-search-backend@1.5.10-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.25-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.4.2-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.25-next.2 + - @backstage/plugin-search-backend-module-pg@0.5.28-next.2 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/plugin-signals-backend@0.1.5-next.2 + - @backstage/plugin-signals-node@0.1.5-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-permission-common@0.7.13 + +## 0.2.99-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/plugin-permission-node@0.7.30-next.1 + - @backstage/plugin-search-backend@1.5.10-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.1-next.1 + - @backstage/plugin-kubernetes-backend@0.18.0-next.1 + - @backstage/plugin-catalog-backend@1.23.0-next.1 + - @backstage/plugin-scaffolder-backend@1.22.8-next.1 + - @backstage/plugin-app-backend@0.3.68-next.1 + - @backstage/plugin-auth-backend@0.22.6-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/plugin-devtools-backend@0.3.5-next.1 + - @backstage/plugin-events-backend@0.3.6-next.1 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/plugin-permission-backend@0.5.43-next.1 + - @backstage/plugin-proxy-backend@0.5.0-next.1 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.20-next.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.36-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.25-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.4.2-next.1 + - @backstage/plugin-search-backend-module-explore@0.1.25-next.1 + - @backstage/plugin-search-backend-module-pg@0.5.28-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + - @backstage/plugin-signals-backend@0.1.5-next.1 + - @backstage/plugin-techdocs-backend@1.10.6-next.1 + - example-app@0.2.98-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.6-next.1 + - @backstage/plugin-signals-node@0.1.5-next.1 + +## 0.2.99-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-devtools-backend@0.3.5-next.0 + - @backstage/plugin-techdocs-backend@1.10.6-next.0 + - @backstage/plugin-catalog-backend@1.23.0-next.0 + - @backstage/plugin-search-backend@1.5.10-next.0 + - @backstage/plugin-proxy-backend@0.5.0-next.0 + - @backstage/plugin-auth-backend@0.22.6-next.0 + - @backstage/plugin-app-backend@0.3.68-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.4.2-next.0 + - @backstage/plugin-search-backend-module-pg@0.5.28-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/plugin-signals-backend@0.1.5-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/plugin-scaffolder-backend@1.22.8-next.0 + - @backstage/plugin-kubernetes-backend@0.17.2-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.25-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.25-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.0 + - example-app@0.2.98-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.6-next.0 + - @backstage/plugin-events-backend@0.3.6-next.0 + - @backstage/plugin-permission-backend@0.5.43-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.20-next.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.1-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.36-next.0 + - @backstage/plugin-signals-node@0.1.5-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.11.0 + - @backstage/plugin-permission-common@0.7.13 + ## 0.2.98 ### Patch Changes diff --git a/packages/backend-legacy/README.md b/packages/backend-legacy/README.md index 20e68dc746..0de555fe70 100644 --- a/packages/backend-legacy/README.md +++ b/packages/backend-legacy/README.md @@ -31,7 +31,7 @@ The backend starts up on port 7007 per default. ### Debugging -The backend is a node process that can be inspected to allow breakpoints and live debugging. To enable this, pass the `--inspect` flag to [backend:dev](https://backstage.io/docs/local-dev/cli-build-system#backend-development). +The backend is a node process that can be inspected to allow breakpoints and live debugging. To enable this, pass the `--inspect` flag to [backend:dev](https://backstage.io/docs/tooling/cli/build-system#backend-development). To debug the backend in [Visual Studio Code](https://code.visualstudio.com/): diff --git a/packages/backend-legacy/package.json b/packages/backend-legacy/package.json index d5a3065df5..819a33ea5d 100644 --- a/packages/backend-legacy/package.json +++ b/packages/backend-legacy/package.json @@ -1,6 +1,6 @@ { "name": "example-backend-legacy", - "version": "0.2.98", + "version": "0.2.99-next.2", "backstage": { "role": "backend" }, diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md index d3d8e4bdf8..c6cf035fcf 100644 --- a/packages/backend-openapi-utils/CHANGELOG.md +++ b/packages/backend-openapi-utils/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/backend-openapi-utils +## 0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/errors@1.2.4 + +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/errors@1.2.4 + ## 0.1.11 ### Patch Changes diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json index badda72b04..c50c9ec1f8 100644 --- a/packages/backend-openapi-utils/package.json +++ b/packages/backend-openapi-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-openapi-utils", "description": "OpenAPI typescript support.", - "version": "0.1.11", + "version": "0.1.12-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md index 494634a9de..347f923808 100644 --- a/packages/backend-plugin-api/CHANGELOG.md +++ b/packages/backend-plugin-api/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/backend-plugin-api +## 0.6.19-next.2 + +### Patch Changes + +- 3aa3fc7: Marked the `TokenManagerService` and `IdentityService` types as deprecated +- Updated dependencies + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + +## 0.6.19-next.1 + +### Patch Changes + +- 9e63318: Added an optional `accessRestrictions` to external access service tokens and service principals in general, such that you can limit their access to certain plugins or permissions. +- 0665b7e: Renamed `BackendPluginConfig`, `BackendModuleConfig`, and `ExtensionPointConfig` respectively to `CreateBackendPluginOptions`, `CreateBackendModuleOptions`, and `CreateExtensionPointOptions` to standardize frontend and backend factories signatures. +- Updated dependencies + - @backstage/plugin-auth-node@0.4.14-next.1 + +## 0.6.19-next.0 + +### Patch Changes + +- 6551b3d: Moved the declaration of the `SchedulerService` here, along with prefixed versions of all of the types it depends on, from `@backstage/backend-tasks` +- 1779188: Start using the `isDatabaseConflictError` helper from the `@backstage/backend-plugin-api` package in order to avoid dependency with the soon to deprecate `@backstage/backend-common` package. +- Updated dependencies + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + ## 0.6.18 ### Patch Changes diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 932ccc74ab..70ab3d8eed 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -8,55 +8,50 @@ import { AuthorizePermissionRequest } from '@backstage/plugin-permission-common'; import { AuthorizePermissionResponse } from '@backstage/plugin-permission-common'; import { Config } from '@backstage/config'; +import { Duration } from 'luxon'; import { Handler } from 'express'; +import { HumanDuration } from '@backstage/types'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { isChildPath } from '@backstage/cli-common'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; +import { PermissionAttributes } from '@backstage/plugin-permission-common'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; -import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { QueryPermissionRequest } from '@backstage/plugin-permission-common'; import { QueryPermissionResponse } from '@backstage/plugin-permission-common'; import { Readable } from 'stream'; import { Request as Request_2 } from 'express'; import { Response as Response_2 } from 'express'; -// @public (undocumented) +// @public export interface AuthService { - // (undocumented) authenticate( token: string, options?: { allowLimitedAccess?: boolean; }, ): Promise; - // (undocumented) getLimitedUserToken( credentials: BackstageCredentials, ): Promise<{ token: string; expiresAt: Date; }>; - // (undocumented) getNoneCredentials(): Promise>; - // (undocumented) getOwnServiceCredentials(): Promise< BackstageCredentials >; - // (undocumented) getPluginRequestToken(options: { onBehalfOf: BackstageCredentials; targetPluginId: string; }): Promise<{ token: string; }>; - // (undocumented) isPrincipal( credentials: BackstageCredentials, type: TType, ): credentials is BackstageCredentials; - // (undocumented) listPublicServiceKeys(): Promise<{ keys: JsonObject[]; }>; @@ -68,13 +63,8 @@ export interface BackendFeature { $$type: '@backstage/BackendFeature'; } -// @public -export interface BackendModuleConfig { - moduleId: string; - pluginId: string; - // (undocumented) - register(reg: BackendModuleRegistrationPoints): void; -} +// @public @deprecated (undocumented) +export type BackendModuleConfig = CreateBackendModuleOptions; // @public export interface BackendModuleRegistrationPoints { @@ -96,12 +86,8 @@ export interface BackendModuleRegistrationPoints { }): void; } -// @public -export interface BackendPluginConfig { - pluginId: string; - // (undocumented) - register(reg: BackendPluginRegistrationPoints): void; -} +// @public @deprecated (undocumented) +export type BackendPluginConfig = CreateBackendPluginOptions; // @public export interface BackendPluginRegistrationPoints { @@ -123,19 +109,27 @@ export interface BackendPluginRegistrationPoints { }): void; } -// @public (undocumented) +// @public export type BackstageCredentials = { $$type: '@backstage/BackstageCredentials'; expiresAt?: Date; principal: TPrincipal; }; -// @public (undocumented) +// @public export type BackstageNonePrincipal = { type: 'none'; }; -// @public (undocumented) +// @public +export type BackstagePrincipalAccessRestrictions = { + permissionNames?: string[]; + permissionAttributes?: { + action?: Array['action']>; + }; +}; + +// @public export type BackstagePrincipalTypes = { user: BackstageUserPrincipal; service: BackstageServicePrincipal; @@ -143,13 +137,14 @@ export type BackstagePrincipalTypes = { unknown: unknown; }; -// @public (undocumented) +// @public export type BackstageServicePrincipal = { type: 'service'; subject: string; + accessRestrictions?: BackstagePrincipalAccessRestrictions; }; -// @public (undocumented) +// @public export interface BackstageUserInfo { // (undocumented) ownershipEntityRefs: string[]; @@ -157,7 +152,7 @@ export interface BackstageUserInfo { userEntityRef: string; } -// @public (undocumented) +// @public export type BackstageUserPrincipal = { type: 'user'; userEntityRef: string; @@ -203,26 +198,48 @@ export namespace coreServices { const rootLifecycle: ServiceRef; const rootLogger: ServiceRef; const scheduler: ServiceRef; - const tokenManager: ServiceRef; + const // @deprecated + tokenManager: ServiceRef; const urlReader: ServiceRef; - const identity: ServiceRef; + const // @deprecated + identity: ServiceRef; } // @public export function createBackendModule( - config: BackendModuleConfig, + options: CreateBackendModuleOptions, ): () => BackendFeature; +// @public +export interface CreateBackendModuleOptions { + moduleId: string; + pluginId: string; + // (undocumented) + register(reg: BackendModuleRegistrationPoints): void; +} + // @public export function createBackendPlugin( - config: BackendPluginConfig, + options: CreateBackendPluginOptions, ): () => BackendFeature; +// @public +export interface CreateBackendPluginOptions { + pluginId: string; + // (undocumented) + register(reg: BackendPluginRegistrationPoints): void; +} + // @public export function createExtensionPoint( - config: ExtensionPointConfig, + options: CreateExtensionPointOptions, ): ExtensionPoint; +// @public +export interface CreateExtensionPointOptions { + id: string; +} + // @public export function createServiceFactory< TService, @@ -307,14 +324,11 @@ export type ExtensionPoint = { $$type: '@backstage/ExtensionPoint'; }; -// @public -export interface ExtensionPointConfig { - id: string; -} +// @public @deprecated (undocumented) +export type ExtensionPointConfig = CreateExtensionPointOptions; -// @public (undocumented) +// @public export interface HttpAuthService { - // (undocumented) credentials( req: Request_2, options?: { @@ -322,7 +336,6 @@ export interface HttpAuthService { allowLimitedAccess?: boolean; }, ): Promise>; - // (undocumented) issueUserCookie( res: Response_2, options?: { @@ -333,15 +346,13 @@ export interface HttpAuthService { }>; } -// @public (undocumented) +// @public export interface HttpRouterService { - // (undocumented) addAuthPolicy(policy: HttpRouterServiceAuthPolicy): void; - // (undocumented) use(handler: Handler): void; } -// @public (undocumented) +// @public export interface HttpRouterServiceAuthPolicy { // (undocumented) allow: 'unauthenticated' | 'user-cookie'; @@ -349,12 +360,15 @@ export interface HttpRouterServiceAuthPolicy { path: string; } -// @public (undocumented) +// @public @deprecated export interface IdentityService extends IdentityApi {} export { isChildPath }; -// @public (undocumented) +// @public +export function isDatabaseConflictError(e: unknown): boolean; + +// @public export interface LifecycleService { addShutdownHook( hook: LifecycleServiceShutdownHook, @@ -396,14 +410,12 @@ export interface LoggerService { warn(message: string, meta?: Error | JsonObject): void; } -// @public (undocumented) +// @public export interface PermissionsService extends PermissionEvaluator { - // (undocumented) authorize( requests: AuthorizePermissionRequest[], options?: PermissionsServiceRequestOptions, ): Promise; - // (undocumented) authorizeConditional( requests: QueryPermissionRequest[], options?: PermissionsServiceRequestOptions, @@ -419,9 +431,8 @@ export type PermissionsServiceRequestOptions = credentials: BackstageCredentials; }; -// @public (undocumented) +// @public export interface PluginMetadataService { - // (undocumented) getId(): string; } @@ -450,6 +461,11 @@ export interface PluginServiceFactoryConfig< service: ServiceRef; } +// @public +export function readSchedulerServiceTaskScheduleDefinitionFromConfig( + config: Config, +): SchedulerServiceTaskScheduleDefinition; + // @public export type ReadTreeOptions = { filter?( @@ -505,18 +521,18 @@ export function resolvePackagePath(name: string, ...paths: string[]): string; // @public export function resolveSafeChildPath(base: string, path: string): string; -// @public (undocumented) +// @public export interface RootConfigService extends Config {} -// @public (undocumented) +// @public export interface RootHttpRouterService { use(path: string, handler: Handler): void; } -// @public (undocumented) +// @public export interface RootLifecycleService extends LifecycleService {} -// @public (undocumented) +// @public export interface RootLoggerService extends LoggerService {} // @public (undocumented) @@ -536,8 +552,70 @@ export interface RootServiceFactoryConfig< service: ServiceRef; } -// @public (undocumented) -export interface SchedulerService extends PluginTaskScheduler {} +// @public +export interface SchedulerService { + createScheduledTaskRunner( + schedule: SchedulerServiceTaskScheduleDefinition, + ): SchedulerServiceTaskRunner; + getScheduledTasks(): Promise; + scheduleTask( + task: SchedulerServiceTaskScheduleDefinition & + SchedulerServiceTaskInvocationDefinition, + ): Promise; + triggerTask(id: string): Promise; +} + +// @public +export type SchedulerServiceTaskDescriptor = { + id: string; + scope: 'global' | 'local'; + settings: { + version: number; + } & JsonObject; +}; + +// @public +export type SchedulerServiceTaskFunction = + | ((abortSignal: AbortSignal) => void | Promise) + | (() => void | Promise); + +// @public +export interface SchedulerServiceTaskInvocationDefinition { + fn: SchedulerServiceTaskFunction; + id: string; + signal?: AbortSignal; +} + +// @public +export interface SchedulerServiceTaskRunner { + run(task: SchedulerServiceTaskInvocationDefinition): Promise; +} + +// @public +export interface SchedulerServiceTaskScheduleDefinition { + frequency: + | { + cron: string; + } + | Duration + | HumanDuration; + initialDelay?: Duration | HumanDuration; + scope?: 'global' | 'local'; + timeout: Duration | HumanDuration; +} + +// @public +export interface SchedulerServiceTaskScheduleDefinitionConfig { + frequency: + | { + cron: string; + } + | string + | HumanDuration; + initialDelay?: string | HumanDuration; + scope?: 'global' | 'local'; + timeout: string | HumanDuration; +} // @public export type SearchOptions = { @@ -594,7 +672,7 @@ export interface ServiceRefConfig { scope?: TScope; } -// @public +// @public @deprecated export interface TokenManagerService { authenticate(token: string): Promise; getToken(): Promise<{ @@ -609,9 +687,8 @@ export interface UrlReaderService { search(url: string, options?: SearchOptions): Promise; } -// @public (undocumented) +// @public export interface UserInfoService { - // (undocumented) getUserInfo(credentials: BackstageCredentials): Promise; } ``` diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index 3199c97bda..da33828aae 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-plugin-api", - "version": "0.6.18", + "version": "0.6.19-next.2", "description": "Core API used by Backstage backend plugins", "backstage": { "role": "node-library" @@ -52,7 +52,6 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/backend-tasks": "workspace:^", "@backstage/cli-common": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", @@ -60,8 +59,10 @@ "@backstage/plugin-permission-common": "workspace:^", "@backstage/types": "workspace:^", "@types/express": "^4.17.6", + "@types/luxon": "^3.0.0", "express": "^4.17.1", - "knex": "^3.0.0" + "knex": "^3.0.0", + "luxon": "^3.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/packages/backend-plugin-api/src/services/definitions/AuthService.ts b/packages/backend-plugin-api/src/services/definitions/AuthService.ts index 827b4122cd..dce5e9be3b 100644 --- a/packages/backend-plugin-api/src/services/definitions/AuthService.ts +++ b/packages/backend-plugin-api/src/services/definitions/AuthService.ts @@ -14,18 +14,33 @@ * limitations under the License. */ +import { PermissionAttributes } from '@backstage/plugin-permission-common'; import { JsonObject } from '@backstage/types'; /** + * Represents a user principal (for example when a user Backstage token issued + * by the auth backend was given to a request). + * + * @remarks + * + * Additional information about the user can be fetched using the + * {@link UserInfoService}. + * * @public */ export type BackstageUserPrincipal = { type: 'user'; + /** + * The entity ref of the user entity that this principal represents. + */ userEntityRef: string; }; /** + * Represents a principal that is not authenticated (for example when no token + * at all was given to a request). + * * @public */ export type BackstageNonePrincipal = { @@ -33,27 +48,105 @@ export type BackstageNonePrincipal = { }; /** + * Represents a service principal (for example when an external access method + * token was given to a request, or the caller was a Backstage backend plugin). * @public */ export type BackstageServicePrincipal = { type: 'service'; - // Exact format TBD, possibly 'plugin:' or 'external:' - subject: string; + /** + * A string that represents the service. + * + * @remarks + * + * This string is only informational, has no well defined semantics, and + * should never be used to drive actual logic in code. + */ + subject: string; // Exact format TBD, possibly 'plugin:' or 'external:' + + /** + * The access restrictions that apply to this principal. + * + * @remarks + * + * If no access restrictions are provided the principal is assumed to have + * unlimited access, at a framework level. The permissions system and + * individual plugins may or may not still apply additional access controls on + * top of this. + */ + accessRestrictions?: BackstagePrincipalAccessRestrictions; }; /** + * The access restrictions that apply to a given principal. + * + * @public + */ +export type BackstagePrincipalAccessRestrictions = { + /** + * If given, the principal is limited to only performing actions with these + * named permissions. + * + * Note that this only applies where permissions checks are enabled in the + * first place. Endpoints that are not protected by the permissions system at + * all, are not affected by this setting. + * + * This array always has at least one element, or is missing entirely. + */ + permissionNames?: string[]; + /** + * If given, the principal is limited to only performing actions whose + * permissions have these attributes. + * + * Note that this only applies where permissions checks are enabled in the + * first place. Endpoints that are not protected by the permissions system at + * all, are not affected by this setting. + * + * This object always has at least one key, or is missing entirely. + */ + permissionAttributes?: { + /** + * Match any of these action values. This array always has at least one + * element, or is missing entirely. + */ + action?: Array['action']>; + }; +}; + +/** + * An opaque representation of credentials, for example as passed in a + * request-response flow. + * * @public */ export type BackstageCredentials = { $$type: '@backstage/BackstageCredentials'; + /** + * If the credentials have a limited lifetime, this is the time at which they + * expire and may no longer be accepted by a receiver. + */ expiresAt?: Date; + /** + * The principal (originator) of the request. + * + * @remarks + * + * This is semantically the originator of a request chain, and may or may not + * represent the immediate caller of your service. For example, in + * on-behalf-of scenarios, the immediate caller may be an intermediary backend + * service, but the principal may still be a user that was the original + * caller. + */ principal: TPrincipal; }; /** + * The types of principal that can be represented in a + * {@link BackstageCredentials} object. + * * @public */ export type BackstagePrincipalTypes = { @@ -64,36 +157,95 @@ export type BackstagePrincipalTypes = { }; /** + * Provides token authentication and credentials management. + * + * See the {@link https://backstage.io/docs/backend-system/core-services/auth | service documentation} for more details. + * * @public */ export interface AuthService { + /** + * Verifies a token and returns the associated credentials. + */ authenticate( token: string, options?: { + /** + * If set to true, allow limited access tokens (such as cookies). + * + * If this flag is not set, or is set to false, calls with limited access + * tokens will lead to a {@link @backstage/errors#NotAllowedError} being + * thrown. + */ allowLimitedAccess?: boolean; }, ): Promise; + /** + * Checks if the given credentials are of the given type, and narrows the + * TypeScript type accordingly if there's a match. + */ isPrincipal( credentials: BackstageCredentials, type: TType, ): credentials is BackstageCredentials; + /** + * Create a credentials object that represents an unauthenticated caller. + */ getNoneCredentials(): Promise>; + /** + * Create a credentials object that represents the current service itself. + */ getOwnServiceCredentials(): Promise< BackstageCredentials >; + /** + * Issue a token that can be used for authenticating calls towards other + * backend plugins. + * + * @remarks + * + * This method should be called before each request. Do not cold on to the + * issued token and reuse it for future calls. + */ getPluginRequestToken(options: { + /** + * The credentials of the originator of the request. + * + * @remarks + * + * This is most commonly the result of + * {@link AuthService.getOwnServiceCredentials} when the current service is + * the originator, or the output of {@link HttpAuthService.credentials} when + * performing requests on behalf of an incoming request identity. + */ onBehalfOf: BackstageCredentials; + /** + * The ID of the plugin that the request is being made to. + */ targetPluginId: string; }): Promise<{ token: string }>; + /** + * Issue a limited user token that can be used e.g. in cookie flows. + */ getLimitedUserToken( + /** + * The credentials that this token should represent. Must be a user + * principal. Commonly the output of {@link HttpAuthService.credentials} is + * used as the input. + */ credentials: BackstageCredentials, ): Promise<{ token: string; expiresAt: Date }>; + /** + * Retrieve the public keys that have been used to sign tokens that were + * issued by this service. This list is periodically pruned from keys that are + * significantly past their expiry. + */ listPublicServiceKeys(): Promise<{ keys: JsonObject[]; }>; diff --git a/packages/backend-plugin-api/src/services/definitions/CacheService.ts b/packages/backend-plugin-api/src/services/definitions/CacheService.ts index ccf88823b0..3b13292a58 100644 --- a/packages/backend-plugin-api/src/services/definitions/CacheService.ts +++ b/packages/backend-plugin-api/src/services/definitions/CacheService.ts @@ -47,6 +47,8 @@ export type CacheServiceOptions = { * A pre-configured, storage agnostic cache service suitable for use by * Backstage plugins. * + * See the {@link https://backstage.io/docs/backend-system/core-services/cache | service documentation} for more details. + * * @public */ export interface CacheService { diff --git a/packages/backend-plugin-api/src/services/definitions/DatabaseService.ts b/packages/backend-plugin-api/src/services/definitions/DatabaseService.ts index 20b7b3b5dc..c7f0e1ccf1 100644 --- a/packages/backend-plugin-api/src/services/definitions/DatabaseService.ts +++ b/packages/backend-plugin-api/src/services/definitions/DatabaseService.ts @@ -17,7 +17,9 @@ import { Knex } from 'knex'; /** - * The DatabaseService manages access to databases that Plugins get. + * Manages access to databases that plugins get. + * + * See the {@link https://backstage.io/docs/backend-system/core-services/database | service documentation} for more details. * * @public */ diff --git a/packages/backend-plugin-api/src/services/definitions/DiscoveryService.ts b/packages/backend-plugin-api/src/services/definitions/DiscoveryService.ts index c3f0820c28..af92a4abb5 100644 --- a/packages/backend-plugin-api/src/services/definitions/DiscoveryService.ts +++ b/packages/backend-plugin-api/src/services/definitions/DiscoveryService.ts @@ -18,6 +18,10 @@ * The DiscoveryService is used to provide a mechanism for backend * plugins to discover the endpoints for itself or other backend plugins. * + * See the {@link https://backstage.io/docs/backend-system/core-services/discovery | service documentation} for more details. + * + * @remarks + * * The purpose of the discovery API is to allow for many different deployment * setups and routing methods through a central configuration, instead * of letting each individual plugin manage that configuration. @@ -32,13 +36,15 @@ export interface DiscoveryService { /** * Returns the internal HTTP base URL for a given plugin, without a trailing slash. * + * @remarks + * * The returned URL should point to an internal endpoint for the plugin, with * the shortest route possible. The URL should be used for service-to-service * communication within a Backstage backend deployment. * - * This method must always be called just before making a request, as opposed to - * fetching the URL when constructing an API client. That is to ensure that more - * flexible routing patterns can be supported. + * This method must always be called just before making each request, as opposed to + * fetching the URL once when constructing an API client. That is to ensure that more + * flexible routing patterns can be supported where a different result might be returned each time. * * For example, asking for the URL for `catalog` may return something * like `http://10.1.2.3/api/catalog` @@ -48,6 +54,8 @@ export interface DiscoveryService { /** * Returns the external HTTP base backend URL for a given plugin, without a trailing slash. * + * @remarks + * * The returned URL should point to an external endpoint for the plugin, such that * it is reachable from the Backstage frontend and other external services. The returned * URL should be usable for example as a callback / webhook URL. diff --git a/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts b/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts index 5d1a2090d4..718aa9b532 100644 --- a/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts +++ b/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts @@ -17,19 +17,84 @@ import { Request, Response } from 'express'; import { BackstageCredentials, BackstagePrincipalTypes } from './AuthService'; -/** @public */ +/** + * Provides handling of credentials in an ongoing request. + * + * See the {@link https://backstage.io/docs/backend-system/core-services/http-auth | service documentation} for more details. + * + * @public + */ export interface HttpAuthService { + /** + * Extracts the caller's credentials from a request. + * + * @remarks + * + * The credentials have been validated before returning, and are guaranteed to + * adhere to whatever policies have been added to this route using + * {@link HttpRouterService.addAuthPolicy}, if any. + * + * Further restrictions can be imposed by passing in options that control the + * allowed types of credential. + * + * You can narrow the returned credentials object to specific principal types + * using {@link AuthService.isPrincipal}. + */ credentials( + /** + * An Express request object. + */ req: Request, + /** + * Optional further restrictions. + */ options?: { + /** + * If specified, allow only principals of the given type(s). + * + * If the incoming credentials were not of a type that matched this + * restriction, a {@link @backstage/errors#NotAllowedError} is thrown. + * + * The default is to allow user and service principals. + */ allow?: Array; + /** + * If set to true, allow limited access tokens (such as cookies). + * + * If this flag is not set, or is set to false, calls with limited access + * tokens will lead to a {@link @backstage/errors#NotAllowedError} being + * thrown. + */ allowLimitedAccess?: boolean; }, ): Promise>; + /** + * Issues a limited access token as a cookie on the given response object. + * This is only possible for requests that were originally made with user + * credentials (such as a Backstage token). + * + * This must be called before sending any payload data. + */ issueUserCookie( + /** + * An Express response object. + */ res: Response, + /** + * Optional further settings. + */ options?: { + /** + * Issue the cookie for this specific credential. Must be a "user" type + * principal, or a "none" type (which leads to deleting the cookie). + * + * @remarks + * + * Normally you do not have to specify this option, because the default + * behavior is to extract the credentials from the request that + * corresponded to the given respnse. + */ credentials?: BackstageCredentials; }, ): Promise<{ expiresAt: Date }>; diff --git a/packages/backend-plugin-api/src/services/definitions/HttpRouterService.ts b/packages/backend-plugin-api/src/services/definitions/HttpRouterService.ts index 695b337754..92f7e950f9 100644 --- a/packages/backend-plugin-api/src/services/definitions/HttpRouterService.ts +++ b/packages/backend-plugin-api/src/services/definitions/HttpRouterService.ts @@ -16,17 +16,51 @@ import { Handler } from 'express'; -/** @public */ +/** + * Options for {@link HttpRouterService.addAuthPolicy}. + * + * @public + */ export interface HttpRouterServiceAuthPolicy { path: string; allow: 'unauthenticated' | 'user-cookie'; } /** + * Allows plugins to register HTTP routes. + * + * See the {@link https://backstage.io/docs/backend-system/core-services/http-router | service documentation} for more details. + * * @public */ export interface HttpRouterService { + /** + * Registers an Express request handler under the plugin's base router. This + * typically makes its base path `/api/`. + */ use(handler: Handler): void; + /** + * Adds an auth policy to the router. This is used to allow unauthenticated or + * cookie based access to parts of a plugin's API. + * + * @remarks + * + * The paths given follow the same pattern as the routers given to the `use` + * method, that is, they are relative to the plugin's base URL, and can + * contain placeholders. + * + * @example + * + * ```ts + * http.addAuthPolicy({ + * path: '/static/:id', + * allow: 'user-cookie', + * }); + * ``` + * + * This allows limited access tokens via cookies on the + * `/api//static/*` paths, but not unauthenticated access. + */ addAuthPolicy(policy: HttpRouterServiceAuthPolicy): void; } diff --git a/packages/backend-plugin-api/src/services/definitions/IdentityService.ts b/packages/backend-plugin-api/src/services/definitions/IdentityService.ts index 330aaf6447..daa45b8200 100644 --- a/packages/backend-plugin-api/src/services/definitions/IdentityService.ts +++ b/packages/backend-plugin-api/src/services/definitions/IdentityService.ts @@ -16,5 +16,12 @@ import { IdentityApi } from '@backstage/plugin-auth-node'; -/** @public */ +/** + * This is the legacy service for identity handling in Backstage. Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead. + * + * See the {@link https://backstage.io/docs/backend-system/core-services/identity | service documentation} for more details. + * + * @public + * @deprecated Please {@link https://backstage.io/docs/tutorials/auth-service-migration | migrate} to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead. + */ export interface IdentityService extends IdentityApi {} diff --git a/packages/backend-plugin-api/src/services/definitions/LifecycleService.ts b/packages/backend-plugin-api/src/services/definitions/LifecycleService.ts index e0641e0457..62ccc54f82 100644 --- a/packages/backend-plugin-api/src/services/definitions/LifecycleService.ts +++ b/packages/backend-plugin-api/src/services/definitions/LifecycleService.ts @@ -47,6 +47,10 @@ export interface LifecycleServiceShutdownOptions { } /** + * Provides registration of plugin startup and shutdown lifecycle hooks. + * + * See the {@link https://backstage.io/docs/backend-system/core-services/lifecycle | service documentation} for more details. + * * @public */ export interface LifecycleService { diff --git a/packages/backend-plugin-api/src/services/definitions/LoggerService.ts b/packages/backend-plugin-api/src/services/definitions/LoggerService.ts index 2e4edc60be..86831d4520 100644 --- a/packages/backend-plugin-api/src/services/definitions/LoggerService.ts +++ b/packages/backend-plugin-api/src/services/definitions/LoggerService.ts @@ -19,6 +19,8 @@ import { JsonObject } from '@backstage/types'; /** * A service that provides a logging facility. * + * See the {@link https://backstage.io/docs/backend-system/core-services/logger | service documentation} for more details. + * * @public */ export interface LoggerService { diff --git a/packages/backend-plugin-api/src/services/definitions/PermissionsService.ts b/packages/backend-plugin-api/src/services/definitions/PermissionsService.ts index 1db15ce5e5..c5046a988b 100644 --- a/packages/backend-plugin-api/src/services/definitions/PermissionsService.ts +++ b/packages/backend-plugin-api/src/services/definitions/PermissionsService.ts @@ -37,13 +37,46 @@ export type PermissionsServiceRequestOptions = credentials: BackstageCredentials; }; -/** @public */ +/** + * Permission system integration for authorization of user/service actions. + * + * See the {@link https://backstage.io/docs/permissions/overview | permissions documentation} + * and the {@link https://backstage.io/docs/backend-system/core-services/permissions | service documentation} + * for more details. + * + * @public + */ export interface PermissionsService extends PermissionEvaluator { + /** + * Evaluates + * {@link @backstage/plugin-permission-common#Permission | Permissions} and + * returns definitive decisions. + * + * @remarks + * + * The returned array has the same number of items, in the same order, as the + * given requests. + */ authorize( requests: AuthorizePermissionRequest[], options?: PermissionsServiceRequestOptions, ): Promise; + /** + * Evaluates {@link @backstage/plugin-permission-common#ResourcePermission | ResourcePermissions} and returns both definitive and + * conditional decisions, depending on the configured + * {@link @backstage/plugin-permission-node#PermissionPolicy}. + * + * @remarks + * + * This method is useful when the + * caller needs more control over the processing of conditional decisions. For example, a plugin + * backend may want to use {@link @backstage/plugin-permission-common#PermissionCriteria | conditions} in a database query instead of + * evaluating each resource in memory. + * + * The returned array has the same number of items, in the same order, as the + * given requests. + */ authorizeConditional( requests: QueryPermissionRequest[], options?: PermissionsServiceRequestOptions, diff --git a/packages/backend-plugin-api/src/services/definitions/PluginMetadataService.ts b/packages/backend-plugin-api/src/services/definitions/PluginMetadataService.ts index 48328e4db9..4fc3c45fd7 100644 --- a/packages/backend-plugin-api/src/services/definitions/PluginMetadataService.ts +++ b/packages/backend-plugin-api/src/services/definitions/PluginMetadataService.ts @@ -15,8 +15,15 @@ */ /** + * Access metadata about the current plugin. + * + * See the {@link https://backstage.io/docs/backend-system/core-services/plugin-metadata | service documentation} for more details. + * * @public */ export interface PluginMetadataService { + /** + * The ID of the current plugin. + */ getId(): string; } diff --git a/packages/backend-plugin-api/src/services/definitions/RootConfigService.ts b/packages/backend-plugin-api/src/services/definitions/RootConfigService.ts index 02343e4ee8..453a7cda37 100644 --- a/packages/backend-plugin-api/src/services/definitions/RootConfigService.ts +++ b/packages/backend-plugin-api/src/services/definitions/RootConfigService.ts @@ -17,6 +17,12 @@ import { Config } from '@backstage/config'; /** + * Provides access to static configuration. + * + * See the {@link https://backstage.io/docs/conf/ | configuration documentation} + * and the {@link https://backstage.io/docs/backend-system/core-services/root-config | service documentation} + * for more details. + * * @public */ export interface RootConfigService extends Config {} diff --git a/packages/backend-plugin-api/src/services/definitions/RootHttpRouterService.ts b/packages/backend-plugin-api/src/services/definitions/RootHttpRouterService.ts index e583e978fa..a1006b749b 100644 --- a/packages/backend-plugin-api/src/services/definitions/RootHttpRouterService.ts +++ b/packages/backend-plugin-api/src/services/definitions/RootHttpRouterService.ts @@ -17,6 +17,10 @@ import { Handler } from 'express'; /** + * HTTP route registration for root services. + * + * See the {@link https://backstage.io/docs/backend-system/core-services/root-http-router | service documentation} for more details. + * * @public */ export interface RootHttpRouterService { diff --git a/packages/backend-plugin-api/src/services/definitions/RootLifecycleService.ts b/packages/backend-plugin-api/src/services/definitions/RootLifecycleService.ts index e0e09cb859..b962fa68ba 100644 --- a/packages/backend-plugin-api/src/services/definitions/RootLifecycleService.ts +++ b/packages/backend-plugin-api/src/services/definitions/RootLifecycleService.ts @@ -16,5 +16,11 @@ import { LifecycleService } from './LifecycleService'; -/** @public */ +/** + * Registration of backend startup and shutdown lifecycle hooks. + * + * See the {@link https://backstage.io/docs/backend-system/core-services/root-lifecycle | service documentation} for more details. + * + * @public + */ export interface RootLifecycleService extends LifecycleService {} diff --git a/packages/backend-plugin-api/src/services/definitions/RootLoggerService.ts b/packages/backend-plugin-api/src/services/definitions/RootLoggerService.ts index ad318ef53f..5d79098e26 100644 --- a/packages/backend-plugin-api/src/services/definitions/RootLoggerService.ts +++ b/packages/backend-plugin-api/src/services/definitions/RootLoggerService.ts @@ -16,5 +16,11 @@ import { LoggerService } from './LoggerService'; -/** @public */ +/** + * Root-level logging. + * + * See the {@link https://backstage.io/docs/backend-system/core-services/root-logger | service documentation} for more details. + * + * @public + */ export interface RootLoggerService extends LoggerService {} diff --git a/packages/backend-plugin-api/src/services/definitions/SchedulerService.test.ts b/packages/backend-plugin-api/src/services/definitions/SchedulerService.test.ts new file mode 100644 index 0000000000..7b20487af4 --- /dev/null +++ b/packages/backend-plugin-api/src/services/definitions/SchedulerService.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { HumanDuration } from '@backstage/types'; +import { Duration } from 'luxon'; +import { readSchedulerServiceTaskScheduleDefinitionFromConfig } from './SchedulerService'; + +describe('readSchedulerServiceTaskScheduleDefinitionFromConfig', () => { + it('all valid values', () => { + const config = new ConfigReader({ + frequency: { + cron: '0 30 * * * *', + }, + timeout: 'PT3M', + initialDelay: { + minutes: 20, + }, + scope: 'global', + }); + + const result = readSchedulerServiceTaskScheduleDefinitionFromConfig(config); + + expect((result.frequency as { cron: string }).cron).toBe('0 30 * * * *'); + expect(result.timeout).toEqual(Duration.fromISO('PT3M')); + expect((result.initialDelay as HumanDuration).minutes).toEqual(20); + expect(result.scope).toBe('global'); + }); + + it('all valid required values', () => { + const config = new ConfigReader({ + frequency: { + cron: '0 30 * * * *', + }, + timeout: 'PT3M', + }); + + const result = readSchedulerServiceTaskScheduleDefinitionFromConfig(config); + + expect((result.frequency as { cron: string }).cron).toBe('0 30 * * * *'); + expect(result.timeout).toEqual(Duration.fromISO('PT3M')); + expect(result.initialDelay).toBeUndefined(); + expect(result.scope).toBeUndefined(); + }); + + it('fail without required frequency', () => { + const config = new ConfigReader({ + timeout: 'PT3M', + }); + + expect(() => + readSchedulerServiceTaskScheduleDefinitionFromConfig(config), + ).toThrow("Missing required config value at 'frequency'"); + }); + + it('fail without required timeout', () => { + const config = new ConfigReader({ + frequency: 'PT30M', + }); + + expect(() => + readSchedulerServiceTaskScheduleDefinitionFromConfig(config), + ).toThrow("Missing required config value at 'timeout'"); + }); + + it('invalid frequency key', () => { + const config = new ConfigReader({ + frequency: { + invalid: 'value', + }, + timeout: 'PT3M', + }); + + expect(() => + readSchedulerServiceTaskScheduleDefinitionFromConfig(config), + ).toThrow( + "Failed to read duration from config at 'frequency', Error: Needs one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'", + ); + }); + + it('invalid frequency value', () => { + const config = new ConfigReader({ + frequency: { + minutes: 'value', + }, + timeout: 'PT3M', + }); + + expect(() => + readSchedulerServiceTaskScheduleDefinitionFromConfig(config), + ).toThrow( + "Failed to read duration from config, Error: Unable to convert config value for key 'frequency.minutes' in 'mock-config' to a number", + ); + }); + + it('frequency value with additional invalid prop', () => { + const config = new ConfigReader({ + frequency: { + minutes: 20, + invalid: 'value', + }, + timeout: 'PT3M', + }); + + expect(() => + readSchedulerServiceTaskScheduleDefinitionFromConfig(config), + ).toThrow( + "Failed to read duration from config at 'frequency', Error: Unknown property 'invalid'; expected one or more of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'", + ); + }); + + it('invalid scope value', () => { + const config = new ConfigReader({ + frequency: { + years: 2, + }, + timeout: 'PT3M', + scope: 'invalid', + }); + + expect(() => + readSchedulerServiceTaskScheduleDefinitionFromConfig(config), + ).toThrow( + 'Only "global" or "local" are allowed for TaskScheduleDefinition.scope, but got: invalid', + ); + }); +}); diff --git a/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts b/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts index 07c436bd6b..91edf27ad6 100644 --- a/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts +++ b/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * 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. @@ -14,7 +14,393 @@ * limitations under the License. */ -import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { Config, readDurationFromConfig } from '@backstage/config'; +import { HumanDuration, JsonObject } from '@backstage/types'; +import { Duration } from 'luxon'; -/** @public */ -export interface SchedulerService extends PluginTaskScheduler {} +/** + * A function that can be called as a scheduled task. + * + * It may optionally accept an abort signal argument. When the signal triggers, + * processing should abort and return as quickly as possible. + * + * @public + */ +export type SchedulerServiceTaskFunction = + | ((abortSignal: AbortSignal) => void | Promise) + | (() => void | Promise); + +/** + * A semi-opaque type to describe an actively scheduled task. + * + * @public + */ +export type SchedulerServiceTaskDescriptor = { + /** + * The unique identifier of the task. + */ + id: string; + /** + * The scope of the task. + */ + scope: 'global' | 'local'; + /** + * The settings that control the task flow. This is a semi-opaque structure + * that is mainly there for debugging purposes. Do not make any assumptions + * about the contents of this field. + */ + settings: { version: number } & JsonObject; +}; + +/** + * Options that control the scheduling of a task. + * + * @public + */ +export interface SchedulerServiceTaskScheduleDefinition { + /** + * How often you want the task to run. The system does its best to avoid + * overlapping invocations. + * + * @remarks + * + * This is the best effort value; under some circumstances there can be + * deviations. For example, if the task runtime is longer than the frequency + * and the timeout has not been given or not been exceeded yet, the next + * invocation of this task will be delayed until after the previous one + * finishes. + * + * This is a required field. + */ + frequency: + | { + /** + * A crontab style string. + * + * @remarks + * + * Overview: + * + * ``` + * ┌────────────── second (optional) + * │ ┌──────────── minute + * │ │ ┌────────── hour + * │ │ │ ┌──────── day of month + * │ │ │ │ ┌────── month + * │ │ │ │ │ ┌──── day of week + * │ │ │ │ │ │ + * │ │ │ │ │ │ + * * * * * * * + * ``` + */ + cron: string; + } + | Duration + | HumanDuration; + + /** + * The maximum amount of time that a single task invocation can take, before + * it's considered timed out and gets "released" such that a new invocation + * is permitted to take place (possibly, then, on a different worker). + */ + timeout: Duration | HumanDuration; + + /** + * The amount of time that should pass before the first invocation happens. + * + * @remarks + * + * This can be useful in cold start scenarios to stagger or delay some heavy + * compute jobs. If no value is given for this field then the first invocation + * will happen as soon as possible according to the cadence. + * + * NOTE: This is a per-worker delay. If you have a cluster of workers all + * collaborating on a task that has its `scope` field set to `'global'`, then + * you may still see the task being processed by other long-lived workers, + * while any given single worker is in its initial sleep delay time e.g. after + * a deployment. Therefore, this parameter is not useful for "globally" pausing + * work; its main intended use is for individual machines to get a chance to + * reach some equilibrium at startup before triggering heavy batch workloads. + */ + initialDelay?: Duration | HumanDuration; + + /** + * Sets the scope of concurrency control / locking to apply for invocations of + * this task. + * + * @remarks + * + * When the scope is set to the default value `'global'`, the scheduler will + * attempt to ensure that only one worker machine runs the task at a time, + * according to the given cadence. This means that as the number of worker + * hosts increases, the invocation frequency of this task will not go up. + * Instead, the load is spread randomly across hosts. This setting is useful + * for tasks that access shared resources, for example catalog ingestion tasks + * where you do not want many machines to repeatedly import the same data and + * trample over each other. + * + * When the scope is set to `'local'`, there is no concurrency control across + * hosts. Each host runs the task according to the given cadence similarly to + * `setInterval`, but the runtime ensures that there are no overlapping runs. + * + * @defaultValue 'global' + */ + scope?: 'global' | 'local'; +} + +/** + * Config options for {@link SchedulerServiceTaskScheduleDefinition} + * that control the scheduling of a task. + * + * @public + */ +export interface SchedulerServiceTaskScheduleDefinitionConfig { + /** + * How often you want the task to run. The system does its best to avoid + * overlapping invocations. + * + * @remarks + * + * This is the best effort value; under some circumstances there can be + * deviations. For example, if the task runtime is longer than the frequency + * and the timeout has not been given or not been exceeded yet, the next + * invocation of this task will be delayed until after the previous one + * finishes. + * + * This is a required field. + */ + frequency: + | { + /** + * A crontab style string. + * + * @remarks + * + * Overview: + * + * ``` + * ┌────────────── second (optional) + * │ ┌──────────── minute + * │ │ ┌────────── hour + * │ │ │ ┌──────── day of month + * │ │ │ │ ┌────── month + * │ │ │ │ │ ┌──── day of week + * │ │ │ │ │ │ + * │ │ │ │ │ │ + * * * * * * * + * ``` + */ + cron: string; + } + | string + | HumanDuration; + + /** + * The maximum amount of time that a single task invocation can take, before + * it's considered timed out and gets "released" such that a new invocation + * is permitted to take place (possibly, then, on a different worker). + */ + timeout: string | HumanDuration; + + /** + * The amount of time that should pass before the first invocation happens. + * + * @remarks + * + * This can be useful in cold start scenarios to stagger or delay some heavy + * compute jobs. If no value is given for this field then the first invocation + * will happen as soon as possible according to the cadence. + * + * NOTE: This is a per-worker delay. If you have a cluster of workers all + * collaborating on a task that has its `scope` field set to `'global'`, then + * you may still see the task being processed by other long-lived workers, + * while any given single worker is in its initial sleep delay time e.g. after + * a deployment. Therefore, this parameter is not useful for "globally" pausing + * work; its main intended use is for individual machines to get a chance to + * reach some equilibrium at startup before triggering heavy batch workloads. + */ + initialDelay?: string | HumanDuration; + + /** + * Sets the scope of concurrency control / locking to apply for invocations of + * this task. + * + * @remarks + * + * When the scope is set to the default value `'global'`, the scheduler will + * attempt to ensure that only one worker machine runs the task at a time, + * according to the given cadence. This means that as the number of worker + * hosts increases, the invocation frequency of this task will not go up. + * Instead, the load is spread randomly across hosts. This setting is useful + * for tasks that access shared resources, for example catalog ingestion tasks + * where you do not want many machines to repeatedly import the same data and + * trample over each other. + * + * When the scope is set to `'local'`, there is no concurrency control across + * hosts. Each host runs the task according to the given cadence similarly to + * `setInterval`, but the runtime ensures that there are no overlapping runs. + * + * @defaultValue 'global' + */ + scope?: 'global' | 'local'; +} + +/** + * Options that apply to the invocation of a given task. + * + * @public + */ +export interface SchedulerServiceTaskInvocationDefinition { + /** + * A unique ID (within the scope of the plugin) for the task. + */ + id: string; + + /** + * The actual task function to be invoked regularly. + */ + fn: SchedulerServiceTaskFunction; + + /** + * An abort signal that, when triggered, will stop the recurring execution of + * the task. + */ + signal?: AbortSignal; +} + +/** + * A previously prepared task schedule, ready to be invoked. + * + * @public + */ +export interface SchedulerServiceTaskRunner { + /** + * Takes the schedule and executes an actual task using it. + * + * @param task - The actual runtime properties of the task + */ + run(task: SchedulerServiceTaskInvocationDefinition): Promise; +} + +/** + * Deals with the scheduling of distributed tasks, for a given plugin. + * + * See the {@link https://backstage.io/docs/backend-system/core-services/scheduler | service documentation} for more details. + * + * @public + */ +export interface SchedulerService { + /** + * Manually triggers a task by ID. + * + * If the task doesn't exist, a NotFoundError is thrown. If the task is + * currently running, a ConflictError is thrown. + * + * @param id - The task ID + */ + triggerTask(id: string): Promise; + + /** + * Schedules a task function for recurring runs. + * + * @remarks + * + * The `scope` task field controls whether to use coordinated exclusive + * invocation across workers, or to just coordinate within the current worker. + * + * This convenience method performs both the scheduling and invocation in one + * go. + * + * @param task - The task definition + */ + scheduleTask( + task: SchedulerServiceTaskScheduleDefinition & + SchedulerServiceTaskInvocationDefinition, + ): Promise; + + /** + * Creates a scheduled but dormant recurring task, ready to be launched at a + * later time. + * + * @remarks + * + * This method is useful for pre-creating a schedule in outer code to be + * passed into an inner implementation, such that the outer code controls + * scheduling while inner code controls implementation. + * + * @param schedule - The task schedule + */ + createScheduledTaskRunner( + schedule: SchedulerServiceTaskScheduleDefinition, + ): SchedulerServiceTaskRunner; + + /** + * Returns all scheduled tasks registered to this scheduler. + * + * @remarks + * + * This method is useful for triggering tasks manually using the triggerTask + * functionality. Note that the returned tasks contain only tasks that have + * been initialized in this instance of the scheduler. + * + * @returns Scheduled tasks + */ + getScheduledTasks(): Promise; +} + +function readDuration(config: Config, key: string): Duration | HumanDuration { + if (typeof config.get(key) === 'string') { + const value = config.getString(key); + const duration = Duration.fromISO(value); + if (!duration.isValid) { + throw new Error(`Invalid duration: ${value}`); + } + return duration; + } + + return readDurationFromConfig(config, { key }); +} + +function readCronOrDuration( + config: Config, + key: string, +): { cron: string } | Duration | HumanDuration { + const value = config.get(key); + if (typeof value === 'object' && (value as { cron?: string }).cron) { + return value as { cron: string }; + } + + return readDuration(config, key); +} + +/** + * Reads a {@link SchedulerServiceTaskScheduleDefinition} from config. Expects + * the config not to be the root config, but the config for the definition. + * + * @param config - config for a TaskScheduleDefinition. + * @public + */ +export function readSchedulerServiceTaskScheduleDefinitionFromConfig( + config: Config, +): SchedulerServiceTaskScheduleDefinition { + const frequency = readCronOrDuration(config, 'frequency'); + const timeout = readDuration(config, 'timeout'); + + const initialDelay = config.has('initialDelay') + ? readDuration(config, 'initialDelay') + : undefined; + + const scope = config.getOptionalString('scope'); + if (scope && !['global', 'local'].includes(scope)) { + throw new Error( + `Only "global" or "local" are allowed for TaskScheduleDefinition.scope, but got: ${scope}`, + ); + } + + return { + frequency, + timeout, + initialDelay, + scope: scope as 'global' | 'local' | undefined, + }; +} diff --git a/packages/backend-plugin-api/src/services/definitions/TokenManagerService.ts b/packages/backend-plugin-api/src/services/definitions/TokenManagerService.ts index c9d7edcaef..52d5d1ff9f 100644 --- a/packages/backend-plugin-api/src/services/definitions/TokenManagerService.ts +++ b/packages/backend-plugin-api/src/services/definitions/TokenManagerService.ts @@ -15,9 +15,12 @@ */ /** - * Interface for creating and validating tokens. + * This is the legacy service for creating and validating tokens. Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead. + * + * See the {@link https://backstage.io/docs/backend-system/core-services/token-manager | service documentation} for more details. * * @public + * @deprecated Please {@link https://backstage.io/docs/tutorials/auth-service-migration | migrate} to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead. */ export interface TokenManagerService { /** diff --git a/packages/backend-plugin-api/src/services/definitions/UrlReaderService.ts b/packages/backend-plugin-api/src/services/definitions/UrlReaderService.ts index 734dcd9902..8cb40ff202 100644 --- a/packages/backend-plugin-api/src/services/definitions/UrlReaderService.ts +++ b/packages/backend-plugin-api/src/services/definitions/UrlReaderService.ts @@ -19,6 +19,8 @@ import { Readable } from 'stream'; /** * A generic interface for fetching plain data from URLs. * + * See the {@link https://backstage.io/docs/backend-system/core-services/url-reader | service documentation} for more details. + * * @public */ export interface UrlReaderService { diff --git a/packages/backend-plugin-api/src/services/definitions/UserInfoService.ts b/packages/backend-plugin-api/src/services/definitions/UserInfoService.ts index 90f5306a58..438de79219 100644 --- a/packages/backend-plugin-api/src/services/definitions/UserInfoService.ts +++ b/packages/backend-plugin-api/src/services/definitions/UserInfoService.ts @@ -16,13 +16,27 @@ import { BackstageCredentials } from './AuthService'; -/** @public */ +/** + * Represents user information that is available to the backend, based on some + * user credentials. + * + * @public + */ export interface BackstageUserInfo { userEntityRef: string; ownershipEntityRefs: string[]; } -/** @public */ +/** + * Authenticated user information retrieval. + * + * See the {@link https://backstage.io/docs/backend-system/core-services/user-info | service documentation} for more details. + * + * @public + */ export interface UserInfoService { + /** + * Retrieve user information based on the provided credentials. + */ getUserInfo(credentials: BackstageCredentials): Promise; } diff --git a/packages/backend-plugin-api/src/services/definitions/coreServices.ts b/packages/backend-plugin-api/src/services/definitions/coreServices.ts index c760afc7b4..4e02611f80 100644 --- a/packages/backend-plugin-api/src/services/definitions/coreServices.ts +++ b/packages/backend-plugin-api/src/services/definitions/coreServices.ts @@ -172,6 +172,7 @@ export namespace coreServices { * The service reference for the plugin scoped {@link TokenManagerService}. * * @public + * @deprecated Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead */ export const tokenManager = createServiceRef< import('./TokenManagerService').TokenManagerService @@ -190,6 +191,7 @@ export namespace coreServices { * The service reference for the plugin scoped {@link IdentityService}. * * @public + * @deprecated Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead */ export const identity = createServiceRef< import('./IdentityService').IdentityService diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index 8a5176379f..f22213a285 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -20,6 +20,7 @@ export type { BackstageCredentials, BackstageUserPrincipal, BackstageServicePrincipal, + BackstagePrincipalAccessRestrictions, BackstagePrincipalTypes, BackstageNonePrincipal, } from './AuthService'; @@ -52,7 +53,16 @@ export type { PluginMetadataService } from './PluginMetadataService'; export type { RootHttpRouterService } from './RootHttpRouterService'; export type { RootLifecycleService } from './RootLifecycleService'; export type { RootLoggerService } from './RootLoggerService'; -export type { SchedulerService } from './SchedulerService'; +export { readSchedulerServiceTaskScheduleDefinitionFromConfig } from './SchedulerService'; +export type { + SchedulerService, + SchedulerServiceTaskDescriptor, + SchedulerServiceTaskFunction, + SchedulerServiceTaskInvocationDefinition, + SchedulerServiceTaskRunner, + SchedulerServiceTaskScheduleDefinition, + SchedulerServiceTaskScheduleDefinitionConfig, +} from './SchedulerService'; export type { TokenManagerService } from './TokenManagerService'; export type { ReadTreeOptions, diff --git a/packages/backend-plugin-api/src/services/index.ts b/packages/backend-plugin-api/src/services/index.ts index dd24127b88..01d3da444b 100644 --- a/packages/backend-plugin-api/src/services/index.ts +++ b/packages/backend-plugin-api/src/services/index.ts @@ -16,3 +16,4 @@ export * from './definitions'; export * from './system'; +export * from './utilities'; diff --git a/packages/backend-common/src/database/util.ts b/packages/backend-plugin-api/src/services/utilities/database.ts similarity index 100% rename from packages/backend-common/src/database/util.ts rename to packages/backend-plugin-api/src/services/utilities/database.ts diff --git a/packages/backend-plugin-api/src/services/utilities/index.ts b/packages/backend-plugin-api/src/services/utilities/index.ts new file mode 100644 index 0000000000..0eec4f3e84 --- /dev/null +++ b/packages/backend-plugin-api/src/services/utilities/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { isDatabaseConflictError } from './database'; diff --git a/packages/backend-plugin-api/src/wiring/factories.ts b/packages/backend-plugin-api/src/wiring/factories.ts index 9fa0f87bab..d0199e775a 100644 --- a/packages/backend-plugin-api/src/wiring/factories.ts +++ b/packages/backend-plugin-api/src/wiring/factories.ts @@ -30,7 +30,7 @@ import { * @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points} * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} */ -export interface ExtensionPointConfig { +export interface CreateExtensionPointOptions { /** * The ID of this extension point. * @@ -46,15 +46,15 @@ export interface ExtensionPointConfig { * @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points} */ export function createExtensionPoint( - config: ExtensionPointConfig, + options: CreateExtensionPointOptions, ): ExtensionPoint { return { - id: config.id, + id: options.id, get T(): T { throw new Error(`tried to read ExtensionPoint.T of ${this}`); }, toString() { - return `extensionPoint{${config.id}}`; + return `extensionPoint{${options.id}}`; }, $$type: '@backstage/ExtensionPoint', }; @@ -67,7 +67,7 @@ export function createExtensionPoint( * @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins} * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} */ -export interface BackendPluginConfig { +export interface CreateBackendPluginOptions { /** * The ID of this plugin. * @@ -85,7 +85,7 @@ export interface BackendPluginConfig { * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} */ export function createBackendPlugin( - config: BackendPluginConfig, + options: CreateBackendPluginOptions, ): () => BackendFeature { const factory: BackendFeatureFactory = () => { let registrations: InternalBackendPluginRegistration[]; @@ -102,7 +102,7 @@ export function createBackendPlugin( let init: InternalBackendPluginRegistration['init'] | undefined = undefined; - config.register({ + options.register({ registerExtensionPoint(ext, impl) { if (init) { throw new Error( @@ -124,14 +124,14 @@ export function createBackendPlugin( if (!init) { throw new Error( - `registerInit was not called by register in ${config.pluginId}`, + `registerInit was not called by register in ${options.pluginId}`, ); } registrations = [ { type: 'plugin', - pluginId: config.pluginId, + pluginId: options.pluginId, extensionPoints, init, }, @@ -152,7 +152,7 @@ export function createBackendPlugin( * @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules} * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} */ -export interface BackendModuleConfig { +export interface CreateBackendModuleOptions { /** * Should exactly match the `id` of the plugin that the module extends. * @@ -175,7 +175,7 @@ export interface BackendModuleConfig { * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} */ export function createBackendModule( - config: BackendModuleConfig, + options: CreateBackendModuleOptions, ): () => BackendFeature { const factory: BackendFeatureFactory = () => { let registrations: InternalBackendModuleRegistration[]; @@ -192,7 +192,7 @@ export function createBackendModule( let init: InternalBackendModuleRegistration['init'] | undefined = undefined; - config.register({ + options.register({ registerExtensionPoint(ext, impl) { if (init) { throw new Error( @@ -214,15 +214,15 @@ export function createBackendModule( if (!init) { throw new Error( - `registerInit was not called by register in ${config.moduleId} module for ${config.pluginId}`, + `registerInit was not called by register in ${options.moduleId} module for ${options.pluginId}`, ); } registrations = [ { type: 'module', - pluginId: config.pluginId, - moduleId: config.moduleId, + pluginId: options.pluginId, + moduleId: options.moduleId, extensionPoints, init, }, diff --git a/packages/backend-plugin-api/src/wiring/index.ts b/packages/backend-plugin-api/src/wiring/index.ts index 49f15c0b55..5197cec050 100644 --- a/packages/backend-plugin-api/src/wiring/index.ts +++ b/packages/backend-plugin-api/src/wiring/index.ts @@ -14,18 +14,44 @@ * limitations under the License. */ -export type { - BackendModuleConfig, - BackendPluginConfig, - ExtensionPointConfig, +import type { + CreateBackendPluginOptions, + CreateBackendModuleOptions, + CreateExtensionPointOptions, } from './factories'; + export { createBackendModule, createBackendPlugin, createExtensionPoint, } from './factories'; + export type { BackendModuleRegistrationPoints, BackendPluginRegistrationPoints, ExtensionPoint, } from './types'; + +export type { + CreateBackendPluginOptions, + CreateBackendModuleOptions, + CreateExtensionPointOptions, +}; + +/** + * @public + * @deprecated Use {@link CreateBackendPluginOptions} instead. + */ +export type BackendPluginConfig = CreateBackendPluginOptions; + +/** + * @public + * @deprecated Use {@link CreateBackendModuleOptions} instead. + */ +export type BackendModuleConfig = CreateBackendModuleOptions; + +/** + * @public + * @deprecated Use {@link CreateExtensionPointOptions} instead. + */ +export type ExtensionPointConfig = CreateExtensionPointOptions; diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md index 629f3e7853..cdc54dcec4 100644 --- a/packages/backend-tasks/CHANGELOG.md +++ b/packages/backend-tasks/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/backend-tasks +## 0.5.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.5.24-next.1 + +### Patch Changes + +- ed473cd: Updated the `TaskScheduleDefinitionConfig` deprecated comment to point to `SchedulerServiceTaskScheduleDefinitionConfig` +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + +## 0.5.24-next.0 + +### Patch Changes + +- 736bc3c: Marked all exports as deprecated and pointed at `@backstage/backend-plugin-api` and `@backstage/backend-defaults` +- 6a576dc: Deprecate the legacy `TaskScheduler.fromConfig` method and stop using the `getVoidlogger` in tests files to reduce the dependency on the soon-to-deprecate `backstage-common` package. +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.5.23 ### Patch Changes diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index 00ef28c9ce..6e8d6eeb2f 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -14,7 +14,7 @@ import { PluginDatabaseManager } from '@backstage/backend-common'; // @public @deprecated export type HumanDuration = HumanDuration_2; -// @public +// @public @deprecated export interface PluginTaskScheduler { createScheduledTaskRunner(schedule: TaskScheduleDefinition): TaskRunner; getScheduledTasks(): Promise; @@ -24,12 +24,12 @@ export interface PluginTaskScheduler { triggerTask(id: string): Promise; } -// @public +// @public @deprecated export function readTaskScheduleDefinitionFromConfig( config: Config, ): TaskScheduleDefinition; -// @public +// @public @deprecated export type TaskDescriptor = { id: string; scope: 'global' | 'local'; @@ -38,24 +38,24 @@ export type TaskDescriptor = { } & JsonObject; }; -// @public +// @public @deprecated export type TaskFunction = | ((abortSignal: AbortSignal) => void | Promise) | (() => void | Promise); -// @public +// @public @deprecated export interface TaskInvocationDefinition { fn: TaskFunction; id: string; signal?: AbortSignal; } -// @public +// @public @deprecated export interface TaskRunner { run(task: TaskInvocationDefinition): Promise; } -// @public +// @public @deprecated export interface TaskScheduleDefinition { frequency: | { @@ -68,7 +68,7 @@ export interface TaskScheduleDefinition { timeout: Duration | HumanDuration_2; } -// @public +// @public @deprecated export interface TaskScheduleDefinitionConfig { frequency: | { @@ -81,7 +81,7 @@ export interface TaskScheduleDefinitionConfig { timeout: string | HumanDuration_2; } -// @public +// @public @deprecated export class TaskScheduler { constructor( databaseManager: LegacyRootDatabaseService, @@ -94,7 +94,7 @@ export class TaskScheduler { databaseManager: PluginDatabaseManager; logger: LoggerService; }): PluginTaskScheduler; - // (undocumented) + // @deprecated (undocumented) static fromConfig( config: Config, options?: { diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index c0331c5e4c..029b73a013 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-tasks", "description": "Common distributed task management library for Backstage backends", - "version": "0.5.23", + "version": "0.5.24-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-tasks/src/deprecated.ts b/packages/backend-tasks/src/deprecated.ts index 4d86cb1700..629e5f6d4d 100644 --- a/packages/backend-tasks/src/deprecated.ts +++ b/packages/backend-tasks/src/deprecated.ts @@ -23,3 +23,5 @@ import { HumanDuration as TypesHumanDuration } from '@backstage/types'; * @deprecated Import from `@backstage/types` instead */ export type HumanDuration = TypesHumanDuration; + +export * from './tasks'; diff --git a/packages/backend-tasks/src/index.ts b/packages/backend-tasks/src/index.ts index d35917056c..0aae81b143 100644 --- a/packages/backend-tasks/src/index.ts +++ b/packages/backend-tasks/src/index.ts @@ -20,5 +20,4 @@ * @packageDocumentation */ -export * from './tasks'; export * from './deprecated'; diff --git a/packages/backend-tasks/src/tasks/LocalTaskWorker.test.ts b/packages/backend-tasks/src/tasks/LocalTaskWorker.test.ts index 4fd4949478..d4d3d16067 100644 --- a/packages/backend-tasks/src/tasks/LocalTaskWorker.test.ts +++ b/packages/backend-tasks/src/tasks/LocalTaskWorker.test.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { LocalTaskWorker } from './LocalTaskWorker'; +import { mockServices } from '@backstage/backend-test-utils'; describe('LocalTaskWorker', () => { - const logger = getVoidLogger(); + const logger = mockServices.logger.mock(); it('runs the happy path (with iso duration) and handles cancellation', async () => { const fn = jest.fn(); diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts index 4b40791cb5..77965632e9 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts @@ -14,8 +14,11 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { + TestDatabaseId, + TestDatabases, + mockServices, +} from '@backstage/backend-test-utils'; import { ConflictError, NotFoundError } from '@backstage/errors'; import { Duration } from 'luxon'; import { migrateBackendTasks } from '../database/migrateBackendTasks'; @@ -51,10 +54,8 @@ describe('PluginTaskManagerImpl', () => { async function init(databaseId: TestDatabaseId) { const knex = await databases.init(databaseId); await migrateBackendTasks(knex); - const manager = new PluginTaskSchedulerImpl( - async () => knex, - getVoidLogger(), - ); + const logger = mockServices.logger.mock(); + const manager = new PluginTaskSchedulerImpl(async () => knex, logger); return { knex, manager }; } diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.test.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.test.ts index 1f0363625e..93b5d7d189 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerJanitor.test.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { TestDatabases } from '@backstage/backend-test-utils'; +import { TestDatabases, mockServices } from '@backstage/backend-test-utils'; import { Knex } from 'knex'; import { Duration } from 'luxon'; import waitForExpect from 'wait-for-expect'; @@ -36,7 +35,7 @@ const getTask = async (knex: Knex): Promise => { }; describe('PluginTaskSchedulerJanitor', () => { - const logger = getVoidLogger(); + const logger = mockServices.logger.mock(); const databases = TestDatabases.create({ ids: [ /* 'MYSQL_8' not supported yet */ diff --git a/packages/backend-tasks/src/tasks/TaskScheduler.test.ts b/packages/backend-tasks/src/tasks/TaskScheduler.test.ts index d286eaee38..24ce73712f 100644 --- a/packages/backend-tasks/src/tasks/TaskScheduler.test.ts +++ b/packages/backend-tasks/src/tasks/TaskScheduler.test.ts @@ -14,8 +14,12 @@ * limitations under the License. */ -import { DatabaseManager, getVoidLogger } from '@backstage/backend-common'; -import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { DatabaseManager } from '@backstage/backend-common'; +import { + TestDatabaseId, + TestDatabases, + mockServices, +} from '@backstage/backend-test-utils'; import { Duration } from 'luxon'; import waitForExpect from 'wait-for-expect'; import { TaskScheduler } from './TaskScheduler'; @@ -24,7 +28,7 @@ import { createTestScopedSignal } from './__testUtils__/createTestScopedSignal'; jest.setTimeout(60_000); describe('TaskScheduler', () => { - const logger = getVoidLogger(); + const logger = mockServices.logger.mock(); const databases = TestDatabases.create(); const testScopedSignal = createTestScopedSignal(); diff --git a/packages/backend-tasks/src/tasks/TaskScheduler.ts b/packages/backend-tasks/src/tasks/TaskScheduler.ts index 672c52cc10..6608ba8e84 100644 --- a/packages/backend-tasks/src/tasks/TaskScheduler.ts +++ b/packages/backend-tasks/src/tasks/TaskScheduler.ts @@ -33,8 +33,13 @@ import { LoggerService } from '@backstage/backend-plugin-api'; * Deals with the scheduling of distributed tasks. * * @public + * @deprecated Please migrate to the new backend system, and depend on `coreServices.scheduler` from `@backstage/backend-plugin-api` instead. */ export class TaskScheduler { + /** + * @deprecated + * It is only used by the legacy backend system, and should not be used in the new backend system. + */ static fromConfig( config: Config, options?: { diff --git a/packages/backend-tasks/src/tasks/TaskWorker.test.ts b/packages/backend-tasks/src/tasks/TaskWorker.test.ts index a2ecdaa779..10ebfde5b4 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.test.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.test.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { TestDatabases } from '@backstage/backend-test-utils'; +import { TestDatabases, mockServices } from '@backstage/backend-test-utils'; import { Duration, DateTime } from 'luxon'; import waitForExpect from 'wait-for-expect'; import { migrateBackendTasks } from '../database/migrateBackendTasks'; @@ -27,7 +26,7 @@ import { createTestScopedSignal } from './__testUtils__/createTestScopedSignal'; jest.setTimeout(60_000); describe('TaskWorker', () => { - const logger = getVoidLogger(); + const logger = mockServices.logger.mock(); const databases = TestDatabases.create(); const testScopedSignal = createTestScopedSignal(); diff --git a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts index 8053936263..5a173f246b 100644 --- a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts +++ b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts @@ -51,6 +51,7 @@ function readCronOrDuration( * * @param config - config for a TaskScheduleDefinition. * @public + * @deprecated Please import `readSchedulerServiceTaskScheduleDefinitionFromConfig` from `@backstage/backend-plugin-api` instead */ export function readTaskScheduleDefinitionFromConfig( config: Config, diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index 14339dda0e..e6a31873be 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -26,6 +26,7 @@ import { z } from 'zod'; * processing should abort and return as quickly as possible. * * @public + * @deprecated Please import `SchedulerServiceTaskFunction` from `@backstage/backend-plugin-api` instead */ export type TaskFunction = | ((abortSignal: AbortSignal) => void | Promise) @@ -35,6 +36,7 @@ export type TaskFunction = * A semi-opaque type to describe an actively scheduled task. * * @public + * @deprecated Please import `SchedulerServiceTaskDescriptor` from `@backstage/backend-plugin-api` instead */ export type TaskDescriptor = { /** @@ -57,6 +59,7 @@ export type TaskDescriptor = { * Options that control the scheduling of a task. * * @public + * @deprecated Please import `SchedulerServiceTaskScheduleDefinition` from `@backstage/backend-plugin-api` instead */ export interface TaskScheduleDefinition { /** @@ -154,6 +157,7 @@ export interface TaskScheduleDefinition { * that control the scheduling of a task. * * @public + * @deprecated Please import `SchedulerServiceTaskScheduleDefinitionConfig` from `@backstage/backend-plugin-api` instead */ export interface TaskScheduleDefinitionConfig { /** @@ -250,6 +254,7 @@ export interface TaskScheduleDefinitionConfig { * Options that apply to the invocation of a given task. * * @public + * @deprecated Please import `SchedulerServiceTaskInvocationDefinition` from `@backstage/backend-plugin-api` instead */ export interface TaskInvocationDefinition { /** @@ -273,6 +278,7 @@ export interface TaskInvocationDefinition { * A previously prepared task schedule, ready to be invoked. * * @public + * @deprecated Please import `SchedulerServiceTaskRunner` from `@backstage/backend-plugin-api` instead */ export interface TaskRunner { /** @@ -287,6 +293,7 @@ export interface TaskRunner { * Deals with the scheduling of distributed tasks, for a given plugin. * * @public + * @deprecated Please use `SchedulerService` from `@backstage/backend-plugin-api` instead (most likely via `coreServices.scheduler`) */ export interface PluginTaskScheduler { /** diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index d20d983fab..a15cb095bd 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/backend-test-utils +## 0.4.0-next.2 + +### Patch Changes + +- 0634fdc: Refactored `TestDatabases` to no longer depend on `backend-common` +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-app-api@0.7.6-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.4.0-next.1 + +### Minor Changes + +- 805cbe7: Added `TestCaches` that functions just like `TestDatabases` + +### Patch Changes + +- 9e63318: Made it possible to give access restrictions to `mockCredentials.service` +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + - @backstage/plugin-events-node@0.3.5-next.0 + +## 0.3.9-next.0 + +### Patch Changes + +- 6a576dc: Fix the logger service mock to prevent returning `undefined` from the `child` method. +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.3.8 ### Patch Changes diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 4f6eb47243..f3d790ebfe 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -11,6 +11,7 @@ import { Backend } from '@backstage/backend-app-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { BackstageNonePrincipal } from '@backstage/backend-plugin-api'; +import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; import { BackstageServicePrincipal } from '@backstage/backend-plugin-api'; import { BackstageUserInfo } from '@backstage/backend-plugin-api'; import { BackstageUserPrincipal } from '@backstage/backend-plugin-api'; @@ -25,6 +26,7 @@ import { HttpRouterFactoryOptions } from '@backstage/backend-app-api'; import { HttpRouterService } from '@backstage/backend-plugin-api'; import { IdentityService } from '@backstage/backend-plugin-api'; import { JsonObject } from '@backstage/types'; +import Keyv from 'keyv'; import { Knex } from 'knex'; import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; @@ -68,6 +70,7 @@ export namespace mockCredentials { } export function service( subject?: string, + accessRestrictions?: BackstagePrincipalAccessRestrictions, ): BackstageCredentials; export namespace service { export function header(options?: TokenOptions): string; @@ -423,6 +426,28 @@ export interface TestBackendOptions { >; } +// @public +export type TestCacheId = 'MEMORY' | 'REDIS_7' | 'MEMCACHED_1'; + +// @public +export class TestCaches { + static create(options?: { + ids?: TestCacheId[]; + disableDocker?: boolean; + }): TestCaches; + // (undocumented) + eachSupportedId(): [TestCacheId][]; + init(id: TestCacheId): Promise<{ + store: string; + connection: string; + keyv: Keyv; + }>; + // (undocumented) + static setDefaults(options: { ids?: TestCacheId[] }): void; + // (undocumented) + supports(id: TestCacheId): boolean; +} + // @public export type TestDatabaseId = | 'POSTGRES_16' diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index cb7711adfd..9dc8868f64 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-test-utils", - "version": "0.3.8", + "version": "0.4.0-next.2", "description": "Test helpers library for Backstage backends", "backstage": { "role": "node-library" @@ -46,24 +46,29 @@ }, "dependencies": { "@backstage/backend-app-api": "workspace:^", - "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-events-node": "workspace:^", "@backstage/types": "workspace:^", + "@keyv/memcache": "^1.3.5", + "@keyv/redis": "^2.5.3", + "@types/keyv": "^4.2.0", "better-sqlite3": "^9.0.0", "cookie": "^0.6.0", "express": "^4.17.1", "fs-extra": "^11.0.0", + "keyv": "^4.5.2", "knex": "^3.0.0", "msw": "^1.0.0", "mysql2": "^3.0.0", "pg": "^8.11.3", + "pg-connection-string": "^2.3.0", "testcontainers": "^10.0.0", "textextensions": "^5.16.0", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "yn": "^4.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/packages/backend-test-utils/src/cache/TestCaches.test.ts b/packages/backend-test-utils/src/cache/TestCaches.test.ts new file mode 100644 index 0000000000..7d5ef69704 --- /dev/null +++ b/packages/backend-test-utils/src/cache/TestCaches.test.ts @@ -0,0 +1,53 @@ +/* + * 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 { isDockerDisabledForTests } from '../util'; +import { TestCaches } from './TestCaches'; + +const itIfDocker = isDockerDisabledForTests() ? it.skip : it; + +jest.setTimeout(60_000); + +describe('TestCaches', () => { + const caches = TestCaches.create(); + + it.each(caches.eachSupportedId())('fires up a cache, %p', async cacheId => { + const { keyv } = await caches.init(cacheId); + await keyv.set('test', 'value'); + await expect(keyv.get('test')).resolves.toBe('value'); + }); + + itIfDocker('clears between tests, part 1', async () => { + const { keyv } = await caches.init('REDIS_7'); + // eslint-disable-next-line jest/no-standalone-expect + await expect(keyv.get('collision')).resolves.toBeUndefined(); + await keyv.set('collision', 'something'); + }); + + itIfDocker('clears between tests, part 2', async () => { + const { keyv } = await caches.init('REDIS_7'); + // eslint-disable-next-line jest/no-standalone-expect + await expect(keyv.get('collision')).resolves.toBeUndefined(); + await keyv.set('collision', 'something'); + }); + + itIfDocker('clears between tests, part 3', async () => { + const { keyv } = await caches.init('REDIS_7'); + // eslint-disable-next-line jest/no-standalone-expect + await expect(keyv.get('collision')).resolves.toBeUndefined(); + await keyv.set('collision', 'something'); + }); +}); diff --git a/packages/backend-test-utils/src/cache/TestCaches.ts b/packages/backend-test-utils/src/cache/TestCaches.ts new file mode 100644 index 0000000000..e9ba0e5c4f --- /dev/null +++ b/packages/backend-test-utils/src/cache/TestCaches.ts @@ -0,0 +1,210 @@ +/* + * 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 Keyv from 'keyv'; +import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests'; +import { connectToExternalMemcache, startMemcachedContainer } from './memcache'; +import { connectToExternalRedis, startRedisContainer } from './redis'; +import { Instance, TestCacheId, TestCacheProperties, allCaches } from './types'; + +/** + * Encapsulates the creation of ephemeral test cache instances for use inside + * unit or integration tests. + * + * @public + */ +export class TestCaches { + private readonly instanceById: Map; + private readonly supportedIds: TestCacheId[]; + private static defaultIds?: TestCacheId[]; + + /** + * Creates an empty `TestCaches` instance, and sets up Jest to clean up all of + * its acquired resources after all tests finish. + * + * You typically want to create just a single instance like this at the top of + * your test file or `describe` block, and then call `init` many times on that + * instance inside the individual tests. Spinning up a "physical" cache + * instance takes a considerable amount of time, slowing down tests. But + * wiping the contents of an instance using `init` is very fast. + */ + static create(options?: { + ids?: TestCacheId[]; + disableDocker?: boolean; + }): TestCaches { + const ids = options?.ids; + const disableDocker = options?.disableDocker ?? isDockerDisabledForTests(); + + let testCacheIds: TestCacheId[]; + if (ids) { + testCacheIds = ids; + } else if (TestCaches.defaultIds) { + testCacheIds = TestCaches.defaultIds; + } else { + testCacheIds = Object.keys(allCaches) as TestCacheId[]; + } + + const supportedIds = testCacheIds.filter(id => { + const properties = allCaches[id]; + if (!properties) { + return false; + } + // If the caller has set up the env with an explicit connection string, + // we'll assume that this target will work + if ( + properties.connectionStringEnvironmentVariableName && + process.env[properties.connectionStringEnvironmentVariableName] + ) { + return true; + } + // If the cache doesn't require docker at all, there's nothing to worry + // about + if (!properties.dockerImageName) { + return true; + } + // If the cache requires docker, but docker is disabled, we will fail. + if (disableDocker) { + return false; + } + return true; + }); + + const caches = new TestCaches(supportedIds); + + if (supportedIds.length > 0) { + afterAll(async () => { + await caches.shutdown(); + }); + } + + return caches; + } + + static setDefaults(options: { ids?: TestCacheId[] }) { + TestCaches.defaultIds = options.ids; + } + + private constructor(supportedIds: TestCacheId[]) { + this.instanceById = new Map(); + this.supportedIds = supportedIds; + } + + supports(id: TestCacheId): boolean { + return this.supportedIds.includes(id); + } + + eachSupportedId(): [TestCacheId][] { + return this.supportedIds.map(id => [id]); + } + + /** + * Returns a fresh, empty cache for the given driver. + * + * @param id - The ID of the cache to use, e.g. 'REDIS_7' + * @returns Cache connection properties + */ + async init( + id: TestCacheId, + ): Promise<{ store: string; connection: string; keyv: Keyv }> { + const properties = allCaches[id]; + if (!properties) { + const candidates = Object.keys(allCaches).join(', '); + throw new Error( + `Unknown test cache ${id}, possible values are ${candidates}`, + ); + } + if (!this.supportedIds.includes(id)) { + const candidates = this.supportedIds.join(', '); + throw new Error( + `Unsupported test cache ${id} for this environment, possible values are ${candidates}`, + ); + } + + // Ensure that a testcontainers instance is up for this ID + let instance: Instance | undefined = this.instanceById.get(id); + if (!instance) { + instance = await this.initAny(properties); + this.instanceById.set(id, instance); + } + + // Ensure that it's cleared of data from previous tests + await instance.keyv.clear(); + + return { + store: instance.store, + connection: instance.connection, + keyv: instance.keyv, + }; + } + + private async initAny(properties: TestCacheProperties): Promise { + switch (properties.store) { + case 'memcache': + return this.initMemcached(properties); + case 'redis': + return this.initRedis(properties); + case 'memory': + return { + store: 'memory', + connection: 'memory', + keyv: new Keyv(), + stop: async () => {}, + }; + default: + throw new Error(`Unknown cache store '${properties.store}'`); + } + } + + private async initMemcached( + properties: TestCacheProperties, + ): Promise { + // Use the connection string if provided + const envVarName = properties.connectionStringEnvironmentVariableName; + if (envVarName) { + const connectionString = process.env[envVarName]; + if (connectionString) { + return connectToExternalMemcache(connectionString); + } + } + + return await startMemcachedContainer(properties.dockerImageName!); + } + + private async initRedis(properties: TestCacheProperties): Promise { + // Use the connection string if provided + const envVarName = properties.connectionStringEnvironmentVariableName; + if (envVarName) { + const connectionString = process.env[envVarName]; + if (connectionString) { + return connectToExternalRedis(connectionString); + } + } + + return await startRedisContainer(properties.dockerImageName!); + } + + private async shutdown() { + const instances = [...this.instanceById.values()]; + this.instanceById.clear(); + await Promise.all( + instances.map(({ stop }) => + stop().catch(error => { + console.warn(`TestCaches: Failed to stop container`, { error }); + }), + ), + ); + } +} diff --git a/plugins/techdocs/src/event-source-polyfill.d.ts b/packages/backend-test-utils/src/cache/index.ts similarity index 71% rename from plugins/techdocs/src/event-source-polyfill.d.ts rename to packages/backend-test-utils/src/cache/index.ts index 8e298bde38..46d95545bf 100644 --- a/plugins/techdocs/src/event-source-polyfill.d.ts +++ b/packages/backend-test-utils/src/cache/index.ts @@ -14,14 +14,5 @@ * limitations under the License. */ -declare module 'event-source-polyfill' { - export class EventSourcePolyfill extends EventSource { - constructor( - url: string, - options?: { - withCredentials?: boolean; - headers?: HeadersInit; - }, - ); - } -} +export { TestCaches } from './TestCaches'; +export type { TestCacheId } from './types'; diff --git a/packages/backend-test-utils/src/database/startMysqlContainer.test.ts b/packages/backend-test-utils/src/cache/memcache.test.ts similarity index 58% rename from packages/backend-test-utils/src/database/startMysqlContainer.test.ts rename to packages/backend-test-utils/src/cache/memcache.test.ts index 23cb128a35..bd912ce9d2 100644 --- a/packages/backend-test-utils/src/database/startMysqlContainer.test.ts +++ b/packages/backend-test-utils/src/cache/memcache.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * Copyright 2024 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,25 +14,21 @@ * limitations under the License. */ -import createConnection from 'knex'; import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests'; -import { startMysqlContainer } from './startMysqlContainer'; +import { startMemcachedContainer } from './memcache'; +import { v4 as uuid } from 'uuid'; const itIfDocker = isDockerDisabledForTests() ? it.skip : it; jest.setTimeout(60_000); -describe('startMysqlContainer', () => { +describe('startMemcachedContainer', () => { itIfDocker('successfully launches the container', async () => { - const { stop, ...connection } = await startMysqlContainer('mysql:8'); - const db = createConnection({ client: 'mysql2', connection }); - try { - const result = await db.select(db.raw('version() AS version')); - // eslint-disable-next-line jest/no-standalone-expect - expect(result[0]?.version).toContain('8.'); - } finally { - await db.destroy(); - await stop(); - } + const { stop, keyv } = await startMemcachedContainer('memcached:1'); + const value = uuid(); + await keyv.set('test', value); + // eslint-disable-next-line jest/no-standalone-expect + await expect(keyv.get('test')).resolves.toBe(value); + await stop(); }); }); diff --git a/packages/backend-test-utils/src/cache/memcache.ts b/packages/backend-test-utils/src/cache/memcache.ts new file mode 100644 index 0000000000..b7ac07cb13 --- /dev/null +++ b/packages/backend-test-utils/src/cache/memcache.ts @@ -0,0 +1,83 @@ +/* + * 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 Keyv from 'keyv'; +import KeyvMemcache from '@keyv/memcache'; +import { v4 as uuid } from 'uuid'; +import { Instance } from './types'; + +async function attemptMemcachedConnection(connection: string): Promise { + const startTime = Date.now(); + + for (;;) { + try { + const store = new KeyvMemcache(connection); + const keyv = new Keyv({ store }); + const value = uuid(); + await keyv.set('test', value); + if ((await keyv.get('test')) === value) { + return keyv; + } + } catch (e) { + if (Date.now() - startTime > 30_000) { + throw new Error( + `Timed out waiting for memcached to be ready for connections, ${e}`, + ); + } + } + + await new Promise(resolve => setTimeout(resolve, 100)); + } +} + +export async function connectToExternalMemcache( + connection: string, +): Promise { + const keyv = await attemptMemcachedConnection(connection); + return { + store: 'memcache', + connection, + keyv, + stop: async () => await keyv.disconnect(), + }; +} + +export async function startMemcachedContainer( + image: string, +): Promise { + // Lazy-load to avoid side-effect of importing testcontainers + const { GenericContainer } = await import('testcontainers'); + + const container = await new GenericContainer(image) + .withExposedPorts(11211) + .start(); + + const host = container.getHost(); + const port = container.getMappedPort(11211); + const connection = `${host}:${port}`; + + const keyv = await attemptMemcachedConnection(connection); + + return { + store: 'memcache', + connection, + keyv, + stop: async () => { + await keyv.disconnect(); + await container.stop({ timeout: 10_000 }); + }, + }; +} diff --git a/packages/backend-test-utils/src/database/startPostgresContainer.test.ts b/packages/backend-test-utils/src/cache/redis.test.ts similarity index 57% rename from packages/backend-test-utils/src/database/startPostgresContainer.test.ts rename to packages/backend-test-utils/src/cache/redis.test.ts index f64a605fd4..6555a26677 100644 --- a/packages/backend-test-utils/src/database/startPostgresContainer.test.ts +++ b/packages/backend-test-utils/src/cache/redis.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * Copyright 2024 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,25 +14,21 @@ * limitations under the License. */ -import createConnection from 'knex'; import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests'; -import { startPostgresContainer } from './startPostgresContainer'; +import { startRedisContainer } from './redis'; +import { v4 as uuid } from 'uuid'; const itIfDocker = isDockerDisabledForTests() ? it.skip : it; jest.setTimeout(60_000); -describe('startPostgresContainer', () => { +describe('startRedisContainer', () => { itIfDocker('successfully launches the container', async () => { - const { stop, ...connection } = await startPostgresContainer('postgres:13'); - const db = createConnection({ client: 'pg', connection }); - try { - const result = await db.select(db.raw('version()')); - // eslint-disable-next-line jest/no-standalone-expect - expect(result[0]?.version).toContain('PostgreSQL'); - } finally { - await db.destroy(); - await stop(); - } + const { stop, keyv } = await startRedisContainer('redis:7'); + const value = uuid(); + await keyv.set('test', value); + // eslint-disable-next-line jest/no-standalone-expect + await expect(keyv.get('test')).resolves.toBe(value); + await stop(); }); }); diff --git a/packages/backend-test-utils/src/cache/redis.ts b/packages/backend-test-utils/src/cache/redis.ts new file mode 100644 index 0000000000..6185e4d076 --- /dev/null +++ b/packages/backend-test-utils/src/cache/redis.ts @@ -0,0 +1,81 @@ +/* + * 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 Keyv from 'keyv'; +import KeyvRedis from '@keyv/redis'; +import { v4 as uuid } from 'uuid'; +import { Instance } from './types'; + +async function attemptRedisConnection(connection: string): Promise { + const startTime = Date.now(); + + for (;;) { + try { + const store = new KeyvRedis(connection); + const keyv = new Keyv({ store }); + const value = uuid(); + await keyv.set('test', value); + if ((await keyv.get('test')) === value) { + return keyv; + } + } catch (e) { + if (Date.now() - startTime > 30_000) { + throw new Error( + `Timed out waiting for redis to be ready for connections, ${e}`, + ); + } + } + + await new Promise(resolve => setTimeout(resolve, 100)); + } +} + +export async function connectToExternalRedis( + connection: string, +): Promise { + const keyv = await attemptRedisConnection(connection); + return { + store: 'redis', + connection, + keyv, + stop: async () => await keyv.disconnect(), + }; +} + +export async function startRedisContainer(image: string): Promise { + // Lazy-load to avoid side-effect of importing testcontainers + const { GenericContainer } = await import('testcontainers'); + + const container = await new GenericContainer(image) + .withExposedPorts(6379) + .start(); + + const host = container.getHost(); + const port = container.getMappedPort(6379); + const connection = `redis://${host}:${port}`; + + const keyv = await attemptRedisConnection(connection); + + return { + store: 'redis', + connection, + keyv, + stop: async () => { + await keyv.disconnect(); + await container.stop({ timeout: 10_000 }); + }, + }; +} diff --git a/packages/backend-test-utils/src/cache/types.ts b/packages/backend-test-utils/src/cache/types.ts new file mode 100644 index 0000000000..1aea522042 --- /dev/null +++ b/packages/backend-test-utils/src/cache/types.ts @@ -0,0 +1,61 @@ +/* + * 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 Keyv from 'keyv'; +import { getDockerImageForName } from '../util/getDockerImageForName'; + +/** + * The possible caches to test against. + * + * @public + */ +export type TestCacheId = 'MEMORY' | 'REDIS_7' | 'MEMCACHED_1'; + +export type TestCacheProperties = { + name: string; + store: string; + dockerImageName?: string; + connectionStringEnvironmentVariableName?: string; +}; + +export type Instance = { + store: string; + connection: string; + keyv: Keyv; + stop: () => Promise; +}; + +export const allCaches: Record = + Object.freeze({ + REDIS_7: { + name: 'Redis 7.x', + store: 'redis', + dockerImageName: getDockerImageForName('redis:7'), + connectionStringEnvironmentVariableName: + 'BACKSTAGE_TEST_CACHE_REDIS7_CONNECTION_STRING', + }, + MEMCACHED_1: { + name: 'Memcached 1.x', + store: 'memcache', + dockerImageName: getDockerImageForName('memcached:1'), + connectionStringEnvironmentVariableName: + 'BACKSTAGE_TEST_CACHE_MEMCACHED1_CONNECTION_STRING', + }, + MEMORY: { + name: 'In-memory', + store: 'memory', + }, + }); diff --git a/packages/backend-test-utils/src/database/TestDatabases.test.ts b/packages/backend-test-utils/src/database/TestDatabases.test.ts index 64e4d76443..fea7189aec 100644 --- a/packages/backend-test-utils/src/database/TestDatabases.test.ts +++ b/packages/backend-test-utils/src/database/TestDatabases.test.ts @@ -14,28 +14,11 @@ * limitations under the License. */ -import knexFactory from 'knex'; -import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests'; -import { startMysqlContainer } from './startMysqlContainer'; -import { startPostgresContainer } from './startPostgresContainer'; import { TestDatabases } from './TestDatabases'; -const itIfDocker = isDockerDisabledForTests() ? it.skip : it; - jest.setTimeout(60_000); describe('TestDatabases', () => { - const OLD_ENV = process.env; - - beforeEach(() => { - jest.resetModules(); - process.env = { ...OLD_ENV }; - }); - - afterAll(() => { - process.env = OLD_ENV; - }); - describe('each create', () => { const dbs = TestDatabases.create(); @@ -55,254 +38,4 @@ describe('TestDatabases', () => { }, ); }); - - describe('each connect', () => { - const dbs = TestDatabases.create(); - - itIfDocker( - 'obeys a provided connection string for postgres 16', - async () => { - const { host, port, user, password, stop } = - await startPostgresContainer('postgres:16'); - - try { - // Leave a mark - process.env.BACKSTAGE_TEST_DATABASE_POSTGRES16_CONNECTION_STRING = `postgresql://${user}:${password}@${host}:${port}`; - const input = await dbs.init('POSTGRES_16'); - await input.schema.createTable('a', table => - table.string('x').primary(), - ); - await input.insert({ x: 'y' }).into('a'); - - // Look for the mark - const database = input.client.config.connection.database; - const output = knexFactory({ - client: 'pg', - connection: { host, port, user, password, database }, - }); - // eslint-disable-next-line jest/no-standalone-expect - await expect(output.select('x').from('a')).resolves.toEqual([ - { x: 'y' }, - ]); - } finally { - await stop(); - } - }, - ); - - itIfDocker( - 'obeys a provided connection string for postgres 15', - async () => { - const { host, port, user, password, stop } = - await startPostgresContainer('postgres:15'); - - try { - // Leave a mark - process.env.BACKSTAGE_TEST_DATABASE_POSTGRES15_CONNECTION_STRING = `postgresql://${user}:${password}@${host}:${port}`; - const input = await dbs.init('POSTGRES_15'); - await input.schema.createTable('a', table => - table.string('x').primary(), - ); - await input.insert({ x: 'y' }).into('a'); - - // Look for the mark - const database = input.client.config.connection.database; - const output = knexFactory({ - client: 'pg', - connection: { host, port, user, password, database }, - }); - // eslint-disable-next-line jest/no-standalone-expect - await expect(output.select('x').from('a')).resolves.toEqual([ - { x: 'y' }, - ]); - } finally { - await stop(); - } - }, - ); - - itIfDocker( - 'obeys a provided connection string for postgres 14', - async () => { - const { host, port, user, password, stop } = - await startPostgresContainer('postgres:14'); - - try { - // Leave a mark - process.env.BACKSTAGE_TEST_DATABASE_POSTGRES14_CONNECTION_STRING = `postgresql://${user}:${password}@${host}:${port}`; - const input = await dbs.init('POSTGRES_14'); - await input.schema.createTable('a', table => - table.string('x').primary(), - ); - await input.insert({ x: 'y' }).into('a'); - - // Look for the mark - const database = input.client.config.connection.database; - const output = knexFactory({ - client: 'pg', - connection: { host, port, user, password, database }, - }); - // eslint-disable-next-line jest/no-standalone-expect - await expect(output.select('x').from('a')).resolves.toEqual([ - { x: 'y' }, - ]); - } finally { - await stop(); - } - }, - ); - - itIfDocker( - 'obeys a provided connection string for postgres 13', - async () => { - const { host, port, user, password, stop } = - await startPostgresContainer('postgres:13'); - - try { - // Leave a mark - process.env.BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING = `postgresql://${user}:${password}@${host}:${port}`; - const input = await dbs.init('POSTGRES_13'); - await input.schema.createTable('a', table => - table.string('x').primary(), - ); - await input.insert({ x: 'y' }).into('a'); - - // Look for the mark - const database = input.client.config.connection.database; - const output = knexFactory({ - client: 'pg', - connection: { host, port, user, password, database }, - }); - // eslint-disable-next-line jest/no-standalone-expect - await expect(output.select('x').from('a')).resolves.toEqual([ - { x: 'y' }, - ]); - } finally { - await stop(); - } - }, - ); - - itIfDocker( - 'obeys a provided connection string for postgres 12', - async () => { - const { host, port, user, password, stop } = - await startPostgresContainer('postgres:12'); - - try { - // Leave a mark - process.env.BACKSTAGE_TEST_DATABASE_POSTGRES12_CONNECTION_STRING = `postgresql://${user}:${password}@${host}:${port}`; - const input = await dbs.init('POSTGRES_12'); - await input.schema.createTable('a', table => - table.string('x').primary(), - ); - await input.insert({ x: 'y' }).into('a'); - - // Look for the mark - const database = input.client.config.connection.database; - const output = knexFactory({ - client: 'pg', - connection: { host, port, user, password, database }, - }); - // eslint-disable-next-line jest/no-standalone-expect - await expect(output.select('x').from('a')).resolves.toEqual([ - { x: 'y' }, - ]); - } finally { - await stop(); - } - }, - ); - - itIfDocker( - 'obeys a provided connection string for postgres 11', - async () => { - const { host, port, user, password, stop } = - await startPostgresContainer('postgres:11'); - - try { - // Leave a mark - process.env.BACKSTAGE_TEST_DATABASE_POSTGRES11_CONNECTION_STRING = `postgresql://${user}:${password}@${host}:${port}`; - const input = await dbs.init('POSTGRES_11'); - await input.schema.createTable('a', table => - table.string('x').primary(), - ); - await input.insert({ x: 'y' }).into('a'); - - // Look for the mark - const database = input.client.config.connection.database; - const output = knexFactory({ - client: 'pg', - connection: { host, port, user, password, database }, - }); - // eslint-disable-next-line jest/no-standalone-expect - await expect(output.select('x').from('a')).resolves.toEqual([ - { x: 'y' }, - ]); - } finally { - await stop(); - } - }, - ); - - itIfDocker( - 'obeys a provided connection string for postgres 9', - async () => { - const { host, port, user, password, stop } = - await startPostgresContainer('postgres:9'); - - try { - // Leave a mark - process.env.BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING = `postgresql://${user}:${password}@${host}:${port}`; - const input = await dbs.init('POSTGRES_9'); - await input.schema.createTable('a', table => - table.string('x').primary(), - ); - await input.insert({ x: 'y' }).into('a'); - - // Look for the mark - const database = input.client.config.connection.database; - const output = knexFactory({ - client: 'pg', - connection: { host, port, user, password, database }, - }); - // eslint-disable-next-line jest/no-standalone-expect - await expect(output.select('x').from('a')).resolves.toEqual([ - { x: 'y' }, - ]); - } finally { - await stop(); - } - }, - ); - - itIfDocker('obeys a provided connection string for mysql 8', async () => { - const { host, port, user, password, stop } = await startMysqlContainer( - 'mysql:8', - ); - - try { - // Leave a mark - process.env.BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING = `mysql://${user}:${password}@${host}:${port}/ignored`; - const input = await dbs.init('MYSQL_8'); - await input.schema.createTable('a', table => - table.string('x').primary(), - ); - await input.insert({ x: 'y' }).into('a'); - - // Look for the mark - const database = input.client.config.connection.database; - const output = knexFactory({ - client: 'mysql2', - connection: { host, port, user, password, database }, - }); - // eslint-disable-next-line jest/no-standalone-expect - await expect(output.select('x').from('a')).resolves.toEqual([ - { x: 'y' }, - ]); - } finally { - await stop(); - } - }); - }); }); diff --git a/packages/backend-test-utils/src/database/TestDatabases.ts b/packages/backend-test-utils/src/database/TestDatabases.ts index 4ea8fe05d7..cd20e2997b 100644 --- a/packages/backend-test-utils/src/database/TestDatabases.ts +++ b/packages/backend-test-utils/src/database/TestDatabases.ts @@ -14,27 +14,18 @@ * limitations under the License. */ -import { DatabaseManager, dropDatabase } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; -import { randomBytes } from 'crypto'; import { Knex } from 'knex'; import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests'; -import { startMysqlContainer } from './startMysqlContainer'; -import { startPostgresContainer } from './startPostgresContainer'; +import { MysqlEngine } from './mysql'; +import { PostgresEngine } from './postgres'; +import { SqliteEngine } from './sqlite'; import { - allDatabases, - Instance, + Engine, TestDatabaseId, TestDatabaseProperties, + allDatabases, } from './types'; -const LARGER_POOL_CONFIG = { - pool: { - min: 0, - max: 50, - }, -}; - /** * Encapsulates the creation of ephemeral test database instances for use * inside unit or integration tests. @@ -42,7 +33,17 @@ const LARGER_POOL_CONFIG = { * @public */ export class TestDatabases { - private readonly instanceById: Map; + private readonly engineFactoryByDriver: Record< + string, + (properties: TestDatabaseProperties) => Promise + > = { + pg: PostgresEngine.create, + mysql: MysqlEngine.create, + mysql2: MysqlEngine.create, + 'better-sqlite3': SqliteEngine.create, + sqlite3: SqliteEngine.create, + }; + private readonly engineByTestDatabaseId: Map; private readonly supportedIds: TestDatabaseId[]; private static defaultIds?: TestDatabaseId[]; @@ -114,7 +115,7 @@ export class TestDatabases { } private constructor(supportedIds: TestDatabaseId[]) { - this.instanceById = new Map(); + this.engineByTestDatabaseId = new Map(); this.supportedIds = supportedIds; } @@ -148,185 +149,29 @@ export class TestDatabases { ); } - let instance: Instance | undefined = this.instanceById.get(id); - - // Ensure that a testcontainers instance is up for this ID - if (!instance) { - instance = await this.initAny(properties); - this.instanceById.set(id, instance); - } - - // Ensure that a unique logical database is created in the instance - const databaseName = `db${randomBytes(16).toString('hex')}`; - const connection = await instance.databaseManager - .forPlugin(databaseName) - .getClient(); - - instance.connections.push(connection); - instance.databaseNames.push(databaseName); - - return connection; - } - - private async initAny(properties: TestDatabaseProperties): Promise { - // Use the connection string if provided - if (properties.driver === 'pg' || properties.driver === 'mysql2') { - const envVarName = properties.connectionStringEnvironmentVariableName; - if (envVarName) { - const connectionString = process.env[envVarName]; - if (connectionString) { - const config = new ConfigReader({ - backend: { - database: { - knexConfig: properties.driver.includes('sqlite') - ? {} - : LARGER_POOL_CONFIG, - client: properties.driver, - connection: connectionString, - }, - }, - }); - const databaseManager = DatabaseManager.fromConfig(config); - const databaseNames: Array = []; - return { - dropDatabases: async () => { - await dropDatabase( - config.getConfig('backend.database'), - ...databaseNames.map( - databaseName => `backstage_plugin_${databaseName}`, - ), - ); - }, - databaseManager, - databaseNames, - connections: [], - }; - } - } - } - - // Otherwise start a container for the purpose - switch (properties.driver) { - case 'pg': - return this.initPostgres(properties); - case 'mysql2': - return this.initMysql(properties); - case 'better-sqlite3': - case 'sqlite3': - return this.initSqlite(properties); - default: + let engine = this.engineByTestDatabaseId.get(id); + if (!engine) { + const factory = this.engineFactoryByDriver[properties.driver]; + if (!factory) { throw new Error(`Unknown database driver ${properties.driver}`); + } + engine = await factory(properties); + this.engineByTestDatabaseId.set(id, engine); } - } - private async initPostgres( - properties: TestDatabaseProperties, - ): Promise { - const { host, port, user, password, stop } = await startPostgresContainer( - properties.dockerImageName!, - ); - - const databaseManager = DatabaseManager.fromConfig( - new ConfigReader({ - backend: { - database: { - knexConfig: LARGER_POOL_CONFIG, - client: 'pg', - connection: { host, port, user, password }, - }, - }, - }), - ); - return { - stopContainer: stop, - databaseManager, - databaseNames: [], - connections: [], - }; - } - - private async initMysql( - properties: TestDatabaseProperties, - ): Promise { - const { host, port, user, password, stop } = await startMysqlContainer( - properties.dockerImageName!, - ); - - const databaseManager = DatabaseManager.fromConfig( - new ConfigReader({ - backend: { - database: { - knexConfig: LARGER_POOL_CONFIG, - client: 'mysql2', - connection: { host, port, user, password }, - }, - }, - }), - ); - - return { - stopContainer: stop, - databaseManager, - databaseNames: [], - connections: [], - }; - } - - private async initSqlite( - properties: TestDatabaseProperties, - ): Promise { - const databaseManager = DatabaseManager.fromConfig( - new ConfigReader({ - backend: { - database: { - client: properties.driver, - connection: ':memory:', - }, - }, - }), - ); - return { - databaseManager, - databaseNames: [], - connections: [], - }; + return await engine.createDatabaseInstance(); } private async shutdown() { - const instances = [...this.instanceById.values()]; - this.instanceById.clear(); + const engines = [...this.engineByTestDatabaseId.values()]; + this.engineByTestDatabaseId.clear(); - for (const { - stopContainer, - dropDatabases, - connections, - databaseManager, - } of instances) { - for (const connection of connections) { - try { - await connection.destroy(); - } catch (error) { - console.warn(`TestDatabases: Failed to destroy connection`, { - connection, - error, - }); - } - } - - // If the database is not running in docker then drop the databases + for (const engine of engines) { try { - await dropDatabases?.(); + await engine.shutdown(); } catch (error) { - console.warn(`TestDatabases: Failed to drop databases`, { - error, - }); - } - - try { - await stopContainer?.(); - } catch (error) { - console.warn(`TestDatabases: Failed to stop container`, { - databaseManager, + console.warn(`TestDatabases: Failed to shutdown engine`, { + engine, error, }); } diff --git a/packages/backend-test-utils/src/database/index.ts b/packages/backend-test-utils/src/database/index.ts index 69e3f41452..949553cef2 100644 --- a/packages/backend-test-utils/src/database/index.ts +++ b/packages/backend-test-utils/src/database/index.ts @@ -14,6 +14,5 @@ * limitations under the License. */ -export { isDockerDisabledForTests } from '../util/isDockerDisabledForTests'; export { TestDatabases } from './TestDatabases'; export type { TestDatabaseId } from './types'; diff --git a/packages/backend-test-utils/src/database/mysql.test.ts b/packages/backend-test-utils/src/database/mysql.test.ts new file mode 100644 index 0000000000..a5b6b854f3 --- /dev/null +++ b/packages/backend-test-utils/src/database/mysql.test.ts @@ -0,0 +1,113 @@ +/* + * 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 knexFactory, { Knex } from 'knex'; +import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests'; +import { MysqlEngine, startMysqlContainer } from './mysql'; +import { Engine, TestDatabaseId, allDatabases } from './types'; + +const itIfDocker = isDockerDisabledForTests() ? it.skip : it; +const ourDatabaseIds = Object.entries(allDatabases) + .filter(([, properties]) => properties.driver.includes('mysql')) + .map(([id]) => id as TestDatabaseId); + +jest.setTimeout(60_000); + +describe('startMysqlContainer', () => { + itIfDocker( + 'successfully launches the container and can stop it without problems', + async () => { + const { connection, stopContainer } = await startMysqlContainer( + 'mysql:8', + ); + const db = knexFactory({ client: 'mysql2', connection }); + try { + const result = await db.select(db.raw('version() AS version')); + // eslint-disable-next-line jest/no-standalone-expect + expect(result[0]?.version).toContain('8.'); + } finally { + await db.destroy(); + await stopContainer(); + } + }, + ); +}); + +describe('MysqlEngine', () => { + const OLD_ENV = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...OLD_ENV }; + }); + + afterAll(() => { + process.env = OLD_ENV; + }); + + itIfDocker.each(ourDatabaseIds)( + 'uses given connection string, %p', + async testDatabaseId => { + const properties = allDatabases[testDatabaseId]; + const { connection } = await startMysqlContainer( + properties.dockerImageName!, + ); + + const outerKnex = knexFactory({ client: properties.driver, connection }); + const databases = await outerKnex + .raw('SHOW DATABASES') + .then(rows => rows[0].length); // account for meta databases, if any + + let knex: Knex | undefined; + let engine: Engine | undefined; + try { + process.env[ + properties.connectionStringEnvironmentVariableName! + ] = `mysql://${connection.user}:${connection.password}@${connection.host}:${connection.port}/ignored`; + engine = await MysqlEngine.create(properties); + knex = await engine.createDatabaseInstance(); + + // eslint-disable-next-line jest/no-standalone-expect + await expect( + outerKnex.raw('SHOW DATABASES').then(rows => rows[0].length), + ).resolves.toBe(databases + 1); + } finally { + await outerKnex.destroy(); + await knex?.destroy(); + await engine?.shutdown(); + } + }, + ); + + itIfDocker.each(ourDatabaseIds)( + 'creates docker containers, %p', + async testDatabaseId => { + const properties = allDatabases[testDatabaseId]; + delete process.env[properties.connectionStringEnvironmentVariableName!]; + const engine = await MysqlEngine.create(properties); + + try { + const knex = await engine.createDatabaseInstance(); + // eslint-disable-next-line jest/no-standalone-expect + await expect( + knex.select(knex.raw('version() as version')), + ).resolves.toEqual([{ version: expect.any(String) }]); + } finally { + await engine.shutdown(); + } + }, + ); +}); diff --git a/packages/backend-test-utils/src/database/mysql.ts b/packages/backend-test-utils/src/database/mysql.ts new file mode 100644 index 0000000000..ab130dc5c8 --- /dev/null +++ b/packages/backend-test-utils/src/database/mysql.ts @@ -0,0 +1,242 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { stringifyError } from '@backstage/errors'; +import { randomBytes } from 'crypto'; +import knexFactory, { Knex } from 'knex'; +import { v4 as uuid } from 'uuid'; +import yn from 'yn'; +import { Engine, LARGER_POOL_CONFIG, TestDatabaseProperties } from './types'; + +async function waitForMysqlReady( + connection: Knex.MySqlConnectionConfig, +): Promise { + const startTime = Date.now(); + + let lastError: Error | undefined; + let attempts = 0; + for (;;) { + attempts += 1; + + let knex: Knex | undefined; + try { + knex = knexFactory({ + client: 'mysql2', + connection: { + // make a copy because the driver mutates this + ...connection, + }, + }); + const result = await knex.select(knex.raw('version() AS version')); + if (Array.isArray(result) && result[0]?.version) { + return; + } + } catch (e) { + lastError = e; + } finally { + await knex?.destroy(); + } + + if (Date.now() - startTime > 30_000) { + throw new Error( + `Timed out waiting for the database to be ready for connections, ${attempts} attempts, ${ + lastError + ? `last error was ${stringifyError(lastError)}` + : '(no errors thrown)' + }`, + ); + } + + await new Promise(resolve => setTimeout(resolve, 100)); + } +} + +export async function startMysqlContainer(image: string): Promise<{ + connection: Knex.MySqlConnectionConfig; + stopContainer: () => Promise; +}> { + const user = 'root'; + const password = uuid(); + + // Lazy-load to avoid side-effect of importing testcontainers + const { GenericContainer } = await import('testcontainers'); + + const container = await new GenericContainer(image) + .withExposedPorts(3306) + .withEnvironment({ MYSQL_ROOT_PASSWORD: password }) + .withTmpFs({ '/var/lib/mysql': 'rw' }) + .start(); + + const host = container.getHost(); + const port = container.getMappedPort(3306); + const connection = { host, port, user, password }; + const stopContainer = async () => { + await container.stop({ timeout: 10_000 }); + }; + + await waitForMysqlReady(connection); + + return { connection, stopContainer }; +} + +export function parseMysqlConnectionString( + connectionString: string, +): Knex.MySqlConnectionConfig { + try { + const { + protocol, + username, + password, + port, + hostname, + pathname, + searchParams, + } = new URL(connectionString); + + if (protocol !== 'mysql:') { + throw new Error(`Unknown protocol ${protocol}`); + } else if (!username || !password) { + throw new Error(`Missing username/password`); + } else if (!pathname.match(/^\/[^/]+$/)) { + throw new Error(`Expected single path segment`); + } + + const result: Knex.MySqlConnectionConfig = { + user: username, + password, + host: hostname, + port: Number(port || 3306), + database: decodeURIComponent(pathname.substring(1)), + }; + + const ssl = searchParams.get('ssl'); + if (ssl) { + result.ssl = ssl; + } + + const debug = searchParams.get('debug'); + if (debug) { + result.debug = yn(debug); + } + + return result; + } catch (e) { + throw new Error(`Error while parsing MySQL connection string, ${e}`, e); + } +} + +export class MysqlEngine implements Engine { + static async create( + properties: TestDatabaseProperties, + ): Promise { + const { connectionStringEnvironmentVariableName, dockerImageName } = + properties; + + if (connectionStringEnvironmentVariableName) { + const connectionString = + process.env[connectionStringEnvironmentVariableName]; + if (connectionString) { + const connection = parseMysqlConnectionString(connectionString); + return new MysqlEngine( + properties, + connection as Knex.MySqlConnectionConfig, + ); + } + } + + if (dockerImageName) { + const { connection, stopContainer } = await startMysqlContainer( + dockerImageName, + ); + return new MysqlEngine(properties, connection, stopContainer); + } + + throw new Error(`Test databasee for ${properties.name} not configured`); + } + + readonly #properties: TestDatabaseProperties; + readonly #connection: Knex.MySqlConnectionConfig; + readonly #knexInstances: Knex[]; + readonly #databaseNames: string[]; + readonly #stopContainer?: () => Promise; + + constructor( + properties: TestDatabaseProperties, + connection: Knex.MySqlConnectionConfig, + stopContainer?: () => Promise, + ) { + this.#properties = properties; + this.#connection = connection; + this.#knexInstances = []; + this.#databaseNames = []; + this.#stopContainer = stopContainer; + } + + async createDatabaseInstance(): Promise { + const adminConnection = this.#connectAdmin(); + try { + const databaseName = `db${randomBytes(16).toString('hex')}`; + + await adminConnection.raw('CREATE DATABASE ??', [databaseName]); + this.#databaseNames.push(databaseName); + + const knexInstance = knexFactory({ + client: this.#properties.driver, + connection: { + ...this.#connection, + database: databaseName, + }, + ...LARGER_POOL_CONFIG, + }); + this.#knexInstances.push(knexInstance); + + return knexInstance; + } finally { + await adminConnection.destroy(); + } + } + + async shutdown(): Promise { + for (const instance of this.#knexInstances) { + await instance.destroy(); + } + + const adminConnection = this.#connectAdmin(); + try { + for (const databaseName of this.#databaseNames) { + await adminConnection.raw('DROP DATABASE ??', [databaseName]); + } + } finally { + await adminConnection.destroy(); + } + + await this.#stopContainer?.(); + } + + #connectAdmin(): Knex { + const connection = { + ...this.#connection, + database: null as unknown as string, + }; + return knexFactory({ + client: this.#properties.driver, + connection, + pool: { + acquireTimeoutMillis: 10000, + }, + }); + } +} diff --git a/packages/backend-test-utils/src/database/postgres.test.ts b/packages/backend-test-utils/src/database/postgres.test.ts new file mode 100644 index 0000000000..3d0d7b041b --- /dev/null +++ b/packages/backend-test-utils/src/database/postgres.test.ts @@ -0,0 +1,113 @@ +/* + * 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 knexFactory, { Knex } from 'knex'; +import { isDockerDisabledForTests } from '../util/isDockerDisabledForTests'; +import { startPostgresContainer, PostgresEngine } from './postgres'; +import { Engine, TestDatabaseId, allDatabases } from './types'; + +const itIfDocker = isDockerDisabledForTests() ? it.skip : it; +const ourDatabaseIds = Object.entries(allDatabases) + .filter(([, properties]) => properties.driver === 'pg') + .map(([id]) => id as TestDatabaseId); + +jest.setTimeout(60_000); + +describe('startPostgresContainer', () => { + itIfDocker( + 'successfully launches the container and can stop it without problems', + async () => { + const { connection, stopContainer } = await startPostgresContainer( + 'postgres:13', + ); + const db = knexFactory({ client: 'pg', connection }); + try { + const result = await db.select(db.raw('version()')); + // eslint-disable-next-line jest/no-standalone-expect + expect(result[0]?.version).toContain('PostgreSQL'); + } finally { + await db.destroy(); + await stopContainer(); + } + }, + ); +}); + +describe('PostgresEngine', () => { + const OLD_ENV = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...OLD_ENV }; + }); + + afterAll(() => { + process.env = OLD_ENV; + }); + + itIfDocker.each(ourDatabaseIds)( + 'uses given connection string, %p', + async testDatabaseId => { + const properties = allDatabases[testDatabaseId]; + const { connection } = await startPostgresContainer( + properties.dockerImageName!, + ); + + const outerKnex = knexFactory({ client: properties.driver, connection }); + const databases = await outerKnex + .from('pg_database') + .then(rows => rows.length); // account for postgres, template0 etc + + let knex: Knex | undefined; + let engine: Engine | undefined; + try { + process.env[ + properties.connectionStringEnvironmentVariableName! + ] = `postgres://${connection.user}:${connection.password}@${connection.host}:${connection.port}`; + engine = await PostgresEngine.create(properties); + knex = await engine.createDatabaseInstance(); + + // eslint-disable-next-line jest/no-standalone-expect + await expect(outerKnex.from('pg_database')).resolves.toHaveLength( + databases + 1, + ); + } finally { + await outerKnex.destroy(); + await knex?.destroy(); + await engine?.shutdown(); + } + }, + ); + + itIfDocker.each(ourDatabaseIds)( + 'creates docker containers, %p', + async testDatabaseId => { + const properties = allDatabases[testDatabaseId]; + delete process.env[properties.connectionStringEnvironmentVariableName!]; + const engine = await PostgresEngine.create(properties); + + try { + const knex = await engine.createDatabaseInstance(); + // eslint-disable-next-line jest/no-standalone-expect + await expect(knex.select(knex.raw('version()'))).resolves.toEqual([ + { version: expect.stringContaining('PostgreSQL') }, + ]); + } finally { + await engine.shutdown(); + } + }, + ); +}); diff --git a/packages/backend-test-utils/src/database/postgres.ts b/packages/backend-test-utils/src/database/postgres.ts new file mode 100644 index 0000000000..c8a946d121 --- /dev/null +++ b/packages/backend-test-utils/src/database/postgres.ts @@ -0,0 +1,195 @@ +/* + * 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 { stringifyError } from '@backstage/errors'; +import { randomBytes } from 'crypto'; +import knexFactory, { Knex } from 'knex'; +import { parse as parsePgConnectionString } from 'pg-connection-string'; +import { v4 as uuid } from 'uuid'; +import { Engine, LARGER_POOL_CONFIG, TestDatabaseProperties } from './types'; + +async function waitForPostgresReady( + connection: Knex.PgConnectionConfig, +): Promise { + const startTime = Date.now(); + + let lastError: Error | undefined; + let attempts = 0; + for (;;) { + attempts += 1; + + let knex: Knex | undefined; + try { + knex = knexFactory({ + client: 'pg', + connection: { + // make a copy because the driver mutates this + ...connection, + }, + }); + const result = await knex.select(knex.raw('version()')); + if (Array.isArray(result) && result[0]?.version) { + return; + } + } catch (e) { + lastError = e; + } finally { + await knex?.destroy(); + } + + if (Date.now() - startTime > 30_000) { + throw new Error( + `Timed out waiting for the database to be ready for connections, ${attempts} attempts, ${ + lastError + ? `last error was ${stringifyError(lastError)}` + : '(no errors thrown)' + }`, + ); + } + + await new Promise(resolve => setTimeout(resolve, 100)); + } +} + +export async function startPostgresContainer(image: string): Promise<{ + connection: Knex.PgConnectionConfig; + stopContainer: () => Promise; +}> { + const user = 'postgres'; + const password = uuid(); + + // Lazy-load to avoid side-effect of importing testcontainers + const { GenericContainer } = await import('testcontainers'); + + const container = await new GenericContainer(image) + .withExposedPorts(5432) + .withEnvironment({ POSTGRES_PASSWORD: password }) + .withTmpFs({ '/var/lib/postgresql/data': 'rw' }) + .start(); + + const host = container.getHost(); + const port = container.getMappedPort(5432); + const connection = { host, port, user, password }; + const stopContainer = async () => { + await container.stop({ timeout: 10_000 }); + }; + + await waitForPostgresReady(connection); + + return { connection, stopContainer }; +} + +export class PostgresEngine implements Engine { + static async create( + properties: TestDatabaseProperties, + ): Promise { + const { connectionStringEnvironmentVariableName, dockerImageName } = + properties; + + if (connectionStringEnvironmentVariableName) { + const connectionString = + process.env[connectionStringEnvironmentVariableName]; + if (connectionString) { + const connection = parsePgConnectionString(connectionString); + return new PostgresEngine( + properties, + connection as Knex.PgConnectionConfig, + ); + } + } + + if (dockerImageName) { + const { connection, stopContainer } = await startPostgresContainer( + dockerImageName, + ); + return new PostgresEngine(properties, connection, stopContainer); + } + + throw new Error(`Test databasee for ${properties.name} not configured`); + } + + readonly #properties: TestDatabaseProperties; + readonly #connection: Knex.PgConnectionConfig; + readonly #knexInstances: Knex[]; + readonly #databaseNames: string[]; + readonly #stopContainer?: () => Promise; + + constructor( + properties: TestDatabaseProperties, + connection: Knex.PgConnectionConfig, + stopContainer?: () => Promise, + ) { + this.#properties = properties; + this.#connection = connection; + this.#knexInstances = []; + this.#databaseNames = []; + this.#stopContainer = stopContainer; + } + + async createDatabaseInstance(): Promise { + const adminConnection = this.#connectAdmin(); + try { + const databaseName = `db${randomBytes(16).toString('hex')}`; + + await adminConnection.raw('CREATE DATABASE ??', [databaseName]); + this.#databaseNames.push(databaseName); + + const knexInstance = knexFactory({ + client: this.#properties.driver, + connection: { + ...this.#connection, + database: databaseName, + }, + ...LARGER_POOL_CONFIG, + }); + this.#knexInstances.push(knexInstance); + + return knexInstance; + } finally { + await adminConnection.destroy(); + } + } + + async shutdown(): Promise { + for (const instance of this.#knexInstances) { + await instance.destroy(); + } + + const adminConnection = this.#connectAdmin(); + try { + for (const databaseName of this.#databaseNames) { + await adminConnection.raw('DROP DATABASE ??', [databaseName]); + } + } finally { + await adminConnection.destroy(); + } + + await this.#stopContainer?.(); + } + + #connectAdmin(): Knex { + return knexFactory({ + client: this.#properties.driver, + connection: { + ...this.#connection, + database: 'postgres', + }, + pool: { + acquireTimeoutMillis: 10000, + }, + }); + } +} diff --git a/packages/backend-test-utils/src/database/sqlite.test.ts b/packages/backend-test-utils/src/database/sqlite.test.ts new file mode 100644 index 0000000000..8eca09a698 --- /dev/null +++ b/packages/backend-test-utils/src/database/sqlite.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SqliteEngine } from './sqlite'; +import { TestDatabaseId, allDatabases } from './types'; + +const ourDatabaseIds = Object.entries(allDatabases) + .filter(([, properties]) => properties.driver.includes('sqlite')) + .map(([id]) => id as TestDatabaseId); + +describe('SqliteEngine', () => { + it.each(ourDatabaseIds)( + 'should create a database instance, %p', + async testDatabaseId => { + for (let i = 0; i < 100; ++i) { + const properties = allDatabases[testDatabaseId]; + const engine = await SqliteEngine.create(properties); + + const instance1 = await engine.createDatabaseInstance(); + const instance2 = await engine.createDatabaseInstance(); + expect(instance1).toBeDefined(); + expect(instance2).toBeDefined(); + expect(instance1).not.toEqual(instance2); + + await instance1.schema.createTable('t', table => { + table.string('value'); + }); + await instance2.schema.createTable('t', table => { + table.string('value'); + }); + + await instance1('t').insert({ value: 'value1' }); + await instance2('t').insert({ value: 'value2' }); + + await expect(instance1('t')).resolves.toEqual([{ value: 'value1' }]); + await expect(instance2('t')).resolves.toEqual([{ value: 'value2' }]); + + await engine.shutdown(); + } + }, + ); +}); diff --git a/packages/backend-test-utils/src/database/sqlite.ts b/packages/backend-test-utils/src/database/sqlite.ts new file mode 100644 index 0000000000..92dc249225 --- /dev/null +++ b/packages/backend-test-utils/src/database/sqlite.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import knexFactory, { Knex } from 'knex'; +import { Engine, TestDatabaseProperties } from './types'; + +export class SqliteEngine implements Engine { + static async create( + properties: TestDatabaseProperties, + ): Promise { + return new SqliteEngine(properties); + } + + readonly #properties: TestDatabaseProperties; + readonly #instances: Knex[]; + + constructor(properties: TestDatabaseProperties) { + this.#properties = properties; + this.#instances = []; + } + + async createDatabaseInstance(): Promise { + const instance = knexFactory({ + client: this.#properties.driver, + connection: ':memory:', + useNullAsDefault: true, + }); + + instance.client.pool.on('createSuccess', (_eventId: any, resource: any) => { + resource.run('PRAGMA foreign_keys = ON', () => {}); + }); + + this.#instances.push(instance); + return instance; + } + + async shutdown(): Promise { + for (const instance of this.#instances) { + await instance.destroy(); + } + } +} diff --git a/packages/backend-test-utils/src/database/startMysqlContainer.ts b/packages/backend-test-utils/src/database/startMysqlContainer.ts deleted file mode 100644 index 2b4c917393..0000000000 --- a/packages/backend-test-utils/src/database/startMysqlContainer.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import createConnection, { Knex } from 'knex'; -import { v4 as uuid } from 'uuid'; - -async function waitForMysqlReady( - connection: Knex.MySqlConnectionConfig, -): Promise { - const startTime = Date.now(); - const db = createConnection({ client: 'mysql2', connection }); - - try { - for (;;) { - try { - const result = await db.select(db.raw('version() AS version')); - if (result[0]?.version) { - return; - } - } catch (e) { - if (Date.now() - startTime > 30_000) { - throw new Error( - `Timed out waiting for the database to be ready for connections, ${e}`, - ); - } - } - - await new Promise(resolve => setTimeout(resolve, 100)); - } - } finally { - db.destroy(); - } -} - -export async function startMysqlContainer(image: string) { - const user = 'root'; - const password = uuid(); - - // Lazy-load to avoid side-effect of importing testcontainers - const { GenericContainer } = await import('testcontainers'); - - const container = await new GenericContainer(image) - .withExposedPorts(3306) - .withEnvironment({ MYSQL_ROOT_PASSWORD: password }) - .withTmpFs({ '/var/lib/mysql': 'rw' }) - .start(); - - const host = container.getHost(); - const port = container.getMappedPort(3306); - const stop = async () => { - await container.stop({ timeout: 10_000 }); - }; - - await waitForMysqlReady({ host, port, user, password }); - - return { host, port, user, password, stop }; -} diff --git a/packages/backend-test-utils/src/database/startPostgresContainer.ts b/packages/backend-test-utils/src/database/startPostgresContainer.ts deleted file mode 100644 index 7a1c3f89c5..0000000000 --- a/packages/backend-test-utils/src/database/startPostgresContainer.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import createConnection, { Knex } from 'knex'; -import { v4 as uuid } from 'uuid'; - -async function waitForPostgresReady( - connection: Knex.PgConnectionConfig, -): Promise { - const startTime = Date.now(); - const db = createConnection({ client: 'pg', connection }); - - try { - for (;;) { - try { - const result = await db.select(db.raw('version()')); - if (Array.isArray(result) && result[0]?.version) { - return; - } - } catch (e) { - if (Date.now() - startTime > 30_000) { - throw new Error( - `Timed out waiting for the database to be ready for connections, ${e}`, - ); - } - } - - await new Promise(resolve => setTimeout(resolve, 100)); - } - } finally { - db.destroy(); - } -} - -export async function startPostgresContainer(image: string) { - const user = 'postgres'; - const password = uuid(); - - // Lazy-load to avoid side-effect of importing testcontainers - const { GenericContainer } = await import('testcontainers'); - - const container = await new GenericContainer(image) - .withExposedPorts(5432) - .withEnvironment({ POSTGRES_PASSWORD: password }) - .withTmpFs({ '/var/lib/postgresql/data': 'rw' }) - .start(); - - const host = container.getHost(); - const port = container.getMappedPort(5432); - const stop = async () => { - await container.stop({ timeout: 10_000 }); - }; - - await waitForPostgresReady({ host, port, user, password }); - - return { host, port, user, password, stop }; -} diff --git a/packages/backend-test-utils/src/database/types.ts b/packages/backend-test-utils/src/database/types.ts index 77a0c4e960..39a8af065c 100644 --- a/packages/backend-test-utils/src/database/types.ts +++ b/packages/backend-test-utils/src/database/types.ts @@ -14,10 +14,14 @@ * limitations under the License. */ -import { DatabaseManager } from '@backstage/backend-common'; import { Knex } from 'knex'; import { getDockerImageForName } from '../util/getDockerImageForName'; +export interface Engine { + createDatabaseInstance(): Promise; + shutdown(): Promise; +} + /** * The possible databases to test against. * @@ -41,14 +45,6 @@ export type TestDatabaseProperties = { connectionStringEnvironmentVariableName?: string; }; -export type Instance = { - stopContainer?: () => Promise; - dropDatabases?: () => Promise; - databaseManager: DatabaseManager; - connections: Array; - databaseNames: Array; -}; - export const allDatabases: Record = Object.freeze({ POSTGRES_16: { @@ -112,3 +108,10 @@ export const allDatabases: Record = driver: 'better-sqlite3', }, }); + +export const LARGER_POOL_CONFIG = { + pool: { + min: 0, + max: 50, + }, +}; diff --git a/packages/backend-test-utils/src/index.ts b/packages/backend-test-utils/src/index.ts index ff1f2ee460..72abd908c5 100644 --- a/packages/backend-test-utils/src/index.ts +++ b/packages/backend-test-utils/src/index.ts @@ -20,6 +20,7 @@ * @packageDocumentation */ +export * from './cache'; export * from './database'; export * from './msw'; export * from './filesystem'; diff --git a/packages/backend-test-utils/src/next/services/mockCredentials.test.ts b/packages/backend-test-utils/src/next/services/mockCredentials.test.ts index ed0071d4b8..343d6d6d20 100644 --- a/packages/backend-test-utils/src/next/services/mockCredentials.test.ts +++ b/packages/backend-test-utils/src/next/services/mockCredentials.test.ts @@ -134,6 +134,16 @@ describe('mockCredentials', () => { expect(mockCredentials.service.invalidHeader()).toBe( 'Bearer mock-invalid-service-token', ); + expect( + mockCredentials.service('test', { permissionNames: ['do.it'] }), + ).toEqual({ + $$type: '@backstage/BackstageCredentials', + principal: { + type: 'service', + subject: 'test', + accessRestrictions: { permissionNames: ['do.it'] }, + }, + }); }); it('should throw on invalid user entity refs', () => { diff --git a/packages/backend-test-utils/src/next/services/mockCredentials.ts b/packages/backend-test-utils/src/next/services/mockCredentials.ts index 16d2381c73..4214320142 100644 --- a/packages/backend-test-utils/src/next/services/mockCredentials.ts +++ b/packages/backend-test-utils/src/next/services/mockCredentials.ts @@ -17,6 +17,7 @@ import { BackstageCredentials, BackstageNonePrincipal, + BackstagePrincipalAccessRestrictions, BackstageServicePrincipal, BackstageUserPrincipal, } from '@backstage/backend-plugin-api'; @@ -202,14 +203,19 @@ export namespace mockCredentials { /** * Creates a mocked credentials object for a service principal. * - * The default subject is 'external:test-service'. + * The default subject is 'external:test-service', and no access restrictions. */ export function service( subject: string = DEFAULT_MOCK_SERVICE_SUBJECT, + accessRestrictions?: BackstagePrincipalAccessRestrictions, ): BackstageCredentials { return { $$type: '@backstage/BackstageCredentials', - principal: { type: 'service', subject }, + principal: { + type: 'service', + subject, + ...(accessRestrictions ? { accessRestrictions } : {}), + }, }; } diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 05b3ae927c..57621a9209 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -57,6 +57,17 @@ import { eventsServiceRef, } from '@backstage/plugin-events-node'; +/** @internal */ +function createLoggerMock() { + return { + child: jest.fn().mockImplementation(createLoggerMock), + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + }; +} + /** @internal */ function simpleFactory< TService, @@ -358,13 +369,10 @@ export namespace mockServices { export namespace logger { export const factory = loggerServiceFactory; - export const mock = simpleMock(coreServices.logger, () => ({ - child: jest.fn(), - debug: jest.fn(), - error: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - })); + + export const mock = simpleMock(coreServices.logger, () => + createLoggerMock(), + ); } export namespace permissions { diff --git a/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts b/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts index b411086728..05eeabd91a 100644 --- a/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts +++ b/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts @@ -20,7 +20,7 @@ export function isDockerDisabledForTests() { // the (relatively heavy, long running) docker based tests. If you want to // still run local tests for all databases, just pass either the CI=1 env // parameter to your test runner, or individual connection strings per - // database. + // database or cache. return ( Boolean(process.env.BACKSTAGE_TEST_DISABLE_DOCKER) || !Boolean(process.env.CI) diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index a0e705f22f..49c67189fe 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,113 @@ # example-backend +## 0.0.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-backend@1.10.6-next.2 + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-defaults@0.3.0-next.2 + - @backstage/plugin-permission-node@0.7.30-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.3.0-next.2 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.2 + - @backstage/plugin-scaffolder-backend@1.22.8-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-app-backend@0.3.68-next.2 + - @backstage/plugin-auth-backend@0.22.6-next.2 + - @backstage/plugin-auth-backend-module-github-provider@0.1.16-next.1 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.5-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-catalog-backend@1.23.0-next.2 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.2-next.1 + - @backstage/plugin-catalog-backend-module-openapi@0.1.37-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.6-next.2 + - @backstage/plugin-devtools-backend@0.3.5-next.2 + - @backstage/plugin-kubernetes-backend@0.18.0-next.2 + - @backstage/plugin-notifications-backend@0.3.0-next.2 + - @backstage/plugin-permission-backend@0.5.43-next.2 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.16-next.1 + - @backstage/plugin-proxy-backend@0.5.0-next.2 + - @backstage/plugin-search-backend@1.5.10-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.25-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.25-next.2 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/plugin-signals-backend@0.1.5-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-permission-common@0.7.13 + +## 0.0.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-permission-node@0.7.30-next.1 + - @backstage/plugin-search-backend@1.5.10-next.1 + - @backstage/backend-defaults@0.3.0-next.1 + - @backstage/plugin-kubernetes-backend@0.18.0-next.1 + - @backstage/plugin-catalog-backend@1.23.0-next.1 + - @backstage/plugin-scaffolder-backend@1.22.8-next.1 + - @backstage/plugin-notifications-backend@0.3.0-next.1 + - @backstage/plugin-app-backend@0.3.68-next.1 + - @backstage/plugin-auth-backend@0.22.6-next.1 + - @backstage/plugin-auth-backend-module-github-provider@0.1.16-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.5-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + - @backstage/plugin-catalog-backend-module-openapi@0.1.37-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.1 + - @backstage/plugin-devtools-backend@0.3.5-next.1 + - @backstage/plugin-permission-backend@0.5.43-next.1 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.16-next.0 + - @backstage/plugin-proxy-backend@0.5.0-next.1 + - @backstage/plugin-scaffolder-backend-module-github@0.2.9-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.25-next.1 + - @backstage/plugin-search-backend-module-explore@0.1.25-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + - @backstage/plugin-signals-backend@0.1.5-next.1 + - @backstage/plugin-techdocs-backend@1.10.6-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.6-next.1 + +## 0.0.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.2.9-next.0 + - @backstage/plugin-devtools-backend@0.3.5-next.0 + - @backstage/plugin-techdocs-backend@1.10.6-next.0 + - @backstage/plugin-catalog-backend@1.23.0-next.0 + - @backstage/plugin-search-backend@1.5.10-next.0 + - @backstage/plugin-proxy-backend@0.5.0-next.0 + - @backstage/plugin-auth-backend@0.22.6-next.0 + - @backstage/plugin-app-backend@0.3.68-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/plugin-signals-backend@0.1.5-next.0 + - @backstage/backend-defaults@0.2.19-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-scaffolder-backend@1.22.8-next.0 + - @backstage/plugin-kubernetes-backend@0.17.2-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.2-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.25-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.25-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.5-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.1.37-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.6-next.0 + - @backstage/plugin-notifications-backend@0.2.2-next.0 + - @backstage/plugin-permission-backend@0.5.43-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.16-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.17-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.16-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-permission-common@0.7.13 + ## 0.0.26 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index a373a142a0..ba22ddae1e 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.0.26", + "version": "0.0.27-next.2", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/cli-common/src/paths.test.ts b/packages/cli-common/src/paths.test.ts index 5c1cf8af08..5fcef9cadf 100644 --- a/packages/cli-common/src/paths.test.ts +++ b/packages/cli-common/src/paths.test.ts @@ -19,6 +19,10 @@ import { resolve as resolvePath } from 'path'; import { findPaths, findRootPath, findOwnDir, findOwnRootDir } from './paths'; describe('paths', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + it('findOwnDir and findOwnRootDir should find owns paths', () => { const dir = findOwnDir(__dirname); const root = findOwnRootDir(dir); @@ -92,4 +96,28 @@ describe('paths', () => { resolvePath(root, 'derp.txt'), ); }); + + it('findPaths should find workspace root with object', () => { + jest.spyOn(JSON, 'parse').mockReturnValue({ workspaces: { packages: [] } }); + jest.spyOn(process, 'cwd').mockReturnValue(__dirname); + + const paths = findPaths(__dirname); + + expect(paths.targetDir).toBe( + resolvePath(__dirname, '../../cli-common/src'), + ); + expect(paths.targetRoot).toBe(resolvePath(__dirname, '../../cli-common')); + }); + + it('findPaths should find workspace root with array', () => { + jest.spyOn(JSON, 'parse').mockReturnValue({ workspaces: [] }); + jest.spyOn(process, 'cwd').mockReturnValue(__dirname); + + const paths = findPaths(__dirname); + + expect(paths.targetDir).toBe( + resolvePath(__dirname, '../../cli-common/src'), + ); + expect(paths.targetRoot).toBe(resolvePath(__dirname, '../../cli-common')); + }); }); diff --git a/packages/cli-common/src/paths.ts b/packages/cli-common/src/paths.ts index bdd0245c76..762e660140 100644 --- a/packages/cli-common/src/paths.ts +++ b/packages/cli-common/src/paths.ts @@ -141,7 +141,7 @@ export function findPaths(searchDir: string): Paths { try { const content = fs.readFileSync(path, 'utf8'); const data = JSON.parse(content); - return Boolean(data.workspaces?.packages); + return Boolean(data.workspaces); } catch (error) { throw new Error( `Failed to parse package.json file while searching for root, ${error}`, diff --git a/packages/cli-node/CHANGELOG.md b/packages/cli-node/CHANGELOG.md index dd58feca17..bc75eb9612 100644 --- a/packages/cli-node/CHANGELOG.md +++ b/packages/cli-node/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/cli-node +## 0.2.6-next.1 + +### Patch Changes + +- a1ae9cc: Updated doc link. +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.2.6-next.0 + +### Patch Changes + +- 93be042: Upgraded @yarnpkg/parsers to stable 3.0 +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.2.5 ### Patch Changes diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index a786d71949..a76d35dc6c 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-node", - "version": "0.2.5", + "version": "0.2.6-next.1", "description": "Node.js library for Backstage CLIs", "backstage": { "role": "node-library" diff --git a/packages/cli-node/src/roles/types.ts b/packages/cli-node/src/roles/types.ts index a913ff29b6..d6eff04541 100644 --- a/packages/cli-node/src/roles/types.ts +++ b/packages/cli-node/src/roles/types.ts @@ -15,7 +15,7 @@ */ /** - * Backstage package role, see {@link https://backstage.io/docs/local-dev/cli-build-system#package-roles | docs}. + * Backstage package role, see {@link https://backstage.io/docs/tooling/cli/build-system#package-roles | docs}. * * @public */ diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 7f59e4ef73..b8e2c19747 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/cli +## 0.26.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-node@0.2.6-next.1 + - @backstage/integration@1.12.0-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## 0.26.7-next.1 + +### Patch Changes + +- 788eca7: Fix readme for new plugins created using cli +- c00f7ee: Fix issue with `esm` loaded dependencies being different from the `cjs` import for Vite dependencies +- Updated dependencies + - @backstage/cli-node@0.2.6-next.0 + - @backstage/config-loader@1.8.0 + +## 0.26.6-next.0 + +### Patch Changes + +- 009da47: Fix `versions:check --fix` when `yarn.lock` has multiple joint versions in the same section +- 9ee948a: Bump `esbuild` target for package builds to `ES2022`. +- Updated dependencies + - @backstage/cli-node@0.2.6-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/integration@1.11.0 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + ## 0.26.5 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 67492919d0..e0212a34ff 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli", - "version": "0.26.5", + "version": "0.26.7-next.2", "description": "CLI for developing Backstage plugins and apps", "backstage": { "role": "cli" diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 53f3b2c25a..1da15cd37e 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -165,12 +165,12 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be }); if (process.env.EXPERIMENTAL_VITE) { - const vite = await import('vite'); - const { default: viteReact } = await import('@vitejs/plugin-react'); - const { nodePolyfills: viteNodePolyfills } = await import( - 'vite-plugin-node-polyfills' - ); - const { createHtmlPlugin: viteHtml } = await import('vite-plugin-html'); + const vite = require('vite'); + const { default: viteReact } = require('@vitejs/plugin-react'); + const { + nodePolyfills: viteNodePolyfills, + } = require('vite-plugin-node-polyfills'); + const { createHtmlPlugin: viteHtml } = require('vite-plugin-html'); viteServer = await vite.createServer({ define: { global: 'window', diff --git a/packages/cli/templates/default-backend-plugin/README.md.hbs b/packages/cli/templates/default-backend-plugin/README.md.hbs index e95c626167..366ed27104 100644 --- a/packages/cli/templates/default-backend-plugin/README.md.hbs +++ b/packages/cli/templates/default-backend-plugin/README.md.hbs @@ -7,7 +7,7 @@ _This plugin was created through the Backstage CLI_ ## Getting started Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn -start` in the root directory, and then navigating to [/{{pluginVar}}/health](http://localhost:7007/api/{{pluginVar}}/health). +start` in the root directory, and then navigating to [/{{id}}/health](http://localhost:7007/api/{{id}}/health). You can also serve the plugin in isolation by running `yarn start` in the plugin directory. This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index f171c222b7..53a9807c61 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/core-app-api +## 1.12.6-next.0 + +### Patch Changes + +- 35fbe09: Added support for configuration of route bindings through static configuration, and default targets for external route refs. + + In addition to configuring route bindings through code, it is now also possible to configure route bindings under the `app.routes.bindings` key, for example: + + ```yaml + app: + routes: + bindings: + catalog.createComponent: catalog-import.importPage + ``` + + Each key in the route binding object is of the form `.`, where the route name is key used in the `externalRoutes` object passed to `createPlugin`. The value is of the same form, but with the name taken from the plugin `routes` option instead. + + The equivalent of the above configuration in code is the following: + + ```ts + const app = createApp({ + // ... + bindRoutes({ bind }) { + bind(catalogPlugin.externalRoutes, { + createComponent: catalogImportPlugin.routes.importPage, + }); + }, + }); + ``` + +- Updated dependencies + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 1.12.5 ### Patch Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 0a74dac3f8..8c9c31946d 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "1.12.5", + "version": "1.12.6-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 59b53bf6c5..85b6bcd684 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -20,7 +20,7 @@ import { renderWithEffects, withLogCollector, } from '@backstage/test-utils'; -import { render, screen, waitFor } from '@testing-library/react'; +import { screen, waitFor, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React, { PropsWithChildren, ReactNode } from 'react'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; @@ -624,7 +624,7 @@ describe('Integration Test', () => { expect(capturedEvents).toHaveLength(2); }); - it('should throw some error when the route has duplicate params', () => { + it('should throw some error when the route has duplicate params', async () => { const app = new AppManager({ apis: [], defaultApis: [], @@ -641,31 +641,43 @@ describe('Integration Test', () => { }, }); + const expectedMessage = + 'Parameter :thing is duplicated in path test/:thing/some/:thing'; + const Provider = app.getProvider(); const Router = app.getRouter(); - const { error: errorLogs } = withLogCollector(() => { - render( - - - - }> - } /> - - - - , - ); + const { error: errorLogs } = await withLogCollector(async () => { + await expect(() => + renderWithEffects( + + + + }> + } /> + + + + , + ), + ).rejects.toThrow(expectedMessage); }); + expect(errorLogs).toEqual([ expect.objectContaining({ - message: expect.stringContaining( - 'Parameter :thing is duplicated in path test/:thing/some/:thing', - ), + detail: new Error(expectedMessage), + type: 'unhandled exception', }), + expect.objectContaining({ + detail: new Error(expectedMessage), + type: 'unhandled exception', + }), + expect.stringContaining( + 'The above error occurred in the component:', + ), ]); }); - it('should throw an error when required external plugin routes are not bound', () => { + it('should throw an error when required external plugin routes are not bound', async () => { const app = new AppManager({ apis: [], defaultApis: [], @@ -676,25 +688,36 @@ describe('Integration Test', () => { configLoader: async () => [], }); + const expectedMessage = + "External route 'extRouteRef1' of the 'blob' plugin must be bound to a target route. See https://backstage.io/link?bind-routes for details."; + const Provider = app.getProvider(); const Router = app.getRouter(); - const { error: errorLogs } = withLogCollector(() => { - render( - - - - } /> - - - , - ); + const { error: errorLogs } = await withLogCollector(async () => { + await expect(() => + renderWithEffects( + + + + } /> + + + , + ), + ).rejects.toThrow(expectedMessage); }); expect(errorLogs).toEqual([ expect.objectContaining({ - message: expect.stringMatching( - /^External route 'extRouteRef1' of the 'blob' plugin must be bound to a target route/, - ), + detail: new Error(expectedMessage), + type: 'unhandled exception', }), + expect.objectContaining({ + detail: new Error(expectedMessage), + type: 'unhandled exception', + }), + expect.stringContaining( + 'The above error occurred in the component:', + ), ]); }); @@ -874,7 +897,9 @@ describe('Integration Test', () => { ); await renderWithEffects(); - await userEvent.click(screen.getByText('Sign Out')); + await act(async () => { + await userEvent.click(screen.getByText('Sign Out')); + }); await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledWith( diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 64736470d2..b28b843d34 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -233,8 +233,10 @@ export class AppManager implements BackstageApp { const appContext = new AppContextImpl(this); - // We only validate routes once - let routesHaveBeenValidated = false; + // We only bind and validate routes once + let routeBindings: ReturnType; + // Store and keep throwing the same error if we encounter one + let routeValidationError: Error | undefined = undefined; const Provider = ({ children }: PropsWithChildren<{}>) => { const needsFeatureFlagRegistrationRef = useRef(true); @@ -243,7 +245,7 @@ export class AppManager implements BackstageApp { [], ); - const { routing, featureFlags, routeBindings } = useMemo(() => { + const { routing, featureFlags } = useMemo(() => { const usesReactRouterBeta = isReactRouterBeta(); if (usesReactRouterBeta) { // eslint-disable-next-line no-console @@ -275,21 +277,9 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be // Initialize APIs once all plugins are available this.getApiHolder(); - return { - ...result, - routeBindings: resolveRouteBindings(this.bindRoutes), - }; + return result; }, [children]); - if (!routesHaveBeenValidated) { - routesHaveBeenValidated = true; - validateRouteParameters(routing.paths, routing.parents); - validateRouteBindings( - routeBindings, - this.plugins as Iterable, - ); - } - const loadedConfig = useConfigLoader( this.configLoader, this.components, @@ -307,6 +297,24 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be return loadedConfig.node; } + if (routeValidationError) { + throw routeValidationError; + } else if (!routeBindings) { + try { + routeBindings = resolveRouteBindings( + this.bindRoutes, + loadedConfig.api, + this.plugins, + ); + + validateRouteParameters(routing.paths, routing.parents); + validateRouteBindings(routeBindings, this.plugins); + } catch (error) { + routeValidationError = error; + throw error; + } + } + // We can't register feature flags just after the element traversal, because the // config API isn't available yet and implementations frequently depend on it. // Instead we make it happen immediately, to make sure all flags are available diff --git a/packages/core-app-api/src/app/resolveRouteBindings.test.ts b/packages/core-app-api/src/app/resolveRouteBindings.test.ts index 5c9fde53b7..9dfbbfd452 100644 --- a/packages/core-app-api/src/app/resolveRouteBindings.test.ts +++ b/packages/core-app-api/src/app/resolveRouteBindings.test.ts @@ -16,17 +16,23 @@ import { createExternalRouteRef, + createPlugin, createRouteRef, } from '@backstage/core-plugin-api'; -import { resolveRouteBindings } from './resolveRouteBindings'; +import { collectRouteIds, resolveRouteBindings } from './resolveRouteBindings'; +import { MockConfigApi } from '@backstage/test-utils'; describe('resolveRouteBindings', () => { it('runs happy path', () => { const external = { myRoute: createExternalRouteRef({ id: '1' }) }; const ref = createRouteRef({ id: 'ref-1' }); - const result = resolveRouteBindings(({ bind }) => { - bind(external, { myRoute: ref }); - }); + const result = resolveRouteBindings( + ({ bind }) => { + bind(external, { myRoute: ref }); + }, + new MockConfigApi({}), + [], + ); expect(result.get(external.myRoute)).toBe(ref); }); @@ -35,9 +41,157 @@ describe('resolveRouteBindings', () => { const external = { myRoute: createExternalRouteRef({ id: '2' }) }; const ref = createRouteRef({ id: 'ref-2' }); expect(() => - resolveRouteBindings(({ bind }) => { - bind(external, { someOtherRoute: ref } as any); - }), + resolveRouteBindings( + ({ bind }) => { + bind(external, { someOtherRoute: ref } as any); + }, + new MockConfigApi({}), + [], + ), ).toThrow('Key someOtherRoute is not an existing external route'); }); + + it('reads bindings from config', () => { + const mySource = createExternalRouteRef({ id: 'test' }); + const myTarget = createRouteRef({ id: 'test' }); + const result = resolveRouteBindings( + () => {}, + new MockConfigApi({ + app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } }, + }), + [ + createPlugin({ + id: 'test', + routes: { + myTarget, + }, + externalRoutes: { + mySource, + }, + }), + ], + ); + + expect(result.get(mySource)).toBe(myTarget); + }); + + it('throws on invalid config', () => { + expect(() => + resolveRouteBindings( + () => {}, + new MockConfigApi({ app: { routes: { bindings: 'derp' } } }), + [], + ), + ).toThrow( + "Invalid type in config for key 'app.routes.bindings' in 'mock-config', got string, wanted object", + ); + + expect(() => + resolveRouteBindings( + () => {}, + new MockConfigApi({ + app: { routes: { bindings: { 'test.mySource': true } } }, + }), + [], + ), + ).toThrow( + "Invalid config at app.routes.bindings['test.mySource'], value must be a non-empty string", + ); + + expect(() => + resolveRouteBindings( + () => {}, + new MockConfigApi({ + app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } }, + }), + [], + ), + ).toThrow( + "Invalid config at app.routes.bindings, 'test.mySource' is not a valid external route", + ); + + expect(() => + resolveRouteBindings( + () => {}, + new MockConfigApi({ + app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } }, + }), + [ + createPlugin({ + id: 'test', + externalRoutes: { + mySource: createExternalRouteRef({ id: 'test' }), + }, + }), + ], + ), + ).toThrow( + "Invalid config at app.routes.bindings['test.mySource'], 'test.myTarget' is not a valid route", + ); + }); + + it('can have default targets, but at the lowest priority', () => { + const source = createExternalRouteRef({ + id: 'test', + defaultTarget: 'test.target1', + }); + const target1 = createRouteRef({ id: 'test' }); + const target2 = createRouteRef({ id: 'test' }); + const plugin = createPlugin({ + id: 'test', + routes: { + target1, + target2, + }, + externalRoutes: { + source, + }, + }); + + // defaultTarget wins only if no bind or config matches + let result = resolveRouteBindings(() => {}, new MockConfigApi({}), [ + plugin, + ]); + + expect(result.get(source)).toBe(target1); + + // config wins over defaultTarget + result = resolveRouteBindings( + () => {}, + new MockConfigApi({ + app: { routes: { bindings: { 'test.source': 'test.target2' } } }, + }), + [plugin], + ); + + expect(result.get(source)).toBe(target2); + + // bind wins over defaultTarget + result = resolveRouteBindings( + ({ bind }) => { + bind(plugin.externalRoutes, { source: plugin.routes.target2 }); + }, + new MockConfigApi({}), + [plugin], + ); + + expect(result.get(source)).toBe(target2); + }); +}); + +describe('collectRouteIds', () => { + it('should assign IDs to routes', () => { + const ref = createRouteRef({ id: 'ignored' }); + const extRef = createExternalRouteRef({ id: 'ignored' }); + + const collected = collectRouteIds([ + createPlugin({ id: 'test', routes: { ref }, externalRoutes: { extRef } }), + ]); + expect(Object.fromEntries(collected.routes)).toEqual({ + 'test.ref': ref, + }); + expect(Object.fromEntries(collected.externalRoutes)).toEqual({ + 'test.extRef': extRef, + }); + }); }); diff --git a/packages/core-app-api/src/app/resolveRouteBindings.ts b/packages/core-app-api/src/app/resolveRouteBindings.ts index 63119b204b..55e1139b29 100644 --- a/packages/core-app-api/src/app/resolveRouteBindings.ts +++ b/packages/core-app-api/src/app/resolveRouteBindings.ts @@ -18,12 +18,63 @@ import { RouteRef, SubRouteRef, ExternalRouteRef, + BackstagePlugin, + AnyRoutes, + AnyExternalRoutes, } from '@backstage/core-plugin-api'; import { AppOptions, AppRouteBinder } from './types'; +import { Config } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; -export function resolveRouteBindings(bindRoutes: AppOptions['bindRoutes']) { +/** @internal */ +export function collectRouteIds( + plugins: Iterable< + Pick< + BackstagePlugin, + 'getId' | 'routes' | 'externalRoutes' + > + >, +) { + const routesById = new Map(); + const externalRoutesById = new Map(); + + for (const plugin of plugins) { + for (const [name, ref] of Object.entries(plugin.routes ?? {})) { + const refId = `${plugin.getId()}.${name}`; + if (routesById.has(refId)) { + throw new Error(`Unexpected duplicate route '${refId}'`); + } + + routesById.set(refId, ref); + } + for (const [name, ref] of Object.entries(plugin.externalRoutes ?? {})) { + const refId = `${plugin.getId()}.${name}`; + if (externalRoutesById.has(refId)) { + throw new Error(`Unexpected duplicate external route '${refId}'`); + } + + externalRoutesById.set(refId, ref); + } + } + + return { routes: routesById, externalRoutes: externalRoutesById }; +} + +/** @internal */ +export function resolveRouteBindings( + bindRoutes: AppOptions['bindRoutes'], + config: Config, + plugins: Iterable< + Pick< + BackstagePlugin, + 'getId' | 'routes' | 'externalRoutes' + > + >, +) { + const routesById = collectRouteIds(plugins); const result = new Map(); + // Perform callback bindings first with highest priority if (bindRoutes) { const bind: AppRouteBinder = ( externalRoutes, @@ -47,5 +98,53 @@ export function resolveRouteBindings(bindRoutes: AppOptions['bindRoutes']) { bindRoutes({ bind }); } + // Then perform config based bindings with lower priority + const bindings = config + .getOptionalConfig('app.routes.bindings') + ?.get(); + if (bindings) { + for (const [externalRefId, targetRefId] of Object.entries(bindings)) { + if (typeof targetRefId !== 'string' || targetRefId === '') { + throw new Error( + `Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string`, + ); + } + + const externalRef = routesById.externalRoutes.get(externalRefId); + if (!externalRef) { + throw new Error( + `Invalid config at app.routes.bindings, '${externalRefId}' is not a valid external route`, + ); + } + if (result.has(externalRef)) { + continue; + } + const targetRef = routesById.routes.get(targetRefId); + if (!targetRef) { + throw new Error( + `Invalid config at app.routes.bindings['${externalRefId}'], '${targetRefId}' is not a valid route`, + ); + } + + result.set(externalRef, targetRef); + } + } + + // Finally fall back to attempting to map defaults, at lowest priority + for (const externalRef of routesById.externalRoutes.values()) { + if (!result.has(externalRef)) { + const defaultRefId = + 'getDefaultTarget' in externalRef + ? (externalRef.getDefaultTarget as () => string | undefined)() + : undefined; + if (defaultRefId) { + const defaultRef = routesById.routes.get(defaultRefId); + if (defaultRef) { + result.set(externalRef, defaultRef); + } + } + } + } + return result; } diff --git a/packages/core-app-api/src/routing/validation.ts b/packages/core-app-api/src/routing/validation.ts index 63950b9fdc..ab0fc42af4 100644 --- a/packages/core-app-api/src/routing/validation.ts +++ b/packages/core-app-api/src/routing/validation.ts @@ -66,7 +66,12 @@ export function validateRouteParameters( // Validates that all non-optional external routes have been bound export function validateRouteBindings( routeBindings: Map, - plugins: Iterable>>, + plugins: Iterable< + Pick< + BackstagePlugin<{}, Record>, + 'getId' | 'externalRoutes' + > + >, ) { for (const plugin of plugins) { if (!plugin.externalRoutes) { diff --git a/packages/core-compat-api/CHANGELOG.md b/packages/core-compat-api/CHANGELOG.md index 67084d7926..852b7303f0 100644 --- a/packages/core-compat-api/CHANGELOG.md +++ b/packages/core-compat-api/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/core-compat-api +## 0.2.6-next.1 + +### Patch Changes + +- 35fbe09: Add support for forwarding default target from legacy external route refs. +- Updated dependencies + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/version-bridge@1.0.8 + +## 0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.9.2 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/version-bridge@1.0.8 + ## 0.2.5 ### Patch Changes diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index 9124060464..9fa663ba7a 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-compat-api", - "version": "0.2.5", + "version": "0.2.6-next.1", "backstage": { "role": "web-library" }, diff --git a/packages/core-compat-api/src/convertLegacyRouteRef.ts b/packages/core-compat-api/src/convertLegacyRouteRef.ts index a5375e0692..c967f9e9b5 100644 --- a/packages/core-compat-api/src/convertLegacyRouteRef.ts +++ b/packages/core-compat-api/src/convertLegacyRouteRef.ts @@ -186,6 +186,10 @@ export function convertLegacyRouteRef( createExternalRouteRef<{ [key in string]: string }>({ params: legacyRef.params as string[], optional: legacyRef.optional, + defaultTarget: + 'getDefaultTarget' in legacyRef + ? (legacyRef.getDefaultTarget as () => string | undefined)() + : undefined, }), ); return Object.assign(legacyRef, { @@ -199,9 +203,9 @@ export function convertLegacyRouteRef( getDescription() { return legacyRefStr; }, + // This might already be implemented in the legacy ref, but we override it just to be sure getDefaultTarget() { - // TODO(freben): These are not yet supported in the old system; just returning undefined for now - return undefined; + return newRef.getDefaultTarget(); }, setId(id: string) { newRef.setId(id); diff --git a/packages/core-components/.eslintrc.js b/packages/core-components/.eslintrc.js index 505ea657a2..f5ec47b36a 100644 --- a/packages/core-components/.eslintrc.js +++ b/packages/core-components/.eslintrc.js @@ -4,6 +4,10 @@ module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { '@backstage/no-top-level-material-ui-4-imports': 'error', }, restrictedImports: [ + { + name: '@backstage/core-components', + message: "To avoid circular dependencies, use relative paths to import '@backstage/core-components' from its subdirectories." + }, { name: '@material-ui/core', message: "Please import '@material-ui/core/...' instead.", diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 4c2a973813..c85c90ae6c 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/core-components +## 0.14.8-next.1 + +### Patch Changes + +- a0b46f6: Having tooltip inherit font size for consistency in catalog table columns +- eae0e4d: Fixed an issue causing `SidebarSubmenu` text to not follow the theme color +- 3e175c8: Removed max width from `Select` component. +- 57d7582: Fixed a bug in `SupportButton` where the title was rendered with the characteristics of a button. +- Updated dependencies + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6-next.0 + - @backstage/version-bridge@1.0.8 + +## 0.14.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/version-bridge@1.0.8 + ## 0.14.7 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 435a013062..43e1713426 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.14.7", + "version": "0.14.8-next.1", "publishConfig": { "access": "public" }, diff --git a/packages/core-components/src/components/OverflowTooltip/OverflowTooltip.tsx b/packages/core-components/src/components/OverflowTooltip/OverflowTooltip.tsx index 4b5b0f4bde..dd4838bd8d 100644 --- a/packages/core-components/src/components/OverflowTooltip/OverflowTooltip.tsx +++ b/packages/core-components/src/components/OverflowTooltip/OverflowTooltip.tsx @@ -34,6 +34,7 @@ const useStyles = makeStyles( overflow: 'visible !important', }, typo: { + fontSize: 'inherit', overflow: 'hidden', textOverflow: 'ellipsis', display: '-webkit-box', @@ -52,7 +53,9 @@ export function OverflowTooltip(props: Props) { title={props.title ?? (props.text || '')} placement={props.placement} > - {props.text} + + {props.text} + ); } diff --git a/packages/core-components/src/components/Select/Select.tsx b/packages/core-components/src/components/Select/Select.tsx index b1a2139b85..924b49f640 100644 --- a/packages/core-components/src/components/Select/Select.tsx +++ b/packages/core-components/src/components/Select/Select.tsx @@ -78,7 +78,6 @@ const useStyles = makeStyles( createStyles({ formControl: { margin: theme.spacing(1, 0), - maxWidth: 300, }, label: { transform: 'initial', diff --git a/packages/core-components/src/components/SupportButton/SupportButton.tsx b/packages/core-components/src/components/SupportButton/SupportButton.tsx index 623eda3098..8626353dd1 100644 --- a/packages/core-components/src/components/SupportButton/SupportButton.tsx +++ b/packages/core-components/src/components/SupportButton/SupportButton.tsx @@ -151,7 +151,11 @@ export function SupportButton(props: SupportButtonProps) { autoFocusItem={Boolean(anchorEl)} > {title && ( - + {title} )} diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx index 6b6951d023..e05cc90fa9 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx @@ -80,7 +80,7 @@ const useStyles = makeStyles< title: { fontSize: theme.typography.h5.fontSize, fontWeight: theme.typography.fontWeightMedium, - color: theme.palette.common.white, + color: theme.palette.navigation.color, padding: theme.spacing(2.5), [theme.breakpoints.down('xs')]: { display: 'none', diff --git a/packages/core-components/src/layout/TabbedCard/TabbedCard.test.tsx b/packages/core-components/src/layout/TabbedCard/TabbedCard.test.tsx index 707d528867..329fb62129 100644 --- a/packages/core-components/src/layout/TabbedCard/TabbedCard.test.tsx +++ b/packages/core-components/src/layout/TabbedCard/TabbedCard.test.tsx @@ -104,7 +104,7 @@ describe('', () => { const rendered = render( wrapInTestApp( - + Test Content 1 diff --git a/packages/core-components/src/setupTests.ts b/packages/core-components/src/setupTests.ts index 963c0f188b..dae4360fd3 100644 --- a/packages/core-components/src/setupTests.ts +++ b/packages/core-components/src/setupTests.ts @@ -15,3 +15,18 @@ */ import '@testing-library/jest-dom'; + +// eslint-disable-next-line no-console +const originalConsoleWarn = console.warn; +// eslint-disable-next-line no-console +console.warn = (...args: any[]) => { + const message = args[0]; + if ( + typeof message === 'string' && + (message.includes('CSSOM.parse is not a function') || + message.includes('[JSS]')) + ) { + return; + } + originalConsoleWarn(...args); +}; diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index 1bd5ab39a4..80e969d40c 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/core-plugin-api +## 1.9.3-next.0 + +### Patch Changes + +- 35fbe09: Added a new `defaultTarget` option to `createExternalRouteRef`. I lets you specify a default target of the route by name, for example `'catalog.catalogIndex'`, which will be used if the target route is present in the app and there is no explicit route binding. +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 1.9.2 ### Patch Changes diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 2a993687b3..134b338c92 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -314,6 +314,7 @@ export function createExternalRouteRef< id: string; params?: ParamKey[]; optional?: Optional; + defaultTarget?: string; }): ExternalRouteRef, Optional>; // @public diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index f75d676365..9cb5dca728 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-plugin-api", "description": "Core API used by Backstage plugins", - "version": "1.9.2", + "version": "1.9.3-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/core-plugin-api/src/routing/ExternalRouteRef.ts b/packages/core-plugin-api/src/routing/ExternalRouteRef.ts index 2ebb81dbfb..d61001aa10 100644 --- a/packages/core-plugin-api/src/routing/ExternalRouteRef.ts +++ b/packages/core-plugin-api/src/routing/ExternalRouteRef.ts @@ -38,11 +38,16 @@ export class ExternalRouteRefImpl< private readonly id: string, readonly params: ParamKeys, readonly optional: Optional, + readonly defaultTarget: string | undefined, ) {} toString() { return `routeRef{type=external,id=${this.id}}`; } + + getDefaultTarget() { + return this.defaultTarget; + } } /** @@ -77,10 +82,19 @@ export function createExternalRouteRef< * if they aren't, `useRouteRef` will return `undefined`. */ optional?: Optional; + + /** + * The route (typically in another plugin) that this should map to by default. + * + * The string is expected to be on the standard `.` form, + * for example `techdocs.docRoot`. + */ + defaultTarget?: string; }): ExternalRouteRef, Optional> { return new ExternalRouteRefImpl( options.id, (options.params ?? []) as ParamKeys>, Boolean(options.optional) as Optional, + options?.defaultTarget, ); } diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index be4c46ab03..b9af320d43 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/create-app +## 0.5.16-next.2 + +### Patch Changes + +- 78363f6: Bumped TypeScript to version `5.4`. +- 81507c8: Updated `node-gyp` to v10 +- Updated dependencies + - @backstage/cli-common@0.1.13 + +## 0.5.16-next.1 + +### Patch Changes + +- Bumped create-app version. + +## 0.5.16-next.0 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.13 + ## 0.5.15 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 422442ed2b..bb2e0caf7d 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.5.15", + "version": "0.5.16-next.2", "publishConfig": { "access": "public" }, diff --git a/packages/create-app/seed-yarn.lock b/packages/create-app/seed-yarn.lock index 00a86105cc..2f41ba9ca3 100644 --- a/packages/create-app/seed-yarn.lock +++ b/packages/create-app/seed-yarn.lock @@ -16,3 +16,8 @@ // package: the name of the package, e.g. @testing-library/react // query: the version query to pin the version for, e.g. ^14.0.0 // version: the version to pin to, must be in range of the query, e.g. 14.11.0 + +ajv@^8.0.0, ajv@^8.10.0, ajv@^8.11.0, ajv@^8.12.0, ajv@^8.14.0, ajv@^8.6.0, ajv@^8.6.3, ajv@^8.8.2, ajv@^8.9.0: + version "8.14.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.14.0.tgz#f514ddfd4756abb200e1704414963620a625ebbb" + integrity sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA== diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index f2f35360cc..7841b5d4e6 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -37,9 +37,9 @@ "@spotify/prettier-config": "^12.0.0", "concurrently": "^8.0.0", "lerna": "^7.3.0", - "node-gyp": "^9.0.0", + "node-gyp": "^10.0.0", "prettier": "^2.3.2", - "typescript": "~5.3.0" + "typescript": "~5.4.0" }, "resolutions": { "@types/react": "^18", diff --git a/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx b/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx index 6768b48dad..71c04122ae 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx @@ -2,7 +2,6 @@ import React, { PropsWithChildren } from 'react'; import { makeStyles } from '@material-ui/core'; import HomeIcon from '@material-ui/icons/Home'; import ExtensionIcon from '@material-ui/icons/Extension'; -import MapIcon from '@material-ui/icons/MyLocation'; import LibraryBooks from '@material-ui/icons/LibraryBooks'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; import LogoFull from './LogoFull'; @@ -72,7 +71,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( {/* End global nav */} - + {/* Items in this group will be scrollable if they run out of space */} diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index 93228e5c8f..049ea1c30c 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -41,7 +41,7 @@ "app": "link:../app", "better-sqlite3": "^9.0.0", "dockerode": "^3.3.1", - "node-gyp": "^9.0.0", + "node-gyp": "^10.0.0", "pg": "^8.11.3", "winston": "^3.2.1" }, diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index c8e32a39d0..303ffaa62c 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/dev-utils +## 1.0.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/app-defaults@1.5.6-next.1 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/theme@0.5.6-next.0 + +## 1.0.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/app-defaults@1.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-plugin-api@1.9.2 + - @backstage/integration-react@1.1.27 + - @backstage/plugin-catalog-react@1.12.1-next.0 + ## 1.0.32 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index de5acd3249..29430659d4 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/dev-utils", - "version": "1.0.32", + "version": "1.0.33-next.1", "description": "Utilities for developing Backstage plugins.", "backstage": { "role": "web-library" diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md index 7ff6862d2f..47cf99d322 100644 --- a/packages/e2e-test/CHANGELOG.md +++ b/packages/e2e-test/CHANGELOG.md @@ -1,5 +1,23 @@ # e2e-test +## 0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.16-next.2 + - @backstage/cli-common@0.1.13 + - @backstage/errors@1.2.4 + +## 0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.16-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/errors@1.2.4 + ## 0.2.16 ### Patch Changes diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index a77bb8c8a1..454602e338 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -1,7 +1,7 @@ { "name": "e2e-test", "description": "E2E test for verifying Backstage packages", - "version": "0.2.16", + "version": "0.2.17-next.1", "private": true, "backstage": { "role": "cli" diff --git a/packages/frontend-app-api/CHANGELOG.md b/packages/frontend-app-api/CHANGELOG.md index b9571091a8..af86bc675b 100644 --- a/packages/frontend-app-api/CHANGELOG.md +++ b/packages/frontend-app-api/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/frontend-app-api +## 0.7.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 0.7.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 0.7.0 ### Minor Changes diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index 2e350b5a98..eb5019898c 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-app-api", - "version": "0.7.0", + "version": "0.7.1-next.1", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-app-api/src/setupTests.ts b/packages/frontend-app-api/src/setupTests.ts index c30f1d15cb..9abf7083b5 100644 --- a/packages/frontend-app-api/src/setupTests.ts +++ b/packages/frontend-app-api/src/setupTests.ts @@ -15,3 +15,18 @@ */ import '@testing-library/jest-dom'; + +// eslint-disable-next-line no-console +const originalConsoleWarn = console.warn; +// eslint-disable-next-line no-console +console.warn = (...args: any[]) => { + const message = args[0]; + if ( + typeof message === 'string' && + (message.includes('CSSOM.parse is not a function') || + message.includes('[JSS]')) + ) { + return; + } + originalConsoleWarn(...args); +}; diff --git a/packages/frontend-plugin-api/CHANGELOG.md b/packages/frontend-plugin-api/CHANGELOG.md index f12de020f5..790b7f32bb 100644 --- a/packages/frontend-plugin-api/CHANGELOG.md +++ b/packages/frontend-plugin-api/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/frontend-plugin-api +## 0.6.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 0.6.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 0.6.5 ### Patch Changes diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index 2dedd6fc12..6b2e15aa50 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-plugin-api", - "version": "0.6.5", + "version": "0.6.6-next.1", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-plugin-api/src/setupTests.ts b/packages/frontend-plugin-api/src/setupTests.ts index c30f1d15cb..9abf7083b5 100644 --- a/packages/frontend-plugin-api/src/setupTests.ts +++ b/packages/frontend-plugin-api/src/setupTests.ts @@ -15,3 +15,18 @@ */ import '@testing-library/jest-dom'; + +// eslint-disable-next-line no-console +const originalConsoleWarn = console.warn; +// eslint-disable-next-line no-console +console.warn = (...args: any[]) => { + const message = args[0]; + if ( + typeof message === 'string' && + (message.includes('CSSOM.parse is not a function') || + message.includes('[JSS]')) + ) { + return; + } + originalConsoleWarn(...args); +}; diff --git a/packages/frontend-test-utils/CHANGELOG.md b/packages/frontend-test-utils/CHANGELOG.md index b2abbd9c2d..2d1d700001 100644 --- a/packages/frontend-test-utils/CHANGELOG.md +++ b/packages/frontend-test-utils/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/frontend-test-utils +## 0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-app-api@0.7.1-next.1 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/test-utils@1.5.6-next.1 + - @backstage/types@1.1.1 + +## 0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-app-api@0.7.1-next.0 + - @backstage/test-utils@1.5.6-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/types@1.1.1 + ## 0.1.7 ### Patch Changes diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index 4845e7e9b1..8da8dbc0fc 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-test-utils", - "version": "0.1.7", + "version": "0.1.8-next.1", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-test-utils/src/setupTests.ts b/packages/frontend-test-utils/src/setupTests.ts index c30f1d15cb..9abf7083b5 100644 --- a/packages/frontend-test-utils/src/setupTests.ts +++ b/packages/frontend-test-utils/src/setupTests.ts @@ -15,3 +15,18 @@ */ import '@testing-library/jest-dom'; + +// eslint-disable-next-line no-console +const originalConsoleWarn = console.warn; +// eslint-disable-next-line no-console +console.warn = (...args: any[]) => { + const message = args[0]; + if ( + typeof message === 'string' && + (message.includes('CSSOM.parse is not a function') || + message.includes('[JSS]')) + ) { + return; + } + originalConsoleWarn(...args); +}; diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index f44e72919c..1f9bc9f9fe 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/integration-react +## 1.1.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/integration@1.12.0-next.0 + - @backstage/config@1.2.0 + ## 1.1.27 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 07effadbae..e7d653b09a 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration-react", - "version": "1.1.27", + "version": "1.1.28-next.0", "description": "Frontend package for managing integrations towards external systems", "backstage": { "role": "web-library" diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index 0c00b80d58..aea2956f17 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/integration +## 1.12.0-next.0 + +### Minor Changes + +- 395b973: Implemented `readTree` for Harness provider to support TechDocs functionality + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 1.11.0 ### Minor Changes diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 2029dac600..4945847751 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -513,12 +513,24 @@ export function getGitLabRequestOptions(config: GitLabIntegrationConfig): { headers: Record; }; +// @public +export function getHarnessArchiveUrl( + config: HarnessIntegrationConfig, + url: string, +): string; + // @public export function getHarnessFileContentsUrl( config: HarnessIntegrationConfig, url: string, ): string; +// @public +export function getHarnessLatestCommitUrl( + config: HarnessIntegrationConfig, + url: string, +): string; + // @public export function getHarnessRequestOptions(config: HarnessIntegrationConfig): { headers?: Record; @@ -764,6 +776,22 @@ export function parseGiteaUrl( path: string; }; +// @public +export function parseHarnessUrl( + config: HarnessIntegrationConfig, + url: string, +): { + baseUrl: string; + accountId: string; + orgName: string; + projectName: string; + refString: string; + repoName: string; + path: string; + refDashStr: string; + branch: string; +}; + // @public export type PersonalAccessTokenCredential = AzureCredentialBase & { kind: 'PersonalAccessToken'; diff --git a/packages/integration/package.json b/packages/integration/package.json index da4b700fd6..ec5944ccbd 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration", - "version": "1.11.0", + "version": "1.12.0-next.0", "description": "Helpers for managing integrations towards external systems", "backstage": { "role": "common-library" diff --git a/packages/integration/src/harness/HarnessIntegration.test.ts b/packages/integration/src/harness/HarnessIntegration.test.ts index 2d85204f16..8a936dc07b 100644 --- a/packages/integration/src/harness/HarnessIntegration.test.ts +++ b/packages/integration/src/harness/HarnessIntegration.test.ts @@ -117,7 +117,7 @@ describe('HarnessIntegration', () => { 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repos/repoName/edit/refMain/~/all-apis.yaml', ), ).toBe( - 'https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projName/repoName/+/edit/all-apis.yaml', + 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repoName/files/refMain/~/all-apis.yaml', ); }); }); diff --git a/packages/integration/src/harness/core.test.ts b/packages/integration/src/harness/core.test.ts index d3d402806a..809c710ecf 100644 --- a/packages/integration/src/harness/core.test.ts +++ b/packages/integration/src/harness/core.test.ts @@ -18,9 +18,12 @@ import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '../helpers'; import { HarnessIntegrationConfig } from './config'; import { + getHarnessArchiveUrl, getHarnessEditContentsUrl, getHarnessFileContentsUrl, + getHarnessLatestCommitUrl, getHarnessRequestOptions, + parseHarnessUrl, } from './core'; describe('Harness code core', () => { @@ -38,7 +41,7 @@ describe('Harness code core', () => { 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repos/repoName/files/refMain/~/all-apis.yaml', ), ).toEqual( - 'https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projName/repoName/+/raw/all-apis.yaml?routingId=accountId&git_ref=refMain', + 'https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projName/repoName/+/raw/all-apis.yaml?routingId=accountId&git_ref=refs/heads/refMain', ); }); }); @@ -54,7 +57,39 @@ describe('Harness code core', () => { 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repos/repoName/edit/refMain/~/all-apis.yaml', ), ).toEqual( - 'https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projName/repoName/+/edit/all-apis.yaml', + 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repoName/files/refMain/~/all-apis.yaml', + ); + }); + }); + + describe('getHarnessArchiveUrl', () => { + it('can create an url from arguments', () => { + const config: HarnessIntegrationConfig = { + host: 'app.harness.io', + }; + expect( + getHarnessArchiveUrl( + config, + 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projectName/repos/repoName/files/branchName', + ), + ).toEqual( + 'https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projectName/repoName/+/archive/branchName.zip?routingId=accountId', + ); + }); + }); + + describe('getHarnessLatestCommitUrl', () => { + it('can create an url from arguments', () => { + const config: HarnessIntegrationConfig = { + host: 'app.harness.io', + }; + expect( + getHarnessLatestCommitUrl( + config, + 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projectName/repos/repoName/files/branchName', + ), + ).toEqual( + 'https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projectName/repoName/+/content?routingId=accountId&include_commit=true&git_ref=refs/heads/branchName', ); }); }); @@ -88,4 +123,50 @@ describe('Harness code core', () => { ).toEqual('a'); }); }); + + describe('parseHarnessUrl', () => { + it('can fetch harness url', () => { + const config: HarnessIntegrationConfig = { + host: 'app.harness.io', + }; + expect( + parseHarnessUrl( + config, + 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projectName/repos/repoName/files/branchName', + ), + ).toEqual({ + accountId: 'accountId', + baseUrl: 'https://app.harness.io', + orgName: 'orgName', + path: '', + projectName: 'projectName', + refDashStr: '', + refString: '', + repoName: 'repoName', + branch: 'branchName', + }); + }); + + it('provide path without starting slash', () => { + const config: HarnessIntegrationConfig = { + host: 'app.harness.io', + }; + expect( + parseHarnessUrl( + config, + 'https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projectName/repos/repoName/files/branchName/simple/~/path', + ), + ).toEqual({ + accountId: 'accountId', + baseUrl: 'https://app.harness.io', + orgName: 'orgName', + path: 'path', + projectName: 'projectName', + refDashStr: 'branchName-simple', + refString: 'branchName/simple', + repoName: 'repoName', + branch: 'branchName/simple', + }); + }); + }); }); diff --git a/packages/integration/src/harness/core.ts b/packages/integration/src/harness/core.ts index 3a61905e27..7faa6b6d9c 100644 --- a/packages/integration/src/harness/core.ts +++ b/packages/integration/src/harness/core.ts @@ -33,36 +33,13 @@ export function getHarnessEditContentsUrl( config: HarnessIntegrationConfig, url: string, ) { - try { - const baseUrl = `https://${config.host}`; - const [ - _blank, - _ng, - _account, - accountId, - _module, - _moduleName, - _org, - orgName, - _projects, - projectName, - _repos, - repoName, - _files, - _ref, - _branch, - ...path - ] = url.replace(baseUrl, '').split('/'); - const pathWithoutSlash = path.join('/').replace(/^\//, ''); - return `${baseUrl}/gateway/code/api/v1/repos/${accountId}/${orgName}/${projectName}/${repoName}/+/edit/${pathWithoutSlash}`; - } catch (e) { - throw new Error(`Incorrect URL: ${url}, ${e}`); - } + const parsedUrl = parseHarnessUrl(config, url); + return `${parsedUrl.baseUrl}/ng/account/${parsedUrl.accountId}/module/code/orgs/${parsedUrl.orgName}/projects/${parsedUrl.projectName}/${parsedUrl.repoName}/files/${parsedUrl.branch}/~/${parsedUrl.path}`; } /** * Given a file path URL, - * it returns an API URL which returns the contents of the file. + * it returns an API URL which returns the contents of the file . * @remarks * * Converts @@ -77,35 +54,52 @@ export function getHarnessFileContentsUrl( config: HarnessIntegrationConfig, url: string, ) { - try { - const baseUrl = `https://${config.host}`; - const [ - _blank, - _ng, - _account, - accountId, - _module, - _moduleName, - _org, - orgName, - _projects, - projectName, - _repos, - repoName, - _files, - _ref, - _branch, - ...path - ] = url.replace(baseUrl, '').split('/'); - const urlParts = url.replace(baseUrl, '').split('/'); - const refAndPath = urlParts.slice(13); - const refIndex = refAndPath.findIndex(item => item === '~'); - const refString = refAndPath.slice(0, refIndex); - const pathWithoutSlash = path.join('/').replace(/^\//, ''); - return `${baseUrl}/gateway/code/api/v1/repos/${accountId}/${orgName}/${projectName}/${repoName}/+/raw/${pathWithoutSlash}?routingId=${accountId}&git_ref=${refString}`; - } catch (e) { - throw new Error(`Incorrect URL: ${url}, ${e}`); - } + const parsedUrl = parseHarnessUrl(config, url); + return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${parsedUrl.accountId}/${parsedUrl.orgName}/${parsedUrl.projectName}/${parsedUrl.repoName}/+/raw/${parsedUrl.path}?routingId=${parsedUrl.accountId}&git_ref=refs/heads/${parsedUrl.refString}`; +} + +/** + * Given a URL pointing to a repository/path, returns a URL + * for archive contents of the repository. + * + * @remarks + * + * Converts + * from: https://qa.harness.io/ng/account/accountId/module/code/orgs/orgId/projects/projectName/repos/repoName/files/branch/~/fileName + * to: https://qa.harness.io/gateway/code/api/v1/repos/accountId/orgId/projectName/repoName/+/archive/branch.zip?routingId=accountId + * + * @param url - A URL pointing to a repository/path + * @param config - The relevant provider config + * @public + */ +export function getHarnessArchiveUrl( + config: HarnessIntegrationConfig, + url: string, +) { + const parsedUrl = parseHarnessUrl(config, url); + return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${parsedUrl.accountId}/${parsedUrl.orgName}/${parsedUrl.projectName}/${parsedUrl.repoName}/+/archive/${parsedUrl.branch}.zip?routingId=${parsedUrl.accountId}`; +} + +/** + * Given a URL pointing to a repository branch, returns a URL + * for latest commit information. + * + * @remarks + * + * Converts + * from: https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projectName/repos/repoName/files/branchName + * to: https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projectName/repoName/+/content?routingId=accountId&include_commit=true&git_ref=refs/heads/branchName + * + * @param url - A URL pointing to a repository branch + * @param config - The relevant provider config + * @public + */ +export function getHarnessLatestCommitUrl( + config: HarnessIntegrationConfig, + url: string, +) { + const parsedUrl = parseHarnessUrl(config, url); + return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${parsedUrl.accountId}/${parsedUrl.orgName}/${parsedUrl.projectName}/${parsedUrl.repoName}/+/content?routingId=${parsedUrl.accountId}&include_commit=true&git_ref=refs/heads/${parsedUrl.branch}`; } /** @@ -130,3 +124,79 @@ export function getHarnessRequestOptions(config: HarnessIntegrationConfig): { headers, }; } + +/** + * Return parsed git url properties. + * + * @param config - A Harness provider config + * @param url - A URL pointing to a repository + * @public + */ +export function parseHarnessUrl( + config: HarnessIntegrationConfig, + url: string, +): { + baseUrl: string; + accountId: string; + orgName: string; + projectName: string; + refString: string; + repoName: string; + path: string; + refDashStr: string; + branch: string; +} { + const baseUrl = `https://${config.host}`; + try { + const pathUrl = new URL(url); + const pathSegments = pathUrl.pathname + .split('/') + .filter(segment => segment !== ''); + const urlParts = pathUrl.pathname.split('/'); + const [ + _ng, + _account, + accountId, + _module, + _moduleName, + _org, + orgName, + _projects, + projectName, + _repos, + repoName, + _files, + _ref, + _branch, + ..._path + ] = pathSegments; + const refAndPath = urlParts.slice( + urlParts.findIndex(i => i === 'files' || i === 'edit') + 1, + ); + const refIndex = refAndPath.findIndex(item => item === '~'); + const refString = refAndPath.slice(0, refIndex).join('/'); + const pathWithoutSlash = + refIndex !== -1 + ? refAndPath + .slice(refIndex + 1) + .join('/') + .replace(/^\//, '') + : ''; + return { + baseUrl: baseUrl, + accountId: accountId, + orgName: orgName, + projectName: projectName, + refString: refString, + path: pathWithoutSlash, + repoName: repoName, + refDashStr: refAndPath.slice(0, refIndex).join('-'), + branch: + refIndex !== -1 + ? refAndPath.slice(0, refIndex).join('/') + : refAndPath.join('/'), + }; + } catch (e) { + throw new Error(`Incorrect URL: ${url}, ${e}`); + } +} diff --git a/packages/integration/src/harness/index.ts b/packages/integration/src/harness/index.ts index 264df657f0..65cd73e49b 100644 --- a/packages/integration/src/harness/index.ts +++ b/packages/integration/src/harness/index.ts @@ -14,6 +14,12 @@ * limitations under the License. */ export { HarnessIntegration } from './HarnessIntegration'; -export { getHarnessRequestOptions, getHarnessFileContentsUrl } from './core'; +export { + getHarnessRequestOptions, + getHarnessFileContentsUrl, + getHarnessArchiveUrl, + getHarnessLatestCommitUrl, + parseHarnessUrl, +} from './core'; export { readHarnessConfig } from './config'; export type { HarnessIntegrationConfig } from './config'; diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index 251ddb28de..986a814a14 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/repo-tools +## 0.9.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-node@0.2.6-next.1 + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/config-loader@1.8.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.13 + - @backstage/errors@1.2.4 + +## 0.9.1-next.1 + +### Patch Changes + +- 8721a02: Add `--client-additional-properties` option to `openapi generate` command +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/cli-node@0.2.6-next.0 + - @backstage/config-loader@1.8.0 + +## 0.9.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-node@0.2.6-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config-loader@1.8.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.13 + - @backstage/errors@1.2.4 + ## 0.9.0 ### Minor Changes diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index 5c2e5e859c..9f83572bfb 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/repo-tools", "description": "CLI for Backstage repo tooling ", - "version": "0.9.0", + "version": "0.9.1-next.2", "publishConfig": { "access": "public" }, diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index 83266ea9f9..24ec2af411 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -54,6 +54,10 @@ function registerPackageCommand(program: Command) { .description( 'Command to generate a client and/or a server stub from an OpenAPI spec.', ) + .option('--client-additional-properties [properties]') + .description( + 'Additional properties that can be passed to @openapitools/openapi-generator-cli', + ) .action( lazy(() => import('./package/schema/openapi/generate').then(m => m.command), diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts index 6963e02664..b843cd27ec 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts @@ -27,12 +27,18 @@ import { exec } from '../../../../../lib/exec'; import { resolvePackagePath } from '@backstage/backend-plugin-api'; import { getPathToCurrentOpenApiSpec } from '../../../../../lib/openapi/helpers'; -async function generate(outputDirectory: string) { +async function generate( + outputDirectory: string, + clientAdditionalProperties?: string, +) { const resolvedOpenapiPath = await getPathToCurrentOpenApiSpec(); const resolvedOutputDirectory = cliPaths.resolveTargetRoot( outputDirectory, OUTPUT_PATH, ); + const additionalProperties = clientAdditionalProperties + ? `--additional-properties=${clientAdditionalProperties}` + : ''; mkdirpSync(resolvedOutputDirectory); await fs.mkdirp(resolvedOutputDirectory); @@ -60,6 +66,7 @@ async function generate(outputDirectory: string) { ), '--generator-key', 'v3.0', + additionalProperties, ], { maxBuffer: Number.MAX_VALUE, @@ -87,9 +94,12 @@ async function generate(outputDirectory: string) { }); } -export async function command(outputPackage: string): Promise { +export async function command( + outputPackage: string, + clientAdditionalProperties?: string, +): Promise { try { - await generate(outputPackage); + await generate(outputPackage, clientAdditionalProperties); console.log( chalk.green(`Generated client in ${outputPackage}/${OUTPUT_PATH}`), ); diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts index 1e48fe3825..41884e2e90 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts @@ -26,7 +26,7 @@ export async function command(opts: OptionValues) { process.exit(1); } if (opts.clientPackage) { - await generateClient(opts.clientPackage); + await generateClient(opts.clientPackage, opts.clientAdditionalProperties); } if (opts.server) { await generateServer(); diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 0bb564782c..d61570eed3 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,43 @@ # techdocs-cli-embedded-app +## 0.2.97-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/plugin-techdocs@1.10.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/plugin-catalog@1.21.0-next.2 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/cli@0.26.7-next.2 + - @backstage/app-defaults@1.5.6-next.1 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-techdocs-react@1.2.5-next.1 + - @backstage/test-utils@1.5.6-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6-next.0 + +## 0.2.97-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.6-next.0 + - @backstage/theme@0.5.6-next.0 + - @backstage/cli@0.26.6-next.0 + - @backstage/app-defaults@1.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/test-utils@1.5.6-next.0 + - @backstage/plugin-techdocs-react@1.2.5-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-plugin-api@1.9.2 + - @backstage/integration-react@1.1.27 + - @backstage/plugin-catalog@1.20.1-next.0 + ## 0.2.96 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index c6f776015c..9e7d0b9c04 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.96", + "version": "0.2.97-next.1", "private": true, "backstage": { "role": "frontend" diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index c1c15bb78c..5192493290 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,35 @@ # @techdocs/cli +## 1.8.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.5-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + +## 1.8.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-techdocs-node@1.12.5-next.1 + +## 1.8.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-techdocs-node@1.12.5-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + ## 1.8.11 ### Patch Changes diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 5af004aeb0..fe0d9e209f 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@techdocs/cli", "description": "Utility CLI for managing TechDocs sites in Backstage.", - "version": "1.8.11", + "version": "1.8.12-next.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 4c8e0c1173..3864b3549c 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/test-utils +## 1.5.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/plugin-permission-react@0.4.23-next.0 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + +## 1.5.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-plugin-api@1.9.2 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-permission-react@0.4.22 + ## 1.5.5 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index d48b652c43..5e61ff8cf4 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/test-utils", - "version": "1.5.5", + "version": "1.5.6-next.1", "description": "Utilities to test Backstage plugins and apps.", "backstage": { "role": "web-library" diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index 397b6c80f5..0cd38f9265 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/theme +## 0.5.6-next.0 + +### Patch Changes + +- 702fa7d: Internal refactor to fix an issue where the MUI 5 `v5-` class prefixing gets removed by tree shaking. + ## 0.5.4 ### Patch Changes diff --git a/packages/theme/package.json b/packages/theme/package.json index 698d4f567c..8cdb23c1ec 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/theme", - "version": "0.5.4", + "version": "0.5.6-next.0", "description": "material-ui theme for use with Backstage.", "backstage": { "role": "web-library" diff --git a/packages/yarn-plugin/.eslintrc.js b/packages/yarn-plugin/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/yarn-plugin/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/yarn-plugin/.gitignore b/packages/yarn-plugin/.gitignore new file mode 100644 index 0000000000..fba89407fd --- /dev/null +++ b/packages/yarn-plugin/.gitignore @@ -0,0 +1,2 @@ +# @yarnpkg/builder build output +bundles/ diff --git a/packages/yarn-plugin/README.md b/packages/yarn-plugin/README.md new file mode 100644 index 0000000000..16507713af --- /dev/null +++ b/packages/yarn-plugin/README.md @@ -0,0 +1,13 @@ +# yarn-plugin-backstage + +This yarn plugin adds a `backstage:` version protocol to yarn, which replaces +specific version ranges for `@backstage/` packages. The only version range +supported by this plugin is `backstage:^`, which has similar semantics to the +corresponding [`workspace` range](https://yarnpkg.com/features/workspaces#cross-references) when using +workspace dependencies; locally, the package will always resolve to the exact +version specified in the manifest for the Backstage release listed in +backstage.json. If the dependent package is published, this version will be +prefixed by `^`. + +**This plugin is still under active development, and requires some further +testing before we recommend it for general use.** diff --git a/packages/yarn-plugin/api-report.md b/packages/yarn-plugin/api-report.md new file mode 100644 index 0000000000..f0300ec9a1 --- /dev/null +++ b/packages/yarn-plugin/api-report.md @@ -0,0 +1,11 @@ +## API Report File for "yarn-plugin-backstage" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Plugin as Plugin_2 } from '@yarnpkg/core'; + +// @public (undocumented) +const plugin: Plugin_2; +export default plugin; +``` diff --git a/packages/yarn-plugin/catalog-info.yaml b/packages/yarn-plugin/catalog-info.yaml new file mode 100644 index 0000000000..c3a9be8fa5 --- /dev/null +++ b/packages/yarn-plugin/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: yarn-plugin-backstage + title: yarn-plugin-backstage + description: Yarn plugin for working with Backstage monorepos +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/packages/yarn-plugin/package.json b/packages/yarn-plugin/package.json new file mode 100644 index 0000000000..b6b97c0475 --- /dev/null +++ b/packages/yarn-plugin/package.json @@ -0,0 +1,45 @@ +{ + "name": "yarn-plugin-backstage", + "version": "0.0.0", + "description": "Yarn plugin for working with Backstage monorepos", + "backstage": { + "role": "node-library" + }, + "private": true, + "keywords": [ + "backstage" + ], + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/yarn-plugin" + }, + "license": "Apache-2.0", + "main": "./src/index.ts", + "scripts": { + "build": "builder build plugin", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "start": "nodemon --", + "test": "backstage-cli package test" + }, + "nodemonConfig": { + "exec": "builder build plugin", + "ext": "ts", + "watch": "./src" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/release-manifests": "workspace:^", + "@yarnpkg/core": "^4.0.3", + "@yarnpkg/fslib": "^3.0.2", + "semver": "^7.6.0" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@yarnpkg/builder": "^4.0.0", + "nodemon": "^3.0.1" + } +} diff --git a/packages/yarn-plugin/src/constants.ts b/packages/yarn-plugin/src/constants.ts new file mode 100644 index 0000000000..4473b1e347 --- /dev/null +++ b/packages/yarn-plugin/src/constants.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const PROTOCOL = 'backstage:'; diff --git a/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.test.ts b/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.test.ts new file mode 100644 index 0000000000..ca99b88437 --- /dev/null +++ b/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.test.ts @@ -0,0 +1,124 @@ +/* + * 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 { Manifest, Workspace } from '@yarnpkg/core'; +import { npath, ppath } from '@yarnpkg/fslib'; +import { createMockDirectory } from '@backstage/backend-test-utils'; + +import { beforeWorkspacePacking } from './beforeWorkspacePacking'; + +jest.mock('@backstage/release-manifests', () => ({ + getManifestByVersion: jest.fn().mockResolvedValue({ + releaseVersion: '1.23.45', + packages: [ + { + name: '@backstage/core', + version: '3.2.1', + }, + ], + }), +})); + +const makeWorkspace = (manifest: object) => { + return { + manifest: Manifest.fromText(JSON.stringify(manifest)), + } as Workspace; +}; + +describe('beforeWorkspacePacking', () => { + const mockDir = createMockDirectory(); + + beforeEach(() => { + jest + .spyOn(ppath, 'cwd') + .mockReturnValue(npath.toPortablePath(mockDir.path)); + + jest + .spyOn(process, 'cwd') + .mockReturnValue(npath.toPortablePath(mockDir.path)); + + mockDir.setContent({ + 'backstage.json': JSON.stringify({ + version: '1.23.45', + }), + 'package.json': JSON.stringify({ + workspaces: { + packages: ['packages/*'], + }, + }), + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe.each` + dependencyType + ${'dependencies'} + ${'devDependencies'} + ${'optionalDependencies'} + `('$dependencyType', ({ dependencyType }) => { + it(`ignores ${dependencyType} that don't use the backstage: protocol`, () => { + const result = { + name: 'test-package', + [dependencyType]: { + foo: '^1.1.1', + }, + }; + + beforeWorkspacePacking(makeWorkspace(result), result); + + expect(result).toEqual({ + name: 'test-package', + [dependencyType]: { + foo: '^1.1.1', + }, + }); + }); + + it(`throws an error for any backstage: versions with a selector other than ^`, async () => { + const result = { + name: 'test-package', + [dependencyType]: { + '@backstage/core': 'backstage:^1.1.1', + }, + }; + + await expect(() => + beforeWorkspacePacking(makeWorkspace(result), result), + ).rejects.toThrow(); + }); + + it('converts backstage:^ versions to the corresponding package version prefixed by ^', async () => { + const result = { + name: 'test-package', + [dependencyType]: { + '@backstage/core': 'backstage:^', + }, + }; + + await beforeWorkspacePacking(makeWorkspace(result), result); + + expect(result).toEqual({ + name: 'test-package', + [dependencyType]: { + '@backstage/core': '^3.2.1', + }, + }); + }); + }); +}); diff --git a/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.ts b/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.ts new file mode 100644 index 0000000000..b804f64226 --- /dev/null +++ b/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Descriptor, Workspace, structUtils } from '@yarnpkg/core'; +import { getCurrentBackstageVersion, getPackageVersion } from '../util'; +import { PROTOCOL } from '../constants'; + +const getFinalDependencyType = ( + dependencyType: string, + descriptor: Descriptor, + workspace: Workspace, +) => { + if (dependencyType !== 'dependencies') { + return dependencyType; + } + + return workspace.manifest.ensureDependencyMeta( + structUtils.makeDescriptor(descriptor, 'unknown'), + ).optional + ? 'optionalDependencies' + : dependencyType; +}; + +export const beforeWorkspacePacking = async ( + workspace: Workspace, + rawManifest: any, +) => { + const backstageVersion = getCurrentBackstageVersion(); + + for (const dependencyType of ['dependencies', 'devDependencies'] as const) { + const entries = Array.from( + workspace.manifest.getForScope(dependencyType).values(), + ).filter(descriptor => descriptor.range.startsWith(PROTOCOL)); + + for (const descriptor of entries) { + const ident = structUtils.stringifyIdent(descriptor); + const range = structUtils.parseRange(descriptor.range); + + if (range.selector !== '^') { + throw new Error( + `Unexpected version range "${descriptor.range}" for dependency on "${ident}"`, + ); + } + + const finalDependencyType = getFinalDependencyType( + dependencyType, + descriptor, + workspace, + ); + + rawManifest[finalDependencyType][ident] = `^${await getPackageVersion( + structUtils.makeDescriptor( + descriptor, + `${PROTOCOL}${backstageVersion}`, + ), + )}`; + } + } +}; diff --git a/packages/yarn-plugin/src/index.ts b/packages/yarn-plugin/src/index.ts new file mode 100644 index 0000000000..6c1c805fd8 --- /dev/null +++ b/packages/yarn-plugin/src/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Yarn plugin for resolving package versions based on + * a Backstage version manifest. + * + * @packageDocumentation + */ + +import { Plugin } from '@yarnpkg/core'; +import { beforeWorkspacePacking } from './handlers/beforeWorkspacePacking'; +import { BackstageResolver } from './resolver/BackstageResolver'; + +/** + * @public + */ +const plugin: Plugin = { + hooks: { + beforeWorkspacePacking, + }, + resolvers: [BackstageResolver], +}; + +export default plugin; diff --git a/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts b/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts new file mode 100644 index 0000000000..4cc0c1c9f2 --- /dev/null +++ b/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts @@ -0,0 +1,181 @@ +/* + * 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 { structUtils } from '@yarnpkg/core'; +import { npath, ppath } from '@yarnpkg/fslib'; +import { BackstageResolver } from './BackstageResolver'; +import { createMockDirectory } from '@backstage/backend-test-utils'; + +jest.mock('@backstage/release-manifests', () => ({ + getManifestByVersion: jest.fn().mockResolvedValue({ + releaseVersion: '1.23.45', + packages: [ + { + name: '@backstage/core', + version: '6.7.8', + }, + ], + }), +})); + +describe('BackstageResolver', () => { + const mockDir = createMockDirectory(); + let backstageResolver: BackstageResolver; + + beforeEach(() => { + jest + .spyOn(ppath, 'cwd') + .mockReturnValue(npath.toPortablePath(mockDir.path)); + + jest + .spyOn(process, 'cwd') + .mockReturnValue(npath.toPortablePath(mockDir.path)); + + mockDir.setContent({ + 'backstage.json': JSON.stringify({ + version: '1.23.45', + }), + 'package.json': JSON.stringify({ + workspaces: { + packages: ['packages/*'], + }, + }), + packages: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + dependencies: { + '@backstage/core': 'backstage:^', + }, + }), + }, + }, + }); + + backstageResolver = new BackstageResolver(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('supportsDescriptor', () => { + it.each([ + ['backstage:^'], + ['backstage:1.26.0-next.3'], + ['backstage:anything'], + ])('returns true for range "%s"', range => { + expect( + backstageResolver.supportsDescriptor( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + range, + ), + ), + ).toEqual(true); + }); + }); + + describe('bindDescriptor', () => { + describe('with range "backstage:^"', () => { + it('returns a descriptor basedwith a version range for the current Backstage version', () => { + expect( + backstageResolver.bindDescriptor( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:^', + ), + ), + ).toEqual( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:1.23.45', + ), + ); + }); + }); + + describe('with range "backstage:1.23.45"', () => { + it('throws an error', () => { + expect(() => + backstageResolver.bindDescriptor( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:1.23.45', + ), + ), + ).toThrow(/unsupported version range/i); + }); + }); + }); + + describe('getCandidates', () => { + it('returns an npm: descriptor based on the manifest for the appropriate backstage version', async () => { + const descriptor = structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:1.23.45', + ); + + await expect( + backstageResolver.getCandidates(descriptor), + ).resolves.toEqual([structUtils.makeLocator(descriptor, 'npm:6.7.8')]); + }); + + it('rejects descriptors not using the backstage: protocol', async () => { + await expect( + backstageResolver.getCandidates( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'npm:1.2.3', + ), + ), + ).rejects.toThrow(/unsupported version protocol/i); + }); + + it('rejects backstage: ranges with a ^ shorthand version', async () => { + await expect( + backstageResolver.getCandidates( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:^', + ), + ), + ).rejects.toThrow(/invalid backstage version/i); + }); + + it('rejects backstage: ranges with a * shorthand version', async () => { + await expect( + backstageResolver.getCandidates( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:*', + ), + ), + ).rejects.toThrow(/invalid backstage version/i); + }); + + it('rejects backstage: ranges with an invalid version specified', async () => { + await expect( + backstageResolver.getCandidates( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:latest', + ), + ), + ).rejects.toThrow(/invalid backstage version/i); + }); + }); +}); diff --git a/packages/yarn-plugin/src/resolver/BackstageResolver.ts b/packages/yarn-plugin/src/resolver/BackstageResolver.ts new file mode 100644 index 0000000000..33bba5df53 --- /dev/null +++ b/packages/yarn-plugin/src/resolver/BackstageResolver.ts @@ -0,0 +1,124 @@ +/* + * 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 { + structUtils, + Descriptor, + Locator, + Package, + Resolver, +} from '@yarnpkg/core'; +import semver from 'semver'; +import { PROTOCOL } from '../constants'; +import { getCurrentBackstageVersion, getPackageVersion } from '../util'; + +export class BackstageResolver implements Resolver { + static protocol = PROTOCOL; + + supportsDescriptor = (descriptor: Descriptor) => + descriptor.range.startsWith(BackstageResolver.protocol); + + shouldPersistResolution = () => true; + + /** + * Called for each dependency present in the dependency list of a package + * definition. If it returns a new descriptor, this new descriptor will be + * used. + * + * In this plugin, we convert the specific range "backstage:^" to + * "backstage:". This new range will be the one + * stored in lockfile entries, ensuring that we re-resolve the package when + * the version in backstage.json changes. + */ + bindDescriptor(descriptor: Descriptor): Descriptor { + if (descriptor.range !== 'backstage:^') { + throw new Error( + `Unsupported version range "${ + descriptor.range + }" for package ${structUtils.stringifyIdent( + descriptor, + )}. The backstage protocol only supports the range "backstage:^".`, + ); + } + + return structUtils.makeDescriptor( + descriptor, + `${PROTOCOL}${getCurrentBackstageVersion()}`, + ); + } + + /** + * Given a descriptor, return the list of locators that potentially satisfy + * it. The implementation in this plugin converts a `backstage:` range with a + * concrete version into the appropriate concrete npm version for that + * backstage release. + */ + async getCandidates(descriptor: Descriptor): Promise { + const range = structUtils.parseRange(descriptor.range); + if (range.protocol !== BackstageResolver.protocol) { + throw new Error( + `Unsupported version protocol in version range "${ + descriptor.range + }" for package ${structUtils.stringifyIdent(descriptor)}`, + ); + } + + if (!semver.valid(range.selector)) { + throw new Error( + `Invalid Backstage version string when resolving version for ${structUtils.stringifyIdent( + descriptor, + )}`, + ); + } + + return [ + structUtils.makeLocator( + descriptor, + `npm:${await getPackageVersion(descriptor)}`, + ), + ]; + } + + /** + * This plugin does not need to support any locators itself, since the + * `getCandidates` method will always convert `backstage:` versions into + * `npm:` versions which can be handled as usual. + */ + supportsLocator = () => false; + + /** + * This resolver never transforms the packages that are actually depended on, + * only replaces versions. As such there's never a need to add additional + * dependencies. + */ + getResolutionDependencies = () => ({}); + + /** + * Candidate versions produced by this resolver always use the `npm:` + * protocol, so this function will never be called. + */ + async getSatisfying(): Promise<{ locators: Locator[]; sorted: boolean }> { + throw new Error('Unreachable'); + } + + /** + * Once transformed into locators (through getCandidates), the versions are + * resolved by the NpmSemverResolver + */ + async resolve(): Promise { + throw new Error(`Unreachable`); + } +} diff --git a/packages/yarn-plugin/src/util.ts b/packages/yarn-plugin/src/util.ts new file mode 100644 index 0000000000..8431e23cfa --- /dev/null +++ b/packages/yarn-plugin/src/util.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ppath, xfs } from '@yarnpkg/fslib'; +import { valid as semverValid } from 'semver'; +import { getManifestByVersion } from '@backstage/release-manifests'; +import { BACKSTAGE_JSON, findPaths } from '@backstage/cli-common'; +import { Descriptor, structUtils } from '@yarnpkg/core'; +import { PROTOCOL } from './constants'; + +export const getCurrentBackstageVersion = () => { + const workspaceRoot = ppath.resolve(findPaths(ppath.cwd()).targetRoot); + + const backstageJson = xfs.readJsonSync( + ppath.join(workspaceRoot, BACKSTAGE_JSON), + ); + + const backstageVersion = semverValid(backstageJson.version); + + if (backstageVersion === null) { + throw new Error('Valid version string not found in backstage.json'); + } + + return backstageVersion; +}; + +export const getPackageVersion = async (descriptor: Descriptor) => { + const ident = structUtils.stringifyIdent(descriptor); + const range = structUtils.parseRange(descriptor.range); + + if (range.protocol !== PROTOCOL) { + throw new Error(`Unexpected ${range.protocol} range when packing`); + } + + if (!semverValid(range.selector)) { + throw new Error(`Missing backstage version in range ${descriptor.range}`); + } + + const manifest = await getManifestByVersion({ + version: range.selector, + }); + + const manifestEntry = manifest.packages.find( + candidate => candidate.name === ident, + ); + + if (!manifestEntry) { + throw new Error( + `Package ${ident} not found in manifest for Backstage v${range.selector}. ` + + `This means the specified package is not included in this Backstage ` + + `release. This may imply the package has been replaced with an alternative - ` + + `please review the documentation for the package. If you need to continue ` + + `using this package, it will be necessary to switch to manually managing its ` + + `version.`, + ); + } + + return manifestEntry.version; +}; diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 92eb1b44f1..8c6c348a34 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-api-docs +## 0.11.6-next.1 + +### Patch Changes + +- 7f84039: The `registerComponent` external route will now by default bind to the catalog import page if it is available. +- 9cdc651: Make sure that the toggle button state is properly reflected in API cards +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/plugin-catalog@1.21.0-next.2 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-permission-react@0.4.23-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-catalog-common@1.0.23 + +## 0.11.6-next.0 + +### Patch Changes + +- 96cd13e: `DefaultApiExplorerPage` now accepts an optional `ownerPickerMode` for toggling the behavior of the `EntityOwnerPicker`, exposing a new mode `` particularly suitable for larger catalogs. In this new mode, `EntityOwnerPicker` will display all the users and groups present in the catalog. +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/plugin-catalog@1.20.1-next.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-catalog-react@1.12.1-next.0 + - @backstage/plugin-permission-react@0.4.22 + ## 0.11.5 ### Patch Changes diff --git a/plugins/api-docs/api-report.md b/plugins/api-docs/api-report.md index 7523198298..2fc852777b 100644 --- a/plugins/api-docs/api-report.md +++ b/plugins/api-docs/api-report.md @@ -9,6 +9,7 @@ import { ApiEntity } from '@backstage/catalog-model'; import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CatalogTableRow } from '@backstage/plugin-catalog'; +import { EntityOwnerPickerProps } from '@backstage/plugin-catalog-react'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; import { JSX as JSX_2 } from 'react'; @@ -105,6 +106,7 @@ export type DefaultApiExplorerPageProps = { initiallySelectedFilter?: UserListFilterKind; columns?: TableColumn[]; actions?: TableProps['actions']; + ownerPickerMode?: EntityOwnerPickerProps['mode']; }; // @public (undocumented) diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 398407e33f..04965a8796 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.11.5", + "version": "0.11.6-next.1", "description": "A Backstage plugin that helps represent API entities in the frontend", "backstage": { "role": "frontend-plugin" diff --git a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx index dc83c2528d..6b5917c52d 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx @@ -35,6 +35,7 @@ import { UserListFilterKind, UserListPicker, CatalogFilterLayout, + EntityOwnerPickerProps, } from '@backstage/plugin-catalog-react'; import React from 'react'; import { registerComponentRouteRef } from '../../routes'; @@ -60,6 +61,7 @@ export type DefaultApiExplorerPageProps = { initiallySelectedFilter?: UserListFilterKind; columns?: TableColumn[]; actions?: TableProps['actions']; + ownerPickerMode?: EntityOwnerPickerProps['mode']; }; /** @@ -67,7 +69,12 @@ export type DefaultApiExplorerPageProps = { * @public */ export const DefaultApiExplorerPage = (props: DefaultApiExplorerPageProps) => { - const { initiallySelectedFilter = 'all', columns, actions } = props; + const { + initiallySelectedFilter = 'all', + columns, + actions, + ownerPickerMode, + } = props; const configApi = useApi(configApiRef); const generatedSubtitle = `${ @@ -101,7 +108,7 @@ export const DefaultApiExplorerPage = (props: DefaultApiExplorerPageProps) => { - + @@ -115,7 +140,11 @@ export const ContextMenu = (props: ContextMenuProps) => { diff --git a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.test.tsx b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.test.tsx index 9d3f29fe7b..70d2193470 100644 --- a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.test.tsx +++ b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.test.tsx @@ -16,10 +16,20 @@ import { OngoingTask } from './OngoingTask'; import React from 'react'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + renderInTestApp, + TestApiProvider, + MockPermissionApi, +} from '@backstage/test-utils'; import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import { act, fireEvent, waitFor, within } from '@testing-library/react'; +import { + PermissionApi, + permissionApiRef, +} from '@backstage/plugin-permission-react'; import { rootRouteRef } from '../../routes'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; +import { SWRConfig } from 'swr'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -49,18 +59,29 @@ describe('OngoingTask', () => { getTask: jest.fn().mockImplementation(async () => {}), }; - beforeEach(() => { + beforeEach(async () => { jest.clearAllMocks(); }); - it('should trigger cancel api on "Cancel" click in context menu', async () => { - const cancelOptionLabel = 'Cancel'; - const rendered = await renderInTestApp( - - - , + const render = (permissionApi?: PermissionApi) => { + // SWR used by the usePermission hook needs cache to be reset for each test + return renderInTestApp( + new Map() }}> + + + + , { mountedRoutes: { '/': rootRouteRef } }, ); + }; + it('should trigger cancel api on "Cancel" click in context menu', async () => { + const rendered = await render(); + const cancelOptionLabel = 'Cancel'; const { getByTestId } = rendered; await act(async () => { @@ -84,13 +105,9 @@ describe('OngoingTask', () => { }); it('should trigger cancel api on "Cancel" button click', async () => { + const rendered = await render(); const cancelOptionLabel = 'Cancel'; - const rendered = await renderInTestApp( - - - , - { mountedRoutes: { '/': rootRouteRef } }, - ); + const { getByTestId } = rendered; await act(async () => { @@ -114,22 +131,12 @@ describe('OngoingTask', () => { }); it('should initially do not display logs', async () => { - const rendered = await renderInTestApp( - - - , - { mountedRoutes: { '/': rootRouteRef } }, - ); + const rendered = await render(); await expect(rendered.findByText('Show Logs')).resolves.toBeInTheDocument(); }); it('should toggle logs visibility', async () => { - const rendered = await renderInTestApp( - - - , - { mountedRoutes: { '/': rootRouteRef } }, - ); + const rendered = await render(); await act(async () => { const element = await rendered.findByText('Show Logs'); fireEvent.click(element); @@ -137,4 +144,22 @@ describe('OngoingTask', () => { await expect(rendered.findByText('Hide Logs')).resolves.toBeInTheDocument(); }); + + it('should have cancel and start over buttons be disabled without the proper permissions', async () => { + const mockAuthorize = jest + .fn() + .mockImplementation(async () => ({ result: AuthorizeResult.DENY })); + const permissionApi: PermissionApi = { authorize: mockAuthorize }; + const rendered = await render(permissionApi); + + const { getByTestId } = rendered; + expect(getByTestId('cancel-button')).toHaveClass('Mui-disabled'); + expect(getByTestId('start-over-button')).toHaveClass('Mui-disabled'); + + await act(async () => { + fireEvent.click(getByTestId('menu-button')); + }); + expect(getByTestId('cancel-task')).toHaveClass('Mui-disabled'); + expect(getByTestId('start-over-task')).toHaveClass('Mui-disabled'); + }); }); diff --git a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx index f50b76e14a..f4099028ba 100644 --- a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx +++ b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx @@ -35,6 +35,12 @@ import { TaskSteps, } from '@backstage/plugin-scaffolder-react/alpha'; import { useAsync } from '@react-hookz/web'; +import { usePermission } from '@backstage/plugin-permission-react'; +import { + taskCancelPermission, + taskReadPermission, + taskCreatePermission, +} from '@backstage/plugin-scaffolder-common/alpha'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -82,6 +88,22 @@ export const OngoingTask = (props: { const [logsVisible, setLogVisibleState] = useState(false); const [buttonBarVisible, setButtonBarVisibleState] = useState(true); + // Used dummy string value for `resourceRef` since `allowed` field will always return `false` if `resourceRef` is `undefined` + const { allowed: canCancelTask } = usePermission({ + permission: taskCancelPermission, + }); + + const { allowed: canReadTask } = usePermission({ + permission: taskReadPermission, + }); + + const { allowed: canCreateTask } = usePermission({ + permission: taskCreatePermission, + }); + + // Start Over endpoint requires user to have both read (to grab parameters) and create (to create new task) permissions + const canStartOver = canReadTask && canCreateTask; + useEffect(() => { if (taskStream.error) { setLogVisibleState(true); @@ -197,7 +219,11 @@ export const OngoingTask = (props: {
diff --git a/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.tsx b/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.tsx index f8fc831976..4ea26b4414 100644 --- a/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.tsx @@ -115,7 +115,7 @@ export const MultiEntityPicker = (props: MultiEntityPickerProps) => { } // We need to check against formData here as that's the previous value for this field. - if (formData.includes(ref) || allowArbitraryValues) { + if (formData?.includes(ref) || allowArbitraryValues) { return entityRef; } } @@ -173,7 +173,7 @@ export const MultiEntityPicker = (props: MultiEntityPickerProps) => { required={required} InputProps={{ ...params.InputProps, - required: formData.length === 0 && required, + required: formData?.length === 0 && required, }} /> )} diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.test.tsx index eb77240d33..0380ac6ce8 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.test.tsx @@ -44,18 +44,18 @@ describe('AzureRepoPicker', () => { }); }); - describe('owner field', () => { - it('calls onChange when the owner changes', () => { + describe('project field', () => { + it('calls onChange when the project changes', () => { const onChange = jest.fn(); const { getAllByRole } = render( , ); - const ownerInput = getAllByRole('textbox')[1]; + const projectInput = getAllByRole('textbox')[1]; - fireEvent.change(ownerInput, { target: { value: 'owner' } }); + fireEvent.change(projectInput, { target: { value: 'project' } }); - expect(onChange).toHaveBeenCalledWith({ owner: 'owner' }); + expect(onChange).toHaveBeenCalledWith({ project: 'project' }); }); }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx index 83f013c8b4..833a0849fb 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/AzureRepoPicker.tsx @@ -24,14 +24,14 @@ import { Select, SelectItem } from '@backstage/core-components'; export const AzureRepoPicker = (props: { allowedOrganizations?: string[]; - allowedOwners?: string[]; + allowedProject?: string[]; rawErrors: string[]; state: RepoUrlPickerState; onChange: (state: RepoUrlPickerState) => void; }) => { const { allowedOrganizations = [], - allowedOwners = [], + allowedProject = [], rawErrors, state, onChange, @@ -41,11 +41,11 @@ export const AzureRepoPicker = (props: { ? allowedOrganizations.map(i => ({ label: i, value: i })) : [{ label: 'Loading...', value: 'loading' }]; - const ownerItems: SelectItem[] = allowedOwners - ? allowedOwners.map(i => ({ label: i, value: i })) + const projectItems: SelectItem[] = allowedProject + ? allowedProject.map(i => ({ label: i, value: i })) : [{ label: 'Loading...', value: 'loading' }]; - const { organization, owner } = state; + const { organization, project } = state; return ( <> @@ -82,26 +82,26 @@ export const AzureRepoPicker = (props: { 0 && !owner} + error={rawErrors?.length > 0 && !project} > - {allowedOwners?.length ? ( + {allowedProject?.length ? ( onChange({ owner: e.target.value })} - value={owner} + id="projectInput" + onChange={e => onChange({ project: e.target.value })} + value={project} /> )} diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx index 85a50fa7cb..1fa723c8c6 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx @@ -58,6 +58,10 @@ describe('RepoUrlPicker', () => { byHost: () => ({ type: 'github' }), }; + const mockIntegrationsApiAzure: Partial = { + byHost: () => ({ type: 'azure' }), + }; + let mockScmAuthApi: Partial; beforeEach(() => { @@ -111,7 +115,7 @@ describe('RepoUrlPicker', () => { const { getByRole } = await renderInTestApp( { ).toBeInTheDocument(); }); + it('should render properly with allowedProject', async () => { + const { getByRole } = await renderInTestApp( + + +
, + }} + /> + + , + ); + + expect(getByRole('option', { name: 'Backstage' })).toBeInTheDocument(); + }); + it('should render properly with title and description', async () => { const { getByText } = await renderInTestApp( { {hostType === 'azure' && ( { const range = new Range(); diff --git a/plugins/search-backend-module-catalog/CHANGELOG.md b/plugins/search-backend-module-catalog/CHANGELOG.md index d0c84a7617..beb7c7dc3c 100644 --- a/plugins/search-backend-module-catalog/CHANGELOG.md +++ b/plugins/search-backend-module-catalog/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-search-backend-module-catalog +## 0.1.25-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + +## 0.1.25-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + +## 0.1.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + ## 0.1.24 ### Patch Changes diff --git a/plugins/search-backend-module-catalog/package.json b/plugins/search-backend-module-catalog/package.json index f7eca60fbd..dea1865fca 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.1.24", + "version": "0.1.25-next.2", "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 5bf9aa19ea..350d8b3d5e 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 1.4.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-common@1.2.11 + +## 1.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + +## 1.4.2-next.0 + +### Patch Changes + +- 6a576dc: Replace the usage of `getVoidLogger` with `mockServices.logger.mock` in order to remove the dependency with the soon-to-be-deprecated `backend-common` package. +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-common@1.2.11 + ## 1.4.1 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 128bc0e28c..3994977fd2 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.4.1", + "version": "1.4.2-next.2", "description": "A module for the search backend that implements search using ElasticSearch", "backstage": { "role": "backend-plugin-module" @@ -62,6 +62,7 @@ }, "devDependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@elastic/elasticsearch-mock": "^1.0.0", "@short.io/opensearch-mock": "^0.3.1" diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts index 602a158b98..3f64e0b768 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { errors } from '@elastic/elasticsearch'; import Mock from '@elastic/elasticsearch-mock'; @@ -26,6 +25,7 @@ import { encodePageCursor, } from './ElasticSearchSearchEngine'; import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineIndexer'; +import { mockServices } from '@backstage/backend-test-utils'; jest.mock('uuid', () => ({ v4: () => 'tag' })); @@ -86,14 +86,14 @@ describe('ElasticSearchSearchEngine', () => { options, 'search', '', - getVoidLogger(), + mockServices.logger.mock(), 1000, ); inspectableSearchEngine = new ElasticSearchSearchEngineForTranslatorTests( options, 'search', '', - getVoidLogger(), + mockServices.logger.mock(), 1000, ); // eslint-disable-next-line dot-notation @@ -915,7 +915,7 @@ describe('ElasticSearchSearchEngine', () => { const getOptional = jest.spyOn(config, 'getOptional'); await ElasticSearchSearchEngine.fromConfig({ - logger: getVoidLogger(), + logger: mockServices.logger.mock(), config, }); @@ -940,7 +940,7 @@ describe('ElasticSearchSearchEngine', () => { expect( async () => await ElasticSearchSearchEngine.fromConfig({ - logger: getVoidLogger(), + logger: mockServices.logger.mock(), config, }), ).not.toThrow(); 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 453a825574..44d33c27a5 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { TestPipeline } from '@backstage/plugin-search-backend-node'; import Mock from '@elastic/elasticsearch-mock'; import { range } from 'lodash'; import { ElasticSearchClientWrapper } from './ElasticSearchClientWrapper'; import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineIndexer'; +import { mockServices } from '@backstage/backend-test-utils'; const mock = new Mock(); const clientWrapper = ElasticSearchClientWrapper.fromClientOptions({ @@ -43,7 +43,7 @@ describe('ElasticSearchSearchEngineIndexer', () => { indexPrefix: '', indexSeparator: '-index__', alias: 'some-type-index__search', - logger: getVoidLogger(), + logger: mockServices.logger.mock(), elasticSearchClientWrapper: clientWrapper, batchSize: 1000, skipRefresh: false, @@ -270,7 +270,7 @@ describe('ElasticSearchSearchEngineIndexer', () => { indexPrefix: '', indexSeparator: '-index__', alias: 'some-type-index__search', - logger: getVoidLogger(), + logger: mockServices.logger.mock(), elasticSearchClientWrapper: mockClientWrapper, batchSize: 1000, skipRefresh: false, @@ -292,7 +292,7 @@ describe('ElasticSearchSearchEngineIndexer', () => { indexPrefix: '', indexSeparator: '-index__', alias: 'some-type-index__search', - logger: getVoidLogger(), + logger: mockServices.logger.mock(), elasticSearchClientWrapper: clientWrapper, batchSize: 1000, skipRefresh: true, diff --git a/plugins/search-backend-module-explore/CHANGELOG.md b/plugins/search-backend-module-explore/CHANGELOG.md index fc8ff4b307..4ca395300b 100644 --- a/plugins/search-backend-module-explore/CHANGELOG.md +++ b/plugins/search-backend-module-explore/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-search-backend-module-explore +## 0.1.25-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/config@1.2.0 + - @backstage/plugin-search-common@1.2.11 + +## 0.1.25-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + +## 0.1.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-search-common@1.2.11 + ## 0.1.24 ### Patch Changes diff --git a/plugins/search-backend-module-explore/package.json b/plugins/search-backend-module-explore/package.json index 28ba6945c3..a195eb08d9 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.1.24", + "version": "0.1.25-next.2", "description": "A module for the search backend that exports explore modules", "backstage": { "role": "backend-plugin-module" diff --git a/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.test.ts b/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.test.ts index 498a4d9fbf..82305d0d86 100644 --- a/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.test.ts +++ b/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.test.ts @@ -14,19 +14,21 @@ * limitations under the License. */ import { - getVoidLogger, PluginEndpointDiscovery, TokenManager, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { TestPipeline } from '@backstage/plugin-search-backend-node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + setupRequestMockHandlers, +} from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { Readable } from 'stream'; import { ToolDocumentCollatorFactory } from './ToolDocumentCollatorFactory'; -const logger = getVoidLogger(); +const logger = mockServices.logger.mock(); const mockTools = { tools: [ diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index ba6674606d..afd7d5d5b6 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-search-backend-module-pg +## 0.5.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-app-api@0.7.6-next.2 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/config@1.2.0 + - @backstage/plugin-search-common@1.2.11 + +## 0.5.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + +## 0.5.28-next.0 + +### Patch Changes + +- 6a576dc: Replace the usage of `getVoidLogger` with `mockServices.logger.mock` in order to remove the dependency with the soon-to-be-deprecated `backend-common` package. +- Updated dependencies + - @backstage/backend-app-api@0.7.6-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-search-common@1.2.11 + ## 0.5.27 ### Patch Changes diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 6645e2642f..f81eef8fc7 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.27", + "version": "0.5.28-next.2", "description": "A module for the search backend that implements search using PostgreSQL", "backstage": { "role": "backend-plugin-module" @@ -47,6 +47,7 @@ "test": "backstage-cli package test" }, "dependencies": { + "@backstage/backend-app-api": "workspace:^", "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts index 9fe0e7091c..ab5ae8be54 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; import { IndexableDocument } from '@backstage/plugin-search-common'; import { Knex } from 'knex'; @@ -31,7 +30,7 @@ export type PgSearchEngineIndexerOptions = { /** @public */ export class PgSearchEngineIndexer extends BatchSearchEngineIndexer { - private logger: Logger; + private logger?: Logger; private store: DatabaseStore; private type: string; private tx: Knex.Transaction | undefined; @@ -41,7 +40,7 @@ export class PgSearchEngineIndexer extends BatchSearchEngineIndexer { super({ batchSize: options.batchSize }); this.store = options.databaseStore; this.type = options.type; - this.logger = options.logger || getVoidLogger(); + this.logger = options.logger; } async initialize(): Promise { @@ -60,7 +59,7 @@ export class PgSearchEngineIndexer extends BatchSearchEngineIndexer { this.numRecords += documents.length; const refs = [...new Set(documents.map(d => d.authorization?.resourceRef))]; - this.logger.debug( + this.logger?.debug( `Attempting to index the following entities: ${refs.toString()}`, ); @@ -79,7 +78,7 @@ export class PgSearchEngineIndexer extends BatchSearchEngineIndexer { // and do not continue. This ensures that collators that return empty sets // of documents do not cause the index to be deleted. if (this.numRecords === 0) { - this.logger.warn( + this.logger?.warn( `Index for ${this.type} was not replaced: indexer received 0 documents`, ); this.tx!.rollback!(); diff --git a/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md b/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md index c56c40ebe4..63f008a851 100644 --- a/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md +++ b/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-search-backend-module-stack-overflow-collator +## 0.1.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/config@1.2.0 + - @backstage/plugin-search-common@1.2.11 + +## 0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-search-common@1.2.11 + ## 0.1.11 ### 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 5ae74d37bc..443cfe1155 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.1.11", + "version": "0.1.12-next.2", "description": "A module for the search backend that exports stack overflow modules", "backstage": { "role": "backend-plugin-module" diff --git a/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts b/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts index 5d2f65923c..f4d4a77c3c 100644 --- a/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts +++ b/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts @@ -13,19 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { StackOverflowQuestionsCollatorFactory, StackOverflowQuestionsCollatorFactoryOptions, } from './StackOverflowQuestionsCollatorFactory'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + setupRequestMockHandlers, +} from '@backstage/backend-test-utils'; import { TestPipeline } from '@backstage/plugin-search-backend-node'; import { ConfigReader } from '@backstage/config'; import { Readable } from 'stream'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; -const logger = getVoidLogger(); +const logger = mockServices.logger.mock(); const mockQuestion = { items: [ diff --git a/plugins/search-backend-module-techdocs/CHANGELOG.md b/plugins/search-backend-module-techdocs/CHANGELOG.md index 569a73c232..e2d7c21867 100644 --- a/plugins/search-backend-module-techdocs/CHANGELOG.md +++ b/plugins/search-backend-module-techdocs/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/plugin-search-backend-module-techdocs +## 0.1.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.5-next.2 + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/plugin-catalog-node@1.12.1-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + +## 0.1.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + - @backstage/plugin-techdocs-node@1.12.5-next.1 + +## 0.1.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-techdocs-node@1.12.5-next.0 + - @backstage/plugin-catalog-node@1.12.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + ## 0.1.23 ### Patch Changes diff --git a/plugins/search-backend-module-techdocs/package.json b/plugins/search-backend-module-techdocs/package.json index c27fe52d93..8690558f8a 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.1.23", + "version": "0.1.24-next.2", "description": "A module for the search backend that exports techdocs modules", "backstage": { "role": "backend-plugin-module" diff --git a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts index 8f56a92096..26329e5ed6 100644 --- a/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts +++ b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts @@ -14,14 +14,16 @@ * limitations under the License. */ import { - getVoidLogger, PluginEndpointDiscovery, TokenManager, } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { TestPipeline } from '@backstage/plugin-search-backend-node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + setupRequestMockHandlers, +} from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { Readable } from 'stream'; @@ -29,7 +31,7 @@ import { DefaultTechDocsCollatorFactory } from './DefaultTechDocsCollatorFactory import { defaultTechDocsCollatorEntityTransformer } from './defaultTechDocsCollatorEntityTransformer'; import { TechDocsCollatorEntityTransformer } from './TechDocsCollatorEntityTransformer'; -const logger = getVoidLogger(); +const logger = mockServices.logger.mock(); const mockSearchDocIndex = { config: { @@ -89,8 +91,8 @@ describe('DefaultTechDocsCollatorFactory', () => { authenticate: jest.fn(), }; const options = { + logger, discovery: mockDiscoveryApi, - logger: getVoidLogger(), tokenManager: mockTokenManager, }; diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index 74ba4c86c1..c0cc9e1c0e 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-search-backend-node +## 1.2.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-tasks@0.5.24-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + +## 1.2.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.1 + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + +## 1.2.24-next.0 + +### Patch Changes + +- 6a576dc: Replace the usage of `getVoidLogger` with `mockServices.logger.mock` in order to remove the dependency with the soon-to-be-deprecated `backend-common` package. +- 5b6f979: Split backend search plugin startup into "init" and "start" stages to ensure necessary initialization has happened before startup +- Updated dependencies + - @backstage/backend-tasks@0.5.24-next.0 + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + ## 1.2.22 ### Patch Changes diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index a3c9e16f95..36a6a5de13 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.2.22", + "version": "1.2.24-next.2", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", "backstage": { "role": "node-library" @@ -60,6 +60,7 @@ }, "devDependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/ndjson": "^2.0.1" } diff --git a/plugins/search-backend-node/src/IndexBuilder.test.ts b/plugins/search-backend-node/src/IndexBuilder.test.ts index 70d00f6974..6796c7c01e 100644 --- a/plugins/search-backend-node/src/IndexBuilder.test.ts +++ b/plugins/search-backend-node/src/IndexBuilder.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks'; import { DocumentCollatorFactory, @@ -23,6 +22,7 @@ import { import { Readable, Transform } from 'stream'; import { IndexBuilder } from './IndexBuilder'; import { LunrSearchEngine, SearchEngine } from './index'; +import { mockServices } from '@backstage/backend-test-utils'; class TestDocumentCollatorFactory implements DocumentCollatorFactory { readonly type: string = 'anything'; @@ -57,7 +57,7 @@ describe('IndexBuilder', () => { let testScheduledTaskRunner: TaskRunner; beforeEach(() => { - const logger = getVoidLogger(); + const logger = mockServices.logger.mock(); testScheduledTaskRunner = { run: async (task: TaskInvocationDefinition & { fn: () => void }) => { task.fn(); diff --git a/plugins/search-backend-node/src/Scheduler.test.ts b/plugins/search-backend-node/src/Scheduler.test.ts index 6daae3013d..f7e68988d2 100644 --- a/plugins/search-backend-node/src/Scheduler.test.ts +++ b/plugins/search-backend-node/src/Scheduler.test.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { Scheduler } from './index'; +import { mockServices } from '@backstage/backend-test-utils'; describe('Scheduler', () => { let testScheduler: Scheduler; beforeEach(() => { - const logger = getVoidLogger(); + const logger = mockServices.logger.mock(); testScheduler = new Scheduler({ logger, }); diff --git a/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.test.ts b/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.test.ts index addcb87497..45cc98e9ba 100644 --- a/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.test.ts +++ b/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.test.ts @@ -15,7 +15,6 @@ */ import { - getVoidLogger, ReadUrlResponse, UrlReader, UrlReaders, @@ -24,10 +23,11 @@ import { ConfigReader } from '@backstage/config'; import { Readable } from 'stream'; import { NewlineDelimitedJsonCollatorFactory } from './NewlineDelimitedJsonCollatorFactory'; import { TestPipeline } from '../test-utils'; +import { mockServices } from '@backstage/backend-test-utils'; describe('DefaultCatalogCollatorFactory', () => { const config = new ConfigReader({}); - const logger = getVoidLogger(); + const logger = mockServices.logger.mock(); it('has expected type', () => { const factory = NewlineDelimitedJsonCollatorFactory.fromConfig(config, { diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts index e5347b3cb1..407fa779ab 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import lunr from 'lunr'; import { IndexableDocument } from '@backstage/plugin-search-common'; import { @@ -27,6 +26,7 @@ import { import { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer'; import { SearchEngine } from '../types'; import { TestPipeline } from '../test-utils'; +import { mockServices } from '@backstage/backend-test-utils'; /** * Just used to test the default translator shipped with LunrSearchEngine. @@ -77,7 +77,9 @@ describe('LunrSearchEngine', () => { let testLunrSearchEngine: SearchEngine; beforeEach(() => { - testLunrSearchEngine = new LunrSearchEngine({ logger: getVoidLogger() }); + testLunrSearchEngine = new LunrSearchEngine({ + logger: mockServices.logger.mock(), + }); jest.clearAllMocks(); }); @@ -107,7 +109,7 @@ describe('LunrSearchEngine', () => { it('should return translated query', async () => { const inspectableSearchEngine = new LunrSearchEngineForTests({ - logger: getVoidLogger(), + logger: mockServices.logger.mock(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -149,7 +151,7 @@ describe('LunrSearchEngine', () => { it('should have default offset and limit', async () => { const inspectableSearchEngine = new LunrSearchEngineForTests({ - logger: getVoidLogger(), + logger: mockServices.logger.mock(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -190,7 +192,7 @@ describe('LunrSearchEngine', () => { it('should return translated query with 1 filter', async () => { const inspectableSearchEngine = new LunrSearchEngineForTests({ - logger: getVoidLogger(), + logger: mockServices.logger.mock(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -235,7 +237,7 @@ describe('LunrSearchEngine', () => { it('should handle single-item array filter as scalar value', async () => { const inspectableSearchEngine = new LunrSearchEngineForTests({ - logger: getVoidLogger(), + logger: mockServices.logger.mock(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -266,7 +268,7 @@ describe('LunrSearchEngine', () => { it('should return translated query with multiple filters', async () => { const inspectableSearchEngine = new LunrSearchEngineForTests({ - logger: getVoidLogger(), + logger: mockServices.logger.mock(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -315,7 +317,7 @@ describe('LunrSearchEngine', () => { it('should throw if translated query references missing field', async () => { const inspectableSearchEngine = new LunrSearchEngineForTests({ - logger: getVoidLogger(), + logger: mockServices.logger.mock(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -477,7 +479,7 @@ describe('LunrSearchEngine', () => { it('should perform search query and return highlight metadata on match', async () => { const inspectableSearchEngine = new LunrSearchEngineForTests({ - logger: getVoidLogger(), + logger: mockServices.logger.mock(), }); const mockDocuments = [ @@ -1012,7 +1014,7 @@ describe('LunrSearchEngine', () => { // Set up an inspectable search engine to pre-set some data. const inspectableSearchEngine = new LunrSearchEngineForTests({ - logger: getVoidLogger(), + logger: mockServices.logger.mock(), }); inspectableSearchEngine.setDocStore({ 'existing-location': doc }); @@ -1047,7 +1049,7 @@ describe('LunrSearchEngine', () => { // Set up an inspectable search engine to pre-set some data. const doc = { title: 'A doc', text: 'test', location: 'some-location' }; const inspectableSearchEngine = new LunrSearchEngineForTests({ - logger: getVoidLogger(), + logger: mockServices.logger.mock(), }); inspectableSearchEngine.setDocStore({ 'existing-location': doc }); @@ -1073,7 +1075,7 @@ describe('LunrSearchEngine', () => { // Set up an inspectable search engine to pre-set some data. const doc = { title: 'A doc', text: 'test', location: 'some-location' }; const inspectableSearchEngine = new LunrSearchEngineForTests({ - logger: getVoidLogger(), + logger: mockServices.logger.mock(), }); inspectableSearchEngine.setDocStore({ 'existing-location': doc }); @@ -1178,7 +1180,9 @@ describe('stopword testing', () => { let testLunrSearchEngine: SearchEngine; beforeEach(() => { - testLunrSearchEngine = new LunrSearchEngine({ logger: getVoidLogger() }); + testLunrSearchEngine = new LunrSearchEngine({ + logger: mockServices.logger.mock(), + }); jest.clearAllMocks(); }); diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index 0a80301ede..c51185b2a3 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-search-backend +## 1.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/backend-defaults@0.3.0-next.2 + - @backstage/plugin-permission-node@0.7.30-next.2 + - @backstage/backend-openapi-utils@0.1.12-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + +## 1.5.10-next.1 + +### Patch Changes + +- 34dc47d: Move @backstage/repo-tools to devDependencies +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/plugin-permission-node@0.7.30-next.1 + - @backstage/backend-defaults@0.3.0-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-search-backend-node@1.2.24-next.1 + +## 1.5.10-next.0 + +### Patch Changes + +- 8869b8e: Updated local development setup. +- 5b6f979: Split backend search plugin startup into "init" and "start" stages to ensure necessary initialization has happened before startup +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-search-backend-node@1.2.24-next.0 + - @backstage/backend-defaults@0.2.19-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/repo-tools@0.9.1-next.0 + - @backstage/plugin-permission-node@0.7.30-next.0 + - @backstage/backend-openapi-utils@0.1.12-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.13 + - @backstage/plugin-search-common@1.2.11 + ## 1.5.8 ### Patch Changes diff --git a/plugins/search-backend/dev/index.ts b/plugins/search-backend/dev/index.ts new file mode 100644 index 0000000000..9c108c0de1 --- /dev/null +++ b/plugins/search-backend/dev/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); +backend.add(import('../src/alpha')); +backend.start(); diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 146e50d177..4a550de16f 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend", - "version": "1.5.8", + "version": "1.5.10-next.2", "description": "The Backstage backend plugin that provides your backstage app with search", "backstage": { "role": "backend-plugin" @@ -49,6 +49,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-defaults": "workspace:^", "@backstage/backend-openapi-utils": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", @@ -57,7 +58,6 @@ "@backstage/plugin-permission-node": "workspace:^", "@backstage/plugin-search-backend-node": "workspace:^", "@backstage/plugin-search-common": "workspace:^", - "@backstage/repo-tools": "workspace:^", "@backstage/types": "workspace:^", "@types/express": "^4.17.6", "dataloader": "^2.0.0", @@ -71,6 +71,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/repo-tools": "workspace:^", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/search-backend/src/service/router.test.ts b/plugins/search-backend/src/service/router.test.ts index 59f2f95afe..81b45bc7e6 100644 --- a/plugins/search-backend/src/service/router.test.ts +++ b/plugins/search-backend/src/service/router.test.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - PluginEndpointDiscovery, - getVoidLogger, -} from '@backstage/backend-common'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { @@ -55,7 +52,7 @@ describe('createRouter', () => { }; beforeAll(async () => { - const logger = getVoidLogger(); + const logger = mockServices.logger.mock(); mockSearchEngine = { getIndexer: jest.fn(), setTranslator: jest.fn(), @@ -259,7 +256,7 @@ describe('createRouter', () => { describe('search result filtering', () => { beforeAll(async () => { - const logger = getVoidLogger(); + const logger = mockServices.logger.mock(); mockSearchEngine = { getIndexer: jest.fn(), setTranslator: jest.fn(), diff --git a/plugins/search-backend/src/service/standaloneServer.ts b/plugins/search-backend/src/service/standaloneServer.ts deleted file mode 100644 index 09fa4f87ee..0000000000 --- a/plugins/search-backend/src/service/standaloneServer.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - createServiceBuilder, - loadBackendConfig, - ServerTokenManager, - HostDiscovery, -} from '@backstage/backend-common'; -import { Server } from 'http'; -import { Logger } from 'winston'; -import { createRouter } from './router'; -import { - LunrSearchEngine, - IndexBuilder, -} from '@backstage/plugin-search-backend-node'; -import { ServerPermissionClient } from '@backstage/plugin-permission-node'; - -export interface ServerOptions { - port: number; - enableCors: boolean; - logger: Logger; -} - -export async function startStandaloneServer( - options: ServerOptions, -): Promise { - const logger = options.logger.child({ service: 'search-backend' }); - const config = await loadBackendConfig({ logger, argv: process.argv }); - const searchEngine = new LunrSearchEngine({ logger }); - const indexBuilder = new IndexBuilder({ logger, searchEngine }); - const discovery = HostDiscovery.fromConfig(config); - const tokenManager = ServerTokenManager.fromConfig(config, { - logger, - }); - const permissions = ServerPermissionClient.fromConfig(config, { - discovery, - tokenManager, - }); - logger.debug('Starting application server...'); - - // TODO: stub out some documents/indices? - - const router = await createRouter({ - engine: indexBuilder.getSearchEngine(), - types: indexBuilder.getDocumentTypes(), - discovery, - permissions, - config, - logger, - }); - - let service = createServiceBuilder(module) - .setPort(options.port) - .addRouter('/search', router); - if (options.enableCors) { - service = service.enableCors({ origin: 'http://localhost:3000' }); - } - - return await service.start().catch(err => { - logger.error(err); - process.exit(1); - }); -} - -module.hot?.accept(); diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md index 046532facf..ab153126eb 100644 --- a/plugins/search-react/CHANGELOG.md +++ b/plugins/search-react/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-search-react +## 1.7.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/theme@0.5.6-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-search-common@1.2.11 + +## 1.7.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-search-common@1.2.11 + ## 1.7.11 ### Patch Changes diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 1e6b2b0e8e..b3fe4f0dc0 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-react", - "version": "1.7.11", + "version": "1.7.12-next.1", "backstage": { "role": "web-library" }, diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index b738d853f8..d1bcaa6c97 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-search +## 1.4.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-search-react@1.7.12-next.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-search-common@1.2.11 + +## 1.4.12-next.1 + +### Patch Changes + +- 4f92394: Migrate from identityApi to fetchApi in frontend plugins. +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.1-next.0 + +## 1.4.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/plugin-search-react@1.7.12-next.0 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-catalog-react@1.12.1-next.0 + - @backstage/plugin-search-common@1.2.11 + ## 1.4.11 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index 1a9a09a5fe..46167d484c 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "1.4.11", + "version": "1.4.12-next.2", "description": "The Backstage plugin that provides your backstage app with search", "backstage": { "role": "frontend-plugin" diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index 9fffa41bfb..5c85d097d4 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -32,9 +32,9 @@ import { import { useApi, DiscoveryApi, - IdentityApi, discoveryApiRef, identityApiRef, + FetchApi, } from '@backstage/core-plugin-api'; import { @@ -81,12 +81,12 @@ export const searchApi = createApiExtension({ api: searchApiRef, deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, factory: ({ - identityApi, + fetchApi, discoveryApi, }: { - identityApi: IdentityApi; + fetchApi: FetchApi; discoveryApi: DiscoveryApi; - }) => new SearchClient({ discoveryApi, identityApi }), + }) => new SearchClient({ discoveryApi, fetchApi }), }, }); diff --git a/plugins/search/src/apis.test.ts b/plugins/search/src/apis.test.ts index 7996ad3fa3..15745d2e68 100644 --- a/plugins/search/src/apis.test.ts +++ b/plugins/search/src/apis.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { MockFetchApi } from '@backstage/test-utils'; import { SearchClient } from './apis'; describe('apis', () => { @@ -26,47 +27,35 @@ describe('apis', () => { const baseUrl = 'https://base-url.com/'; const getBaseUrl = jest.fn().mockResolvedValue(baseUrl); - const token = 'AUTHTOKEN'; - const withToken = jest.fn().mockResolvedValue({ token }); - const withoutToken = jest.fn().mockResolvedValue({ token: undefined }); - const createIdentityApiMock = (getCredentials: any) => ({ - signOut: jest.fn(), + const identityApi = { + getCredentials: jest.fn(), getProfileInfo: jest.fn(), getBackstageIdentity: jest.fn(), - getCredentials, + signOut: jest.fn(), + }; + const json = jest.fn(); + const mockFetch = jest.fn().mockResolvedValue({ + ok: true, + json, + }); + const fetchApi = new MockFetchApi({ + baseImplementation: mockFetch, + injectIdentityAuth: { identityApi }, }); const client = new SearchClient({ discoveryApi: { getBaseUrl }, - identityApi: createIdentityApiMock(withoutToken), - }); - - const json = jest.fn(); - const originalFetch = window.fetch; - window.fetch = jest.fn().mockResolvedValue({ json, ok: true }); - - afterAll(() => { - window.fetch = originalFetch; + fetchApi, }); it('Fetch is called with expected URL (including stringified Q params)', async () => { + identityApi.getCredentials.mockResolvedValue({}); await client.query(query); expect(getBaseUrl).toHaveBeenLastCalledWith('search'); - expect(fetch).toHaveBeenLastCalledWith(`${baseUrl}/query?term=`, { - headers: {}, - }); - }); - - it('Sets Authorization if token is available', async () => { - const authedClient = new SearchClient({ - discoveryApi: { getBaseUrl }, - identityApi: createIdentityApiMock(withToken), - }); - await authedClient.query(query); - expect(getBaseUrl).toHaveBeenLastCalledWith('search'); - expect(fetch).toHaveBeenLastCalledWith(`${baseUrl}/query?term=`, { - headers: { Authorization: `Bearer ${token}` }, - }); + expect(mockFetch).toHaveBeenLastCalledWith( + `${baseUrl}/query?term=`, + undefined, + ); }); it('Resolves JSON from fetch response', async () => { diff --git a/plugins/search/src/apis.ts b/plugins/search/src/apis.ts index eb5f47a65c..b56d88a22c 100644 --- a/plugins/search/src/apis.ts +++ b/plugins/search/src/apis.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; +import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; import { ResponseError } from '@backstage/errors'; import { SearchApi } from '@backstage/plugin-search-react'; import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common'; @@ -23,25 +23,19 @@ import qs from 'qs'; export class SearchClient implements SearchApi { private readonly discoveryApi: DiscoveryApi; - private readonly identityApi: IdentityApi; + private readonly fetchApi: FetchApi; - constructor(options: { - discoveryApi: DiscoveryApi; - identityApi: IdentityApi; - }) { + constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi }) { this.discoveryApi = options.discoveryApi; - this.identityApi = options.identityApi; + this.fetchApi = options.fetchApi; } async query(query: SearchQuery): Promise { - const { token } = await this.identityApi.getCredentials(); const queryString = qs.stringify(query); const url = `${await this.discoveryApi.getBaseUrl( 'search', )}/query?${queryString}`; - const response = await fetch(url, { - headers: token ? { Authorization: `Bearer ${token}` } : {}, - }); + const response = await this.fetchApi.fetch(url); if (!response.ok) { throw await ResponseError.fromResponse(response); diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index 07a645f73e..1adfd2e684 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -23,7 +23,7 @@ import { createRoutableExtension, discoveryApiRef, createComponentExtension, - identityApiRef, + fetchApiRef, } from '@backstage/core-plugin-api'; export const rootRouteRef = createRouteRef({ @@ -38,9 +38,9 @@ export const searchPlugin = createPlugin({ apis: [ createApiFactory({ api: searchApiRef, - deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, - factory: ({ discoveryApi, identityApi }) => { - return new SearchClient({ discoveryApi, identityApi }); + deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef }, + factory: ({ discoveryApi, fetchApi }) => { + return new SearchClient({ discoveryApi, fetchApi }); }, }), ], diff --git a/plugins/signals-backend/CHANGELOG.md b/plugins/signals-backend/CHANGELOG.md index f6eeac9bf4..ecb0b6905b 100644 --- a/plugins/signals-backend/CHANGELOG.md +++ b/plugins/signals-backend/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/plugin-signals-backend +## 0.1.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/plugin-signals-node@0.1.5-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## 0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/plugin-signals-node@0.1.5-next.1 + +## 0.1.5-next.0 + +### Patch Changes + +- 6a576dc: Replace the usage of `getVoidLogger` with `mockServices.logger.mock` in order to remove the dependency with the soon-to-be-deprecated `backend-common` package. +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/plugin-signals-node@0.1.5-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.1.4 ### Patch Changes diff --git a/plugins/signals-backend/package.json b/plugins/signals-backend/package.json index b148d4d5e5..9b9429cd8e 100644 --- a/plugins/signals-backend/package.json +++ b/plugins/signals-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals-backend", - "version": "0.1.4", + "version": "0.1.5-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -46,6 +46,7 @@ }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^", diff --git a/plugins/signals-backend/src/service/SignalManager.test.ts b/plugins/signals-backend/src/service/SignalManager.test.ts index 7d3a6ddf59..eccc715d1b 100644 --- a/plugins/signals-backend/src/service/SignalManager.test.ts +++ b/plugins/signals-backend/src/service/SignalManager.test.ts @@ -16,8 +16,8 @@ import { WebSocket } from 'ws'; import { EventsServiceSubscribeOptions } from '@backstage/plugin-events-node'; import { SignalManager } from './SignalManager'; -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; +import { mockServices } from '@backstage/backend-test-utils'; class MockWebSocket { closed: boolean = false; @@ -76,7 +76,7 @@ describe('SignalManager', () => { const manager = SignalManager.create({ events: mockEvents, - logger: getVoidLogger(), + logger: mockServices.logger.mock(), config: new ConfigReader({}), lifecycle: mockLifecycle as any, }); diff --git a/plugins/signals-backend/src/service/router.test.ts b/plugins/signals-backend/src/service/router.test.ts index c081031c54..000afec23c 100644 --- a/plugins/signals-backend/src/service/router.test.ts +++ b/plugins/signals-backend/src/service/router.test.ts @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - getVoidLogger, - PluginEndpointDiscovery, -} from '@backstage/backend-common'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; import express from 'express'; import request from 'supertest'; @@ -25,6 +22,7 @@ import { EventsService } from '@backstage/plugin-events-node'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { UserInfoService } from '@backstage/backend-plugin-api'; import { ConfigReader } from '@backstage/config'; +import { mockServices } from '@backstage/backend-test-utils'; const eventsServiceMock: jest.Mocked = { subscribe: jest.fn(), @@ -49,7 +47,7 @@ describe('createRouter', () => { beforeAll(async () => { const router = await createRouter({ - logger: getVoidLogger(), + logger: mockServices.logger.mock(), identity: identityApiMock, events: eventsServiceMock, discovery, diff --git a/plugins/signals-node/CHANGELOG.md b/plugins/signals-node/CHANGELOG.md index e26991c787..1e033a75e2 100644 --- a/plugins/signals-node/CHANGELOG.md +++ b/plugins/signals-node/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-signals-node +## 0.1.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/plugin-events-node@0.3.5-next.1 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## 0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + - @backstage/plugin-events-node@0.3.5-next.0 + +## 0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/plugin-events-node@0.3.5-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.1.4 ### Patch Changes diff --git a/plugins/signals-node/package.json b/plugins/signals-node/package.json index 4d1ad9e1ef..7541d19ff8 100644 --- a/plugins/signals-node/package.json +++ b/plugins/signals-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-signals-node", "description": "Node.js library for the signals plugin", - "version": "0.1.4", + "version": "0.1.5-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/signals-react/CHANGELOG.md b/plugins/signals-react/CHANGELOG.md index 72f0bc4084..b013dfeb84 100644 --- a/plugins/signals-react/CHANGELOG.md +++ b/plugins/signals-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-signals-react +## 0.0.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/types@1.1.1 + ## 0.0.3 ### Patch Changes diff --git a/plugins/signals-react/package.json b/plugins/signals-react/package.json index 4b36dddd5c..2812ef6aa8 100644 --- a/plugins/signals-react/package.json +++ b/plugins/signals-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals-react", - "version": "0.0.3", + "version": "0.0.4-next.0", "description": "Web library for the signals plugin", "backstage": { "role": "web-library" diff --git a/plugins/signals/CHANGELOG.md b/plugins/signals/CHANGELOG.md index 971a849a03..fb60992916 100644 --- a/plugins/signals/CHANGELOG.md +++ b/plugins/signals/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-signals +## 0.0.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/plugin-signals-react@0.0.4-next.0 + - @backstage/theme@0.5.6-next.0 + - @backstage/types@1.1.1 + +## 0.0.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.3 + ## 0.0.6 ### Patch Changes diff --git a/plugins/signals/package.json b/plugins/signals/package.json index ec618415a8..d313593271 100644 --- a/plugins/signals/package.json +++ b/plugins/signals/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals", - "version": "0.0.6", + "version": "0.0.7-next.1", "backstage": { "role": "frontend-plugin" }, diff --git a/plugins/techdocs-addons-test-utils/CHANGELOG.md b/plugins/techdocs-addons-test-utils/CHANGELOG.md index 8a4b3d1eb8..5838a42270 100644 --- a/plugins/techdocs-addons-test-utils/CHANGELOG.md +++ b/plugins/techdocs-addons-test-utils/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-techdocs-addons-test-utils +## 1.0.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/plugin-catalog@1.21.0-next.2 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-search-react@1.7.12-next.1 + - @backstage/plugin-techdocs-react@1.2.5-next.1 + - @backstage/test-utils@1.5.6-next.1 + +## 1.0.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.6-next.0 + - @backstage/test-utils@1.5.6-next.0 + - @backstage/plugin-search-react@1.7.12-next.0 + - @backstage/plugin-techdocs-react@1.2.5-next.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-plugin-api@1.9.2 + - @backstage/integration-react@1.1.27 + - @backstage/plugin-catalog@1.20.1-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.0 + ## 1.0.32 ### Patch Changes diff --git a/plugins/techdocs-addons-test-utils/package.json b/plugins/techdocs-addons-test-utils/package.json index 02161159e9..8fe3c2a942 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.32", + "version": "1.0.33-next.1", "backstage": { "role": "web-library" }, diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index d4d18976b0..f201caa389 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/plugin-techdocs-backend +## 1.10.6-next.2 + +### Patch Changes + +- e64bfb2: Allow defining custom build log transport for techdocs builder +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.5-next.2 + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + +## 1.10.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.1 + - @backstage/plugin-techdocs-node@1.12.5-next.1 + +## 1.10.6-next.0 + +### Patch Changes + +- 8869b8e: Updated local development setup. +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.24-next.0 + - @backstage/plugin-techdocs-node@1.12.5-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + - @backstage/plugin-catalog-common@1.0.23 + - @backstage/plugin-permission-common@0.7.13 + ## 1.10.5 ### Patch Changes diff --git a/plugins/techdocs-backend/dev/index.ts b/plugins/techdocs-backend/dev/index.ts new file mode 100644 index 0000000000..43c75d24df --- /dev/null +++ b/plugins/techdocs-backend/dev/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); +backend.add(import('../src/alpha')); +backend.start(); diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index a5a8e0ba76..c6773b1f64 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "1.10.5", + "version": "1.10.6-next.2", "description": "The Backstage backend plugin that renders technical documentation for your components", "backstage": { "role": "backend-plugin" @@ -73,6 +73,7 @@ "winston": "^3.2.1" }, "devDependencies": { + "@backstage/backend-defaults": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/dockerode": "^3.3.0", diff --git a/plugins/techdocs-backend/src/cache/TechDocsCache.test.ts b/plugins/techdocs-backend/src/cache/TechDocsCache.test.ts index fa4c81682c..7dff7069db 100644 --- a/plugins/techdocs-backend/src/cache/TechDocsCache.test.ts +++ b/plugins/techdocs-backend/src/cache/TechDocsCache.test.ts @@ -14,9 +14,10 @@ * limitations under the License. */ -import { CacheClient, getVoidLogger } from '@backstage/backend-common'; +import { CacheClient, loggerToWinstonLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { CacheInvalidationError, TechDocsCache } from './TechDocsCache'; +import { mockServices } from '@backstage/backend-test-utils'; const cached = (str: string): string => { return Buffer.from(str).toString('base64'); @@ -35,7 +36,7 @@ describe('TechDocsCache', () => { }; CacheUnderTest = TechDocsCache.fromConfig(new ConfigReader({}), { cache: MockClient, - logger: getVoidLogger(), + logger: loggerToWinstonLogger(mockServices.logger.mock()), }); }); @@ -82,7 +83,7 @@ describe('TechDocsCache', () => { }), { cache: MockClient, - logger: getVoidLogger(), + logger: loggerToWinstonLogger(mockServices.logger.mock()), }, ); diff --git a/plugins/techdocs-backend/src/cache/cacheMiddleware.test.ts b/plugins/techdocs-backend/src/cache/cacheMiddleware.test.ts index 4dd993a163..1d814c3caa 100644 --- a/plugins/techdocs-backend/src/cache/cacheMiddleware.test.ts +++ b/plugins/techdocs-backend/src/cache/cacheMiddleware.test.ts @@ -14,11 +14,12 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; +import { loggerToWinstonLogger } from '@backstage/backend-common'; import express from 'express'; import request from 'supertest'; import { createCacheMiddleware } from './cacheMiddleware'; import { TechDocsCache } from './TechDocsCache'; +import { mockServices } from '@backstage/backend-test-utils'; /** * Mocks cached HTTP response. @@ -57,7 +58,7 @@ describe('createCacheMiddleware', () => { invalidateMultiple: jest.fn().mockResolvedValue(undefined), } as unknown as jest.Mocked; const router = await createCacheMiddleware({ - logger: getVoidLogger(), + logger: loggerToWinstonLogger(mockServices.logger.mock()), cache, }); app = express().use(router); diff --git a/plugins/techdocs-backend/src/plugin.ts b/plugins/techdocs-backend/src/plugin.ts index 6f670abfa9..094335a0f0 100644 --- a/plugins/techdocs-backend/src/plugin.ts +++ b/plugins/techdocs-backend/src/plugin.ts @@ -38,6 +38,7 @@ import { } from '@backstage/plugin-techdocs-node'; import Docker from 'dockerode'; import { createRouter } from '@backstage/plugin-techdocs-backend'; +import * as winston from 'winston'; /** * The TechDocs plugin is responsible for serving and building documentation for any entity. @@ -47,6 +48,7 @@ export const techdocsPlugin = createBackendPlugin({ pluginId: 'techdocs', register(env) { let docsBuildStrategy: DocsBuildStrategy | undefined; + let buildLogTransport: winston.transport | undefined; env.registerExtensionPoint(techdocsBuildsExtensionPoint, { setBuildStrategy(buildStrategy: DocsBuildStrategy) { if (docsBuildStrategy) { @@ -54,6 +56,12 @@ export const techdocsPlugin = createBackendPlugin({ } docsBuildStrategy = buildStrategy; }, + setBuildLogTransport(transport: winston.transport) { + if (buildLogTransport) { + throw new Error('BuildLogTransport may only be set once'); + } + buildLogTransport = transport; + }, }); let customTechdocsGenerator: TechdocsGenerator | undefined; @@ -138,6 +146,7 @@ export const techdocsPlugin = createBackendPlugin({ logger: winstonLogger, cache: cacheManager, docsBuildStrategy, + buildLogTransport, preparers, generators, publisher, diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts index 3f1a857c4d..a0fc908ee5 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts @@ -16,17 +16,20 @@ import { PluginEndpointDiscovery, - getVoidLogger, TokenManager, + loggerToWinstonLogger, } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { DefaultTechDocsCollator } from './DefaultTechDocsCollator'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { + mockServices, + setupRequestMockHandlers, +} from '@backstage/backend-test-utils'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { ConfigReader } from '@backstage/config'; -const logger = getVoidLogger(); +const logger = loggerToWinstonLogger(mockServices.logger.mock()); const mockSearchDocIndex = { config: { diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts index ff714eee63..bb66c303b7 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts @@ -15,7 +15,7 @@ */ import { - getVoidLogger, + loggerToWinstonLogger, PluginEndpointDiscovery, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; @@ -30,6 +30,7 @@ import * as winston from 'winston'; import { TechDocsCache } from '../cache'; import { DocsBuilder, shouldCheckForUpdate } from '../DocsBuilder'; import { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer'; +import { mockServices } from '@backstage/backend-test-utils'; jest.mock('../DocsBuilder'); jest.useFakeTimers(); @@ -97,7 +98,7 @@ describe('DocsSynchronizer', () => { docsSynchronizer = new DocsSynchronizer({ publisher, config: new ConfigReader({}), - logger: getVoidLogger(), + logger: loggerToWinstonLogger(mockServices.logger.mock()), buildLogTransport: mockBuildLogTransport, scmIntegrations: ScmIntegrations.fromConfig(new ConfigReader({})), cache, @@ -346,7 +347,7 @@ describe('DocsSynchronizer', () => { config: new ConfigReader({ techdocs: { legacyUseCaseSensitiveTripletPaths: true }, }), - logger: getVoidLogger(), + logger: loggerToWinstonLogger(mockServices.logger.mock()), buildLogTransport: new winston.transports.Stream({ stream: new PassThrough(), }), diff --git a/plugins/techdocs-backend/src/service/router.test.ts b/plugins/techdocs-backend/src/service/router.test.ts index 224a87cb43..72da3023b0 100644 --- a/plugins/techdocs-backend/src/service/router.test.ts +++ b/plugins/techdocs-backend/src/service/router.test.ts @@ -16,7 +16,7 @@ import { errorHandler, - getVoidLogger, + loggerToWinstonLogger, PluginCacheManager, PluginEndpointDiscovery, } from '@backstage/backend-common'; @@ -33,6 +33,7 @@ import { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer'; import { CachedEntityLoader } from './CachedEntityLoader'; import { createEventStream, createRouter, RouterOptions } from './router'; import { TechDocsCache } from '../cache'; +import { mockServices } from '@backstage/backend-test-utils'; jest.mock('@backstage/catalog-client'); jest.mock('@backstage/config'); @@ -126,7 +127,7 @@ describe('createRouter', () => { generators, publisher, config: new ConfigReader({}), - logger: getVoidLogger(), + logger: loggerToWinstonLogger(mockServices.logger.mock()), discovery, cache, docsBuildStrategy, @@ -134,7 +135,7 @@ describe('createRouter', () => { const recommendedOptions = { publisher, config: new ConfigReader({}), - logger: getVoidLogger(), + logger: loggerToWinstonLogger(mockServices.logger.mock()), discovery, cache, docsBuildStrategy, diff --git a/plugins/techdocs-backend/src/service/standaloneServer.ts b/plugins/techdocs-backend/src/service/standaloneServer.ts deleted file mode 100644 index ce981633ca..0000000000 --- a/plugins/techdocs-backend/src/service/standaloneServer.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - CacheManager, - createServiceBuilder, - DockerContainerRunner, - HostDiscovery, - UrlReader, -} from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; -import { - DirectoryPreparer, - Generators, - Preparers, - Publisher, - TechdocsGenerator, -} from '@backstage/plugin-techdocs-node'; -import Docker from 'dockerode'; -import { Server } from 'http'; -import { Logger } from 'winston'; -import { createRouter } from './router'; - -export interface ServerOptions { - port: number; - enableCors: boolean; - logger: Logger; -} - -export async function startStandaloneServer( - options: ServerOptions, -): Promise { - const logger = options.logger.child({ service: 'techdocs-backend' }); - const config = new ConfigReader({ - techdocs: { - publisher: { - type: 'local', - }, - }, - }); - const discovery = HostDiscovery.fromConfig(config); - const mockUrlReader: jest.Mocked = { - readUrl: jest.fn(), - readTree: jest.fn(), - search: jest.fn(), - }; - - logger.debug('Creating application...'); - const preparers = new Preparers(); - const directoryPreparer = DirectoryPreparer.fromConfig(config, { - logger, - reader: mockUrlReader, - }); - preparers.register('dir', directoryPreparer); - - const dockerClient = new Docker(); - const containerRunner = new DockerContainerRunner({ dockerClient }); - - const generators = new Generators(); - const techdocsGenerator = TechdocsGenerator.fromConfig(config, { - logger, - containerRunner, - }); - generators.register('techdocs', techdocsGenerator); - - const publisher = await Publisher.fromConfig(config, { logger, discovery }); - - const cache = CacheManager.fromConfig(config).forPlugin('techdocs'); - - logger.debug('Starting application server...'); - const router = await createRouter({ - preparers, - generators, - logger, - publisher, - config, - discovery, - cache, - }); - let service = createServiceBuilder(module) - .setPort(options.port) - .addRouter('/techdocs', router); - if (options.enableCors) { - service = service.enableCors({ origin: 'http://localhost:3000' }); - } - return await service.start().catch(err => { - logger.error(err); - process.exit(1); - }); -} - -module.hot?.accept(); diff --git a/plugins/techdocs-module-addons-contrib/CHANGELOG.md b/plugins/techdocs-module-addons-contrib/CHANGELOG.md index 2ab0cef022..72efbff1af 100644 --- a/plugins/techdocs-module-addons-contrib/CHANGELOG.md +++ b/plugins/techdocs-module-addons-contrib/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-techdocs-module-addons-contrib +## 1.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/integration@1.12.0-next.0 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-techdocs-react@1.2.5-next.1 + +## 1.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/plugin-techdocs-react@1.2.5-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/integration@1.11.0 + - @backstage/integration-react@1.1.27 + ## 1.1.10 ### Patch Changes diff --git a/plugins/techdocs-module-addons-contrib/package.json b/plugins/techdocs-module-addons-contrib/package.json index 2aaca2c4e0..8b0e76168a 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.10", + "version": "1.1.11-next.1", "description": "Plugin module for contributed TechDocs Addons", "backstage": { "role": "frontend-plugin-module" diff --git a/plugins/techdocs-module-addons-contrib/src/TextSize/TextSize.test.tsx b/plugins/techdocs-module-addons-contrib/src/TextSize/TextSize.test.tsx index 081c718c2f..21478f0636 100644 --- a/plugins/techdocs-module-addons-contrib/src/TextSize/TextSize.test.tsx +++ b/plugins/techdocs-module-addons-contrib/src/TextSize/TextSize.test.tsx @@ -15,11 +15,8 @@ */ import { TechDocsAddonTester } from '@backstage/plugin-techdocs-addons-test-utils'; - import React from 'react'; - -import { fireEvent, waitFor } from '@testing-library/react'; - +import { fireEvent, waitFor, act } from '@testing-library/react'; import { TextSize } from '../plugin'; describe('TextSize', () => { @@ -47,7 +44,9 @@ describe('TextSize', () => { const slider = getByRole('slider'); - slider.focus(); + act(() => { + slider.focus(); + }); fireEvent.keyDown(slider, { key: 'ArrowRight', diff --git a/plugins/techdocs-module-addons-contrib/src/setupTests.ts b/plugins/techdocs-module-addons-contrib/src/setupTests.ts index 228079bc19..680ab8237b 100644 --- a/plugins/techdocs-module-addons-contrib/src/setupTests.ts +++ b/plugins/techdocs-module-addons-contrib/src/setupTests.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import '@testing-library/jest-dom'; Element.prototype.scrollIntoView = jest.fn(); @@ -41,3 +42,5 @@ global.BroadcastChannel = jest close: jest.fn(), }; }); + +window.scroll = jest.fn(); diff --git a/plugins/techdocs-node/CHANGELOG.md b/plugins/techdocs-node/CHANGELOG.md index 59a0cc2055..5242fa8ec6 100644 --- a/plugins/techdocs-node/CHANGELOG.md +++ b/plugins/techdocs-node/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-techdocs-node +## 1.12.5-next.2 + +### Patch Changes + +- e64bfb2: Allow defining custom build log transport for techdocs builder +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/integration@1.12.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-common@1.2.11 + +## 1.12.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + +## 1.12.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.11.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-common@1.2.11 + ## 1.12.4 ### Patch Changes diff --git a/plugins/techdocs-node/api-report.md b/plugins/techdocs-node/api-report.md index 4e79ee712c..2d3ca90983 100644 --- a/plugins/techdocs-node/api-report.md +++ b/plugins/techdocs-node/api-report.md @@ -16,6 +16,7 @@ import { Logger } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { UrlReader } from '@backstage/backend-common'; +import * as winston from 'winston'; import { Writable } from 'stream'; // @public @@ -240,6 +241,8 @@ export type SupportedGeneratorKey = 'techdocs' | string; // @public export interface TechdocsBuildsExtensionPoint { + // (undocumented) + setBuildLogTransport(transport: winston.transport): void; // (undocumented) setBuildStrategy(buildStrategy: DocsBuildStrategy): void; } diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index 7d4c83545d..21bbc89f4d 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-node", - "version": "1.12.4", + "version": "1.12.5-next.2", "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-node/src/extensions.ts b/plugins/techdocs-node/src/extensions.ts index ed292a3b17..8e7c9ef244 100644 --- a/plugins/techdocs-node/src/extensions.ts +++ b/plugins/techdocs-node/src/extensions.ts @@ -16,6 +16,7 @@ import { createExtensionPoint } from '@backstage/backend-plugin-api'; import { DocsBuildStrategy } from './techdocsTypes'; import { PreparerBase, RemoteProtocol, TechdocsGenerator } from './stages'; +import * as winston from 'winston'; /** * Extension point type for configuring Techdocs builds. @@ -24,6 +25,7 @@ import { PreparerBase, RemoteProtocol, TechdocsGenerator } from './stages'; */ export interface TechdocsBuildsExtensionPoint { setBuildStrategy(buildStrategy: DocsBuildStrategy): void; + setBuildLogTransport(transport: winston.transport): void; } /** diff --git a/plugins/techdocs-node/src/stages/generate/generators.test.ts b/plugins/techdocs-node/src/stages/generate/generators.test.ts index a1c585e795..3e0571af54 100644 --- a/plugins/techdocs-node/src/stages/generate/generators.test.ts +++ b/plugins/techdocs-node/src/stages/generate/generators.test.ts @@ -14,12 +14,16 @@ * limitations under the License. */ -import { ContainerRunner, getVoidLogger } from '@backstage/backend-common'; +import { + ContainerRunner, + loggerToWinstonLogger, +} from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { Generators } from './generators'; import { TechdocsGenerator } from './techdocs'; +import { mockServices } from '@backstage/backend-test-utils'; -const logger = getVoidLogger(); +const logger = loggerToWinstonLogger(mockServices.logger.mock()); const mockEntity = { apiVersion: 'version', diff --git a/plugins/techdocs-node/src/stages/generate/helpers.test.ts b/plugins/techdocs-node/src/stages/generate/helpers.test.ts index 1b47cfb05c..3589b71c60 100644 --- a/plugins/techdocs-node/src/stages/generate/helpers.test.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.test.ts @@ -14,10 +14,12 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; -import { createMockDirectory } from '@backstage/backend-test-utils'; +import { + createMockDirectory, + mockServices, +} from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import path, { resolve as resolvePath } from 'path'; import { ParsedLocationAnnotation } from '../../helpers'; @@ -35,6 +37,7 @@ import { patchMkdocsYmlWithPlugins, } from './mkdocsPatchers'; import yaml from 'js-yaml'; +import { loggerToWinstonLogger } from '@backstage/backend-common'; const mockEntity = { apiVersion: 'version', @@ -90,7 +93,7 @@ const mkdocsYmlWithAdditionalPluginsWithConfig = fs.readFileSync( const mkdocsYmlWithEnvTag = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_with_env_tag.yml'), ); -const mockLogger = getVoidLogger(); +const mockLogger = loggerToWinstonLogger(mockServices.logger.mock()); const warn = jest.spyOn(mockLogger, 'warn'); const scmIntegrations = ScmIntegrations.fromConfig(new ConfigReader({})); diff --git a/plugins/techdocs-node/src/stages/prepare/dir.test.ts b/plugins/techdocs-node/src/stages/prepare/dir.test.ts index 164cfde687..5fbd56264e 100644 --- a/plugins/techdocs-node/src/stages/prepare/dir.test.ts +++ b/plugins/techdocs-node/src/stages/prepare/dir.test.ts @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getVoidLogger, UrlReader } from '@backstage/backend-common'; +import { UrlReader, loggerToWinstonLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { DirectoryPreparer } from './dir'; +import { mockServices } from '@backstage/backend-test-utils'; function normalizePath(path: string) { return path @@ -28,7 +29,7 @@ jest.mock('../../helpers', () => ({ ...jest.requireActual<{}>('../../helpers'), })); -const logger = getVoidLogger(); +const logger = loggerToWinstonLogger(mockServices.logger.mock()); const createMockEntity = (annotations: {}) => { return { diff --git a/plugins/techdocs-node/src/stages/publish/awsS3.test.ts b/plugins/techdocs-node/src/stages/publish/awsS3.test.ts index 9a9c9b4542..e43b303dd4 100644 --- a/plugins/techdocs-node/src/stages/publish/awsS3.test.ts +++ b/plugins/techdocs-node/src/stages/publish/awsS3.test.ts @@ -24,7 +24,6 @@ import { S3Client, UploadPartCommand, } from '@aws-sdk/client-s3'; -import { getVoidLogger } from '@backstage/backend-common'; import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { @@ -39,7 +38,11 @@ import path from 'path'; import fs from 'fs-extra'; import { AwsS3Publish } from './awsS3'; import { Readable } from 'stream'; -import { createMockDirectory } from '@backstage/backend-test-utils'; +import { + createMockDirectory, + mockServices, +} from '@backstage/backend-test-utils'; +import { loggerToWinstonLogger } from '@backstage/backend-common'; const env = process.env; let s3Mock: AwsClientStub; @@ -87,7 +90,7 @@ class ErrorReadable extends Readable { } } -const logger = getVoidLogger(); +const logger = loggerToWinstonLogger(mockServices.logger.mock()); const loggerInfoSpy = jest.spyOn(logger, 'info'); const loggerErrorSpy = jest.spyOn(logger, 'error'); diff --git a/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts b/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts index 50e0863f3c..6cbfa76c81 100644 --- a/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts +++ b/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; +import { loggerToWinstonLogger } from '@backstage/backend-common'; import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import express from 'express'; @@ -27,7 +27,10 @@ import { BlobUploadCommonResponse, ContainerGetPropertiesResponse, } from '@azure/storage-blob'; -import { createMockDirectory } from '@backstage/backend-test-utils'; +import { + createMockDirectory, + mockServices, +} from '@backstage/backend-test-utils'; const mockDir = createMockDirectory(); @@ -221,7 +224,7 @@ const getEntityRootDir = (entity: Entity) => { return mockDir.resolve(namespace || DEFAULT_NAMESPACE, kind, name); }; -const logger = getVoidLogger(); +const logger = loggerToWinstonLogger(mockServices.logger.mock()); jest.spyOn(logger, 'error').mockReturnValue(logger); const createPublisherFromConfig = ({ diff --git a/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts b/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts index 17bfb3e3c4..0c94a69d6a 100644 --- a/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts +++ b/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; +import { loggerToWinstonLogger } from '@backstage/backend-common'; import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import express from 'express'; @@ -23,7 +23,10 @@ import path from 'path'; import fs from 'fs-extra'; import { Readable } from 'stream'; import { GoogleGCSPublish } from './googleStorage'; -import { createMockDirectory } from '@backstage/backend-test-utils'; +import { + createMockDirectory, + mockServices, +} from '@backstage/backend-test-utils'; const mockDir = createMockDirectory(); @@ -136,7 +139,7 @@ const getEntityRootDir = (entity: Entity) => { return mockDir.resolve(namespace || DEFAULT_NAMESPACE, kind, name); }; -const logger = getVoidLogger(); +const logger = loggerToWinstonLogger(mockServices.logger.mock()); jest.spyOn(logger, 'info').mockReturnValue(logger); jest.spyOn(logger, 'error').mockReturnValue(logger); diff --git a/plugins/techdocs-node/src/stages/publish/local.test.ts b/plugins/techdocs-node/src/stages/publish/local.test.ts index 89a38c7ed2..e5e71c51ec 100644 --- a/plugins/techdocs-node/src/stages/publish/local.test.ts +++ b/plugins/techdocs-node/src/stages/publish/local.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { - getVoidLogger, + loggerToWinstonLogger, PluginEndpointDiscovery, } from '@backstage/backend-common'; import { overridePackagePathResolution } from '@backstage/backend-plugin-api/testUtils'; @@ -24,7 +24,10 @@ import request from 'supertest'; import * as os from 'os'; import { LocalPublish } from './local'; import path from 'path'; -import { createMockDirectory } from '@backstage/backend-test-utils'; +import { + createMockDirectory, + mockServices, +} from '@backstage/backend-test-utils'; const createMockEntity = (annotations = {}, lowerCase = false) => { return { @@ -53,7 +56,7 @@ overridePackagePathResolution({ }, }); -const logger = getVoidLogger(); +const logger = loggerToWinstonLogger(mockServices.logger.mock()); describe('local publisher', () => { const mockDir = createMockDirectory(); diff --git a/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts b/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts index 93fe54ac27..aa0e5f0afa 100644 --- a/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts +++ b/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; +import { loggerToWinstonLogger } from '@backstage/backend-common'; import { Entity, CompoundEntityRef, @@ -28,7 +28,10 @@ import path from 'path'; import { OpenStackSwiftPublish } from './openStackSwift'; import { PublisherBase, TechDocsMetadata } from './types'; import { Stream, Readable } from 'stream'; -import { createMockDirectory } from '@backstage/backend-test-utils'; +import { + createMockDirectory, + mockServices, +} from '@backstage/backend-test-utils'; const mockDir = createMockDirectory(); @@ -169,7 +172,7 @@ const getPosixEntityRootDir = (entity: Entity) => { ); }; -const logger = getVoidLogger(); +const logger = loggerToWinstonLogger(mockServices.logger.mock()); let publisher: PublisherBase; diff --git a/plugins/techdocs-node/src/stages/publish/publish.test.ts b/plugins/techdocs-node/src/stages/publish/publish.test.ts index ab300412a4..4059eccfeb 100644 --- a/plugins/techdocs-node/src/stages/publish/publish.test.ts +++ b/plugins/techdocs-node/src/stages/publish/publish.test.ts @@ -14,8 +14,8 @@ * limitations under the License. */ import { - getVoidLogger, PluginEndpointDiscovery, + loggerToWinstonLogger, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { Publisher } from './publish'; @@ -24,8 +24,9 @@ import { GoogleGCSPublish } from './googleStorage'; import { AwsS3Publish } from './awsS3'; import { AzureBlobStoragePublish } from './azureBlobStorage'; import { OpenStackSwiftPublish } from './openStackSwift'; +import { mockServices } from '@backstage/backend-test-utils'; -const logger = getVoidLogger(); +const logger = loggerToWinstonLogger(mockServices.logger.mock()); const discovery: jest.Mocked = { getBaseUrl: jest.fn().mockResolvedValueOnce('http://localhost:7007'), getExternalBaseUrl: jest.fn(), diff --git a/plugins/techdocs-react/CHANGELOG.md b/plugins/techdocs-react/CHANGELOG.md index 4b76fe42e4..569a1cb49b 100644 --- a/plugins/techdocs-react/CHANGELOG.md +++ b/plugins/techdocs-react/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-techdocs-react +## 1.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/version-bridge@1.0.8 + +## 1.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/version-bridge@1.0.8 + ## 1.2.4 ### Patch Changes diff --git a/plugins/techdocs-react/package.json b/plugins/techdocs-react/package.json index 283e3afcff..8b10c5e3c9 100644 --- a/plugins/techdocs-react/package.json +++ b/plugins/techdocs-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-react", - "version": "1.2.4", + "version": "1.2.5-next.1", "description": "Shared frontend utilities for TechDocs and Addons", "backstage": { "role": "web-library" diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 7e8f3056e5..2c7ad7fec8 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-techdocs +## 1.10.6-next.1 + +### Patch Changes + +- cbebad1: Internal updates to allow reusing Backstage's `fetchApi` implementation for event source requests. This allows you to for example, override the `Authorization` header. +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/integration@1.12.0-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/integration-react@1.1.28-next.0 + - @backstage/plugin-auth-react@0.1.3-next.1 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/plugin-search-react@1.7.12-next.1 + - @backstage/plugin-techdocs-react@1.2.5-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6-next.0 + - @backstage/plugin-search-common@1.2.11 + +## 1.10.6-next.0 + +### Patch Changes + +- 654af4a: mkdocs-material have updated their CSS variable template, and a few are unset in Backstage. This patch adds the missing variables to ensure coverage. +- 96cd13e: `TechDocsIndexPage` now accepts an optional `ownerPickerMode` for toggling the behavior of the `EntityOwnerPicker`, exposing a new mode `` particularly suitable for larger catalogs. In this new mode, `EntityOwnerPicker` will display all the users and groups present in the catalog. +- e40bd9a: Fixed bug in CopyToClipboardButton component where positioning of the "Copy to clipboard" button in techdocs code snippets was broken in some cases +- 1256d88: Fix weird opening behaviour of the component. +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/plugin-search-react@1.7.12-next.0 + - @backstage/plugin-techdocs-react@1.2.5-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/integration@1.11.0 + - @backstage/integration-react@1.1.27 + - @backstage/plugin-auth-react@0.1.3-next.0 + - @backstage/plugin-catalog-react@1.12.1-next.0 + - @backstage/plugin-search-common@1.2.11 + ## 1.10.5 ### Patch Changes diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index 06b972bf5f..ebd6adcd3c 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -12,6 +12,7 @@ import { Config } from '@backstage/config'; import { CSSProperties } from '@material-ui/styles/withStyles'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { EntityOwnerPickerProps } from '@backstage/plugin-catalog-react'; import { FetchApi } from '@backstage/core-plugin-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { JSX as JSX_2 } from 'react'; @@ -292,6 +293,7 @@ export type TechDocsIndexPageProps = { initialFilter?: UserListFilterKind; columns?: TableColumn[]; actions?: TableProps['actions']; + ownerPickerMode?: EntityOwnerPickerProps['mode']; }; // @public @deprecated (undocumented) @@ -463,8 +465,8 @@ export class TechDocsStorageClient implements TechDocsStorageApi_2 { constructor(options: { configApi: Config; discoveryApi: DiscoveryApi; - identityApi: IdentityApi; fetchApi: FetchApi; + identityApi?: IdentityApi; }); // (undocumented) configApi: Config; @@ -483,8 +485,6 @@ export class TechDocsStorageClient implements TechDocsStorageApi_2 { getEntityDocs(entityId: CompoundEntityRef, path: string): Promise; // (undocumented) getStorageUrl(): Promise; - // (undocumented) - identityApi: IdentityApi; syncEntityDocs( entityId: CompoundEntityRef, logHandler?: (line: string) => void, diff --git a/plugins/techdocs/dev/index.tsx b/plugins/techdocs/dev/index.tsx index 9cf0364c63..57ed36c4e3 100644 --- a/plugins/techdocs/dev/index.tsx +++ b/plugins/techdocs/dev/index.tsx @@ -25,11 +25,7 @@ import { techdocsStorageApiRef, } from '../src'; -import { - configApiRef, - discoveryApiRef, - identityApiRef, -} from '@backstage/core-plugin-api'; +import { configApiRef, discoveryApiRef } from '@backstage/core-plugin-api'; import { TechDocsReaderPageProvider } from '@backstage/plugin-techdocs-react'; import { Header, Page, TabbedLayout } from '@backstage/core-components'; @@ -135,7 +131,6 @@ createDevApp() deps: { configApi: configApiRef, discoveryApi: discoveryApiRef, - identityApi: identityApiRef, }, factory: () => apiBridge, }) diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index bc9c4c8022..e4d6c0eaef 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "1.10.5", + "version": "1.10.6-next.1", "description": "The Backstage plugin that renders technical documentation for your components", "backstage": { "role": "frontend-plugin" @@ -70,9 +70,9 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@material-ui/styles": "^4.10.0", + "@microsoft/fetch-event-source": "^2.0.1", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", "dompurify": "^3.0.0", - "event-source-polyfill": "1.0.25", "git-url-parse": "^14.0.0", "jss": "~10.10.0", "lodash": "^4.17.21", @@ -90,7 +90,6 @@ "@testing-library/react": "^15.0.0", "@testing-library/user-event": "^14.0.0", "@types/dompurify": "^3.0.0", - "@types/event-source-polyfill": "^1.0.0", "canvas": "^2.10.2" }, "peerDependencies": { diff --git a/plugins/techdocs/src/alpha.tsx b/plugins/techdocs/src/alpha.tsx index 88dce3b48e..07fd7667ff 100644 --- a/plugins/techdocs/src/alpha.tsx +++ b/plugins/techdocs/src/alpha.tsx @@ -29,7 +29,6 @@ import { createApiFactory, discoveryApiRef, fetchApiRef, - identityApiRef, } from '@backstage/core-plugin-api'; import { compatWrapper, @@ -55,14 +54,12 @@ const techDocsStorageApi = createApiExtension({ deps: { configApi: configApiRef, discoveryApi: discoveryApiRef, - identityApi: identityApiRef, fetchApi: fetchApiRef, }, - factory: ({ configApi, discoveryApi, identityApi, fetchApi }) => + factory: ({ configApi, discoveryApi, fetchApi }) => new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }), }), diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index dde52cda49..62d497d790 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -17,16 +17,15 @@ import { UrlPatternDiscovery } from '@backstage/core-app-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { NotFoundError } from '@backstage/errors'; -import { EventSourcePolyfill } from 'event-source-polyfill'; +import { fetchEventSource } from '@microsoft/fetch-event-source'; import { MockConfigApi, MockFetchApi } from '@backstage/test-utils'; import { TechDocsStorageClient } from './client'; -const MockedEventSource = EventSourcePolyfill as jest.MockedClass< - typeof EventSourcePolyfill +jest.mock('@microsoft/fetch-event-source'); +const mockFetchEventSource = fetchEventSource as jest.MockedFunction< + typeof fetchEventSource >; -jest.mock('event-source-polyfill'); - const mockEntity = { kind: 'Component', namespace: 'default', @@ -51,7 +50,6 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); @@ -72,7 +70,6 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); @@ -84,71 +81,50 @@ describe('TechDocsStorageClient', () => { }); describe('syncEntityDocs', () => { - it('should create eventsource without headers', async () => { + it('should create eventsource with fetch', async () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); - MockedEventSource.prototype.addEventListener.mockImplementation( - (type, fn) => { - if (type === 'finish' && typeof fn === 'function') { - fn({ data: '{"updated": false}' } as any); - } - }, - ); - + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onopen, onmessage } = options; + await Promise.resolve(); + await onopen?.({ ok: true } as Response); + await Promise.resolve(); + onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); + }); identityApi.getCredentials.mockResolvedValue({}); await storageApi.syncEntityDocs(mockEntity); - expect(MockedEventSource).toHaveBeenCalledWith( + expect(mockFetchEventSource).toHaveBeenCalledWith( 'http://backstage:9191/api/techdocs/sync/default/Component/test-component', - { withCredentials: true, headers: {} }, - ); - }); - - it('should create eventsource with headers', async () => { - const storageApi = new TechDocsStorageClient({ - configApi, - discoveryApi, - identityApi, - fetchApi, - }); - - MockedEventSource.prototype.addEventListener.mockImplementation( - (type, fn) => { - if (type === 'finish' && typeof fn === 'function') { - fn({ data: '{"updated": false}' } as any); - } + { + fetch: fetchApi.fetch, + onerror: expect.any(Function), + onmessage: expect.any(Function), + signal: expect.any(AbortSignal), }, ); - - identityApi.getCredentials.mockResolvedValue({ token: 'token' }); - await storageApi.syncEntityDocs(mockEntity); - - expect(MockedEventSource).toHaveBeenCalledWith( - 'http://backstage:9191/api/techdocs/sync/default/Component/test-component', - { withCredentials: true, headers: { Authorization: 'Bearer token' } }, - ); }); it('should resolve to cached', async () => { + const myFetchApi = new MockFetchApi({ + injectIdentityAuth: { identityApi }, + }); const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, - fetchApi, + fetchApi: myFetchApi, + }); + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onopen, onmessage } = options; + await Promise.resolve(); + await onopen?.({ ok: true } as Response); + await Promise.resolve(); + onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); }); - - MockedEventSource.prototype.addEventListener.mockImplementation( - (type, fn) => { - if (type === 'finish' && typeof fn === 'function') { - fn({ data: '{"updated": false}' } as any); - } - }, - ); identityApi.getCredentials.mockResolvedValue({}); await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual( @@ -160,17 +136,15 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); - - MockedEventSource.prototype.addEventListener.mockImplementation( - (type, fn) => { - if (type === 'finish' && typeof fn === 'function') { - fn({ data: '{"updated": true}' } as any); - } - }, - ); + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onopen, onmessage } = options; + await Promise.resolve(); + await onopen?.({ ok: true } as Response); + await Promise.resolve(); + onmessage?.({ id: '', event: 'finish', data: '{"updated": true}' }); + }); identityApi.getCredentials.mockResolvedValue({}); await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual( @@ -182,21 +156,18 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); - MockedEventSource.prototype.addEventListener.mockImplementation( - (type, fn) => { - if (type === 'log' && typeof fn === 'function') { - fn({ data: '"A log message"' } as any); - } - - if (type === 'finish' && typeof fn === 'function') { - fn({ data: '{"updated": false}' } as any); - } - }, - ); + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onopen, onmessage } = options; + await Promise.resolve(); + await onopen?.({ ok: true } as Response); + await Promise.resolve(); + onmessage?.({ id: '', event: 'log', data: '"A log message"' }); + await Promise.resolve(); + onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); + }); identityApi.getCredentials.mockResolvedValue({}); const logHandler = jest.fn(); @@ -212,25 +183,18 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onerror } = options; + onerror?.(new NotFoundError('Some not found warning')); + }); + // we await later after we emitted the error identityApi.getCredentials.mockResolvedValue({}); const promise = storageApi.syncEntityDocs(mockEntity).then(); - // flush the event loop - await new Promise(r => setTimeout(r)); - - const instance = MockedEventSource.mock - .instances[0] as jest.Mocked; - - instance.onerror?.({ - status: 404, - message: 'Some not found warning', - } as any); - await expect(promise).rejects.toThrow(NotFoundError); await expect(promise).rejects.toThrow('Some not found warning'); }); @@ -239,7 +203,6 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); @@ -247,16 +210,10 @@ describe('TechDocsStorageClient', () => { identityApi.getCredentials.mockResolvedValue({}); const promise = storageApi.syncEntityDocs(mockEntity).then(); - // flush the event loop - await new Promise(r => setTimeout(r)); - - const instance = MockedEventSource.mock - .instances[0] as jest.Mocked; - - instance.onerror?.({ - type: 'error', - data: 'Some other error', - } as any); + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onerror } = options; + onerror?.(new Error('Some other error')); + }); await expect(promise).rejects.toThrow(Error); await expect(promise).rejects.toThrow('Some other error'); diff --git a/plugins/techdocs/src/client.ts b/plugins/techdocs/src/client.ts index 0f11eb4068..d00be2a01f 100644 --- a/plugins/techdocs/src/client.ts +++ b/plugins/techdocs/src/client.ts @@ -29,7 +29,7 @@ import { TechDocsMetadata, TechDocsStorageApi, } from '@backstage/plugin-techdocs-react'; -import { EventSourcePolyfill } from 'event-source-polyfill'; +import { fetchEventSource } from '@microsoft/fetch-event-source'; /** * API to talk to `techdocs-backend`. @@ -124,18 +124,17 @@ export class TechDocsClient implements TechDocsApi { export class TechDocsStorageClient implements TechDocsStorageApi { public configApi: Config; public discoveryApi: DiscoveryApi; - public identityApi: IdentityApi; private fetchApi: FetchApi; constructor(options: { configApi: Config; discoveryApi: DiscoveryApi; - identityApi: IdentityApi; fetchApi: FetchApi; + /** @deprecated identityApi is not needed any more */ + identityApi?: IdentityApi; }) { this.configApi = options.configApi; this.discoveryApi = options.discoveryApi; - this.identityApi = options.identityApi; this.fetchApi = options.fetchApi; } @@ -213,47 +212,32 @@ export class TechDocsStorageClient implements TechDocsStorageApi { const apiOrigin = await this.getApiOrigin(); const url = `${apiOrigin}/sync/${namespace}/${kind}/${name}`; - const { token } = await this.identityApi.getCredentials(); return new Promise((resolve, reject) => { - // Polyfill is used to add support for custom headers and auth - const source = new EventSourcePolyfill(url, { - withCredentials: true, - headers: token ? { Authorization: `Bearer ${token}` } : {}, - }); - - source.addEventListener('log', (e: any) => { - if (e.data) { - logHandler(JSON.parse(e.data)); - } - }); - - source.addEventListener('finish', (e: any) => { - let updated: boolean = false; - - if (e.data) { - ({ updated } = JSON.parse(e.data)); - } - - resolve(updated ? 'updated' : 'cached'); - }); - - source.onerror = (e: any) => { - source.close(); - - switch (e.status) { - // the endpoint returned a 404 status - case 404: - reject(new NotFoundError(e.message)); - return; - - // also handles the event-stream close. the reject is ignored if the Promise was already - // resolved by a finish event. - default: + const ctrl = new AbortController(); + fetchEventSource(url, { + fetch: this.fetchApi.fetch, + signal: ctrl.signal, + onmessage(e: any) { + if (e.event === 'log') { + if (e.data) { + logHandler(JSON.parse(e.data)); + } + } else if (e.event === 'finish') { + let updated: boolean = false; + if (e.data) { + ({ updated } = JSON.parse(e.data)); + } + resolve(updated ? 'updated' : 'cached'); + } else if (e.event === 'error') { reject(new Error(e.data)); - return; - } - }; + } + }, + onerror(err) { + ctrl.abort(); + reject(err); + }, + }); }); } diff --git a/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx index 9b9b79ef66..2fbd5bb50a 100644 --- a/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx @@ -46,7 +46,7 @@ export type DefaultTechDocsHomeProps = TechDocsIndexPageProps; * @public */ export const DefaultTechDocsHome = (props: TechDocsIndexPageProps) => { - const { initialFilter = 'owned', columns, actions } = props; + const { initialFilter = 'owned', columns, actions, ownerPickerMode } = props; return ( @@ -60,7 +60,7 @@ export const DefaultTechDocsHome = (props: TechDocsIndexPageProps) => { - + diff --git a/plugins/techdocs/src/home/components/TechDocsIndexPage.tsx b/plugins/techdocs/src/home/components/TechDocsIndexPage.tsx index b5c568cd76..adbb68d3e9 100644 --- a/plugins/techdocs/src/home/components/TechDocsIndexPage.tsx +++ b/plugins/techdocs/src/home/components/TechDocsIndexPage.tsx @@ -17,7 +17,10 @@ import React from 'react'; import { useOutlet } from 'react-router-dom'; import { TableColumn, TableProps } from '@backstage/core-components'; -import { UserListFilterKind } from '@backstage/plugin-catalog-react'; +import { + EntityOwnerPickerProps, + UserListFilterKind, +} from '@backstage/plugin-catalog-react'; import { DefaultTechDocsHome } from './DefaultTechDocsHome'; import { DocsTableRow } from './Tables'; @@ -30,6 +33,7 @@ export type TechDocsIndexPageProps = { initialFilter?: UserListFilterKind; columns?: TableColumn[]; actions?: TableProps['actions']; + ownerPickerMode?: EntityOwnerPickerProps['mode']; }; export const TechDocsIndexPage = (props: TechDocsIndexPageProps) => { diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts index bdd4814898..efa6bdfc1f 100644 --- a/plugins/techdocs/src/plugin.ts +++ b/plugins/techdocs/src/plugin.ts @@ -31,7 +31,6 @@ import { createRoutableExtension, discoveryApiRef, fetchApiRef, - identityApiRef, } from '@backstage/core-plugin-api'; import { createSearchResultListItemExtension, @@ -52,14 +51,12 @@ export const techdocsPlugin = createPlugin({ deps: { configApi: configApiRef, discoveryApi: discoveryApiRef, - identityApi: identityApiRef, fetchApi: fetchApiRef, }, - factory: ({ configApi, discoveryApi, identityApi, fetchApi }) => + factory: ({ configApi, discoveryApi, fetchApi }) => new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }), }), diff --git a/plugins/techdocs/src/search/components/TechDocsSearch.test.tsx b/plugins/techdocs/src/search/components/TechDocsSearch.test.tsx index fd008e301c..35770d43b5 100644 --- a/plugins/techdocs/src/search/components/TechDocsSearch.test.tsx +++ b/plugins/techdocs/src/search/components/TechDocsSearch.test.tsx @@ -18,7 +18,13 @@ import { ApiProvider } from '@backstage/core-app-api'; import { searchApiRef } from '@backstage/plugin-search-react'; import { TestApiRegistry, renderInTestApp } from '@backstage/test-utils'; import Button from '@material-ui/core/Button'; -import { fireEvent, screen, waitFor, within } from '@testing-library/react'; +import { + fireEvent, + screen, + waitFor, + within, + act, +} from '@testing-library/react'; import React, { useState } from 'react'; import { TechDocsSearch } from './TechDocsSearch'; @@ -91,9 +97,11 @@ it('should trigger query when autocomplete input changed', async () => { const autocomplete = screen.getByTestId('techdocs-search-bar'); const input = within(autocomplete).getByRole('textbox'); - autocomplete.click(); - autocomplete.focus(); - fireEvent.change(input, { target: { value: 'asdf' } }); + act(() => { + autocomplete.click(); + autocomplete.focus(); + fireEvent.change(input, { target: { value: 'asdf' } }); + }); await singleResult; await waitFor(() => @@ -148,7 +156,9 @@ it('should update filter values when a new entityName is provided', async () => }); const button = screen.getByText('Update Entity'); - button.click(); + act(() => { + button.click(); + }); await singleResult; await waitFor(() => diff --git a/plugins/user-settings-backend/CHANGELOG.md b/plugins/user-settings-backend/CHANGELOG.md index f689d8090f..0e7600d0e3 100644 --- a/plugins/user-settings-backend/CHANGELOG.md +++ b/plugins/user-settings-backend/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-user-settings-backend +## 0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.2 + - @backstage/backend-common@0.23.0-next.2 + - @backstage/plugin-auth-node@0.4.14-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.2.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.19-next.1 + - @backstage/backend-common@0.23.0-next.1 + - @backstage/plugin-auth-node@0.4.14-next.1 + +## 0.2.18-next.0 + +### Patch Changes + +- 8869b8e: Updated local development setup. +- Updated dependencies + - @backstage/backend-common@0.22.1-next.0 + - @backstage/backend-plugin-api@0.6.19-next.0 + - @backstage/plugin-auth-node@0.4.14-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.2.17 ### Patch Changes diff --git a/plugins/user-settings-backend/dev/index.ts b/plugins/user-settings-backend/dev/index.ts new file mode 100644 index 0000000000..49fa7cb6bd --- /dev/null +++ b/plugins/user-settings-backend/dev/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { createBackend } from '@backstage/backend-defaults'; +import { mockServices } from '@backstage/backend-test-utils'; + +const backend = createBackend(); +backend.add(mockServices.identity.factory()); +backend.add(import('../src/alpha')); +backend.start(); diff --git a/plugins/user-settings-backend/package.json b/plugins/user-settings-backend/package.json index ac5e5d9af2..f3d764a7eb 100644 --- a/plugins/user-settings-backend/package.json +++ b/plugins/user-settings-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings-backend", - "version": "0.2.17", + "version": "0.2.18-next.2", "description": "The Backstage backend plugin to manage user settings", "backstage": { "role": "backend-plugin" @@ -60,6 +60,7 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-defaults": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", diff --git a/plugins/user-settings-backend/src/service/standaloneServer.ts b/plugins/user-settings-backend/src/service/standaloneServer.ts deleted file mode 100644 index 48a640636f..0000000000 --- a/plugins/user-settings-backend/src/service/standaloneServer.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - createServiceBuilder, - DatabaseManager, -} from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; -import { IdentityApi } from '@backstage/plugin-auth-node'; -import { Server } from 'http'; -import { Logger } from 'winston'; -import { DatabaseUserSettingsStore } from '../database/DatabaseUserSettingsStore'; -import { createRouterInternal } from './router'; - -export interface ServerOptions { - port: number; - enableCors: boolean; - logger: Logger; -} - -export async function startStandaloneServer( - options: ServerOptions, -): Promise { - const logger = options.logger.child({ service: 'storage-backend' }); - - const manager = DatabaseManager.fromConfig( - new ConfigReader({ - backend: { - database: { client: 'better-sqlite3', connection: ':memory:' }, - }, - }), - ); - const database = manager.forPlugin('user-settings'); - - logger.debug('Starting application server...'); - - const identityMock: IdentityApi = { - async getIdentity({ request }) { - const token = request.headers.authorization?.split(' ')[1]; - return { - identity: { - type: 'user', - ownershipEntityRefs: [], - userEntityRef: token || 'user:default/john_doe', - }, - token: token || 'no-token', - }; - }, - }; - - const router = await createRouterInternal({ - userSettingsStore: await DatabaseUserSettingsStore.create({ database }), - identity: identityMock, - }); - - let service = createServiceBuilder(module) - .setPort(options.port) - .addRouter('/user-settings', router); - - if (options.enableCors) { - service = service.enableCors({ origin: 'http://localhost:3000' }); - } - - return await service.start().catch(err => { - logger.error(err); - process.exit(1); - }); -} - -module.hot?.accept(); diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index 85930ec041..859f4636dc 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-user-settings +## 0.8.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.8-next.1 + - @backstage/core-compat-api@0.2.6-next.1 + - @backstage/core-plugin-api@1.9.3-next.0 + - @backstage/core-app-api@1.12.6-next.0 + - @backstage/frontend-plugin-api@0.6.6-next.1 + - @backstage/plugin-catalog-react@1.12.1-next.1 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6-next.0 + - @backstage/types@1.1.1 + +## 0.8.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.5.6-next.0 + - @backstage/core-components@0.14.8-next.0 + - @backstage/core-app-api@1.12.5 + - @backstage/core-compat-api@0.2.6-next.0 + - @backstage/core-plugin-api@1.9.2 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.6-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-react@1.12.1-next.0 + ## 0.8.6 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index b3f0f244c8..bf636fd19b 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.6", + "version": "0.8.7-next.1", "description": "A Backstage plugin that provides a settings page", "backstage": { "role": "frontend-plugin" diff --git a/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx b/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx index f6be87f14f..d4b941c850 100644 --- a/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx +++ b/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx @@ -22,6 +22,7 @@ import { useOutlet } from 'react-router-dom'; import { SettingsLayout } from '../SettingsLayout'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { entityRouteRef } from '@backstage/plugin-catalog-react'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -34,7 +35,9 @@ describe('', () => { }); it('should render the default settings page with 3 tabs', async () => { - const { container } = await renderInTestApp(); + const { container } = await renderInTestApp(, { + mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef }, + }); const tabs = container.querySelectorAll('[class*=MuiTabs-root] a'); expect(tabs).toHaveLength(3); @@ -47,7 +50,9 @@ describe('', () => { ); (useOutlet as jest.Mock).mockReturnValue(advancedTabRoute); - const { container } = await renderInTestApp(); + const { container } = await renderInTestApp(, { + mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef }, + }); const tabs = container.querySelectorAll('[class*=MuiTabs-root] a'); expect(tabs).toHaveLength(4); @@ -61,7 +66,9 @@ describe('', () => { ); (useOutlet as jest.Mock).mockReturnValue(advancedTabRoute); - const { container } = await renderInTestApp(); + const { container } = await renderInTestApp(, { + mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef }, + }); const tabs = container.querySelectorAll('[class*=MuiTabs-root] a'); expect(tabs).toHaveLength(4); @@ -84,7 +91,9 @@ describe('', () => { ); (useOutlet as jest.Mock).mockReturnValue(customLayout); - const { container } = await renderInTestApp(); + const { container } = await renderInTestApp(, { + mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef }, + }); const tabs = container.querySelectorAll('[class*=MuiTabs-root] a'); expect(tabs).toHaveLength(2); diff --git a/scripts/check-docs-quality.js b/scripts/check-docs-quality.js index 9a9336b3e9..0493be1bca 100755 --- a/scripts/check-docs-quality.js +++ b/scripts/check-docs-quality.js @@ -71,15 +71,10 @@ async function exitIfMissingVale() { // eslint-disable-next-line @backstage/no-undeclared-imports await require('command-exists')('vale'); } catch (e) { - if (process.env.CI) { - console.log( - `Language linter (vale) was not found. Please install vale linter (https://vale.sh/docs/vale-cli/installation/).\n`, - ); - process.exit(1); - } - console.log(`Language linter (vale) generated errors. Please check the errors and review any markdown files that you changed. - Possibly update .github/vale/config/vocabularies/Backstage/accept.txt to add new valid words.\n`); - process.exit(0); + console.log( + `Language linter (vale) was not found. Please install vale linter (https://vale.sh/docs/vale-cli/installation/).\n`, + ); + process.exit(process.env.CI ? 1 : 0); } } diff --git a/scripts/verify-local-dependencies.js b/scripts/verify-local-dependencies.js index e2aaf8c33a..963bc40c9c 100755 --- a/scripts/verify-local-dependencies.js +++ b/scripts/verify-local-dependencies.js @@ -36,11 +36,11 @@ const depTypes = [ const roleRules = [ { sourceRole: ['frontend-plugin', 'web-library'], - targetRole: ['backend-plugin', 'node-library'], + targetRole: ['backend-plugin', 'node-library', 'backend-plugin-module'], message: `Package SOURCE_NAME with frontend role SOURCE_ROLE has a dependency on package TARGET_NAME with backend role TARGET_ROLE, which is not permitted`, }, { - sourceRole: ['backend-plugin', 'node-library'], + sourceRole: ['backend-plugin', 'node-library', 'backend-plugin-module'], targetRole: ['frontend-plugin', 'web-library'], message: `Package SOURCE_NAME with backend role SOURCE_ROLE has a dependency on package TARGET_NAME with frontend role TARGET_ROLE, which is not permitted`, }, @@ -51,8 +51,9 @@ const roleRules = [ 'web-library', 'backend-plugin', 'node-library', + 'backend-plugin-module', ], - message: `Polymorphic package SOURCE_NAME has a dependency on package TARGET_NAME with role TARGET_ROLE, which is not permitted since it's not also polymorphic`, + message: `Polymorphic package SOURCE_NAME with role SOURCE_ROLE has a dependency on package TARGET_NAME with role TARGET_ROLE, which is not permitted since it's not also polymorphic`, }, { sourceRole: ['frontend-plugin', 'web-library'], @@ -62,7 +63,7 @@ const roleRules = [ '@backstage/plugin-api-docs', '@backstage/plugin-techdocs-addons-test-utils', ], - message: `Package SOURCE_NAME with role SOURCE_ROLE has a dependency on another plugin package TARGET_NAME, which is not permitted`, + message: `Package SOURCE_NAME with role SOURCE_ROLE has a dependency on another plugin package TARGET_NAME with role TARGET_ROLE, which is not permitted`, }, { sourceRole: ['frontend-plugin', 'web-library'], @@ -80,7 +81,7 @@ const roleRules = [ '@backstage/plugin-techdocs-addons-test-utils', '@backstage/plugin-user-settings', ], - message: `Plugin package SOURCE_NAME with role SOURCE_ROLE has a runtime dependency on package TARGET_NAME, which is not permitted. If you are using this dependency for dev server purposes, you can move it to devDependencies instead.`, + message: `Plugin package SOURCE_NAME with role SOURCE_ROLE has a runtime dependency on package TARGET_NAME with role TARGET_ROLE, which is not permitted. If you are using this dependency for dev server purposes, you can move it to devDependencies instead.`, }, { sourceRole: ['backend-plugin', 'node-library'], @@ -92,7 +93,7 @@ const roleRules = [ '@backstage/backend-test-utils', '@backstage/backend-dynamic-feature-service', ], - message: `Plugin package SOURCE_NAME with role SOURCE_ROLE has a runtime dependency on package TARGET_NAME, which is not permitted. If you are using this dependency for dev server purposes, you can move it to devDependencies instead.`, + message: `Plugin package SOURCE_NAME with role SOURCE_ROLE has a runtime dependency on package TARGET_NAME with role TARGET_ROLE, which is not permitted. If you are using this dependency for dev server purposes, you can move it to devDependencies instead.`, }, ]; diff --git a/yarn.lock b/yarn.lock index 60a33729d6..a67630c6a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,6 +19,157 @@ __metadata: languageName: node linkType: hard +"@algolia/cache-browser-local-storage@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/cache-browser-local-storage@npm:4.23.2" + dependencies: + "@algolia/cache-common": 4.23.2 + checksum: 3b6b09666ba38f3675927f8193928e168becbb77fd3f0b27d1f7a94540be81c9837950c9e82a08502f48cff36938602ff8067575e6c12aea3869525ea3dcf69a + languageName: node + linkType: hard + +"@algolia/cache-common@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/cache-common@npm:4.23.2" + checksum: 45cbf8feafbd13219982c178c3173c22f836e7d1b4bfc87ce346e7d6a565d45c822b3ad301afec120a1131f91f31f9c078ae897b917885277a952a9cc67515e2 + languageName: node + linkType: hard + +"@algolia/cache-in-memory@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/cache-in-memory@npm:4.23.2" + dependencies: + "@algolia/cache-common": 4.23.2 + checksum: a89ed4320e94825effd647124fdfef18a2beee3942efa7fe865d071be04de096a8b3835cd0d872ba349681770ce9c8cf12720b9f74b0124db67b715f64c6a8fa + languageName: node + linkType: hard + +"@algolia/client-account@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/client-account@npm:4.23.2" + dependencies: + "@algolia/client-common": 4.23.2 + "@algolia/client-search": 4.23.2 + "@algolia/transporter": 4.23.2 + checksum: fa180f2c9c25e455d3094627586fc880c1b88c5bb0eba7ce663792d416d887f35108202e2a7279abd4a3df7c75bf0ef7fe63c21243fe7324970b350e654a0471 + languageName: node + linkType: hard + +"@algolia/client-analytics@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/client-analytics@npm:4.23.2" + dependencies: + "@algolia/client-common": 4.23.2 + "@algolia/client-search": 4.23.2 + "@algolia/requester-common": 4.23.2 + "@algolia/transporter": 4.23.2 + checksum: 51302bf7dbdfd97a88e4a181649d09a848f8b8a167ef55a0f951f00e3091f01c27aa8541698ef08fe029d36558447e983361bef880522c1ccc18e852db2a905c + languageName: node + linkType: hard + +"@algolia/client-common@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/client-common@npm:4.23.2" + dependencies: + "@algolia/requester-common": 4.23.2 + "@algolia/transporter": 4.23.2 + checksum: 0d36e794e682794d8831a2a92050a8fdd4f9a50e80debd41f608b4401c61fc9c96ec68b23385c474b34cc6bc2dc9468e418c07274878507e8fabdf73dd534322 + languageName: node + linkType: hard + +"@algolia/client-personalization@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/client-personalization@npm:4.23.2" + dependencies: + "@algolia/client-common": 4.23.2 + "@algolia/requester-common": 4.23.2 + "@algolia/transporter": 4.23.2 + checksum: b8fc0fe3922f8db8b13572d3eb4f472defa277261e5cbf6b01d1a59ff470b88380f1b569c3abb25e43ec1abdcfd276c199ffd336464904cdfa65a346e309853e + languageName: node + linkType: hard + +"@algolia/client-search@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/client-search@npm:4.23.2" + dependencies: + "@algolia/client-common": 4.23.2 + "@algolia/requester-common": 4.23.2 + "@algolia/transporter": 4.23.2 + checksum: e8eee05b362c84b33a7f4ba5e3af5274a3b4dde9cf20d4c755dce0f31715495f982574c84cc98f6b67d92229a06f9d5f9588b4d8659413131d4a589a3bd9a3f4 + languageName: node + linkType: hard + +"@algolia/logger-common@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/logger-common@npm:4.23.2" + checksum: da3c48adce896c91dd7a9770f8e6378394d4c7f0cd6f50710d340e411be0c8c52cd731488822ebec2926d2cbba7d0674dead23255f43bdf3f811f10a68c80f5e + languageName: node + linkType: hard + +"@algolia/logger-console@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/logger-console@npm:4.23.2" + dependencies: + "@algolia/logger-common": 4.23.2 + checksum: d3c82c5a6a15399621898bf8206a636be0c7edab59c22fd48c2590d77804365266a87e721b6ceee9ff97a8e5a7cd1b9353cf56dd7b3ac31b5ff36e7634decd13 + languageName: node + linkType: hard + +"@algolia/recommend@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/recommend@npm:4.23.2" + dependencies: + "@algolia/cache-browser-local-storage": 4.23.2 + "@algolia/cache-common": 4.23.2 + "@algolia/cache-in-memory": 4.23.2 + "@algolia/client-common": 4.23.2 + "@algolia/client-search": 4.23.2 + "@algolia/logger-common": 4.23.2 + "@algolia/logger-console": 4.23.2 + "@algolia/requester-browser-xhr": 4.23.2 + "@algolia/requester-common": 4.23.2 + "@algolia/requester-node-http": 4.23.2 + "@algolia/transporter": 4.23.2 + checksum: fa2b81656ac9aa557e06b57b3acde558d29da84ce45fddbe083145ec22cc540653f24436df2ecd24ffcfaf7c6115862833e76f6e745dad64b19a5a4a8aa36330 + languageName: node + linkType: hard + +"@algolia/requester-browser-xhr@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/requester-browser-xhr@npm:4.23.2" + dependencies: + "@algolia/requester-common": 4.23.2 + checksum: a16bdcebac7febd82052fab2bb9caef6c3e84a94092c0b70f2ea3aefff825099f6c6aa02bfeaacff6c5157e3bb89bfcfd690746e34aeb988272d64c4e31fb560 + languageName: node + linkType: hard + +"@algolia/requester-common@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/requester-common@npm:4.23.2" + checksum: a57da6b4a675b2176aa66d3148939e023f8fb2eb1e0a8830e4e07d2ecf4edae3416ee8614dd5bae4b8a1fc40fc3a418db24180a6a9cc64b227312e00512fc456 + languageName: node + linkType: hard + +"@algolia/requester-node-http@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/requester-node-http@npm:4.23.2" + dependencies: + "@algolia/requester-common": 4.23.2 + checksum: 3085543774fbdf77f043d22ec9528815408e103a9c628f84c702338f16bcbd1cf818d70c7fcb3a11c4542e5b264c56a7e30840c56fd895c5645f72ba16808fc8 + languageName: node + linkType: hard + +"@algolia/transporter@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/transporter@npm:4.23.2" + dependencies: + "@algolia/cache-common": 4.23.2 + "@algolia/logger-common": 4.23.2 + "@algolia/requester-common": 4.23.2 + checksum: 8ac57c003e6c5ab39c0cd873c325746c90e6bdbe0857c4157b551678cc4efed48e9423317dfced722d094810649d1ae44210c98122d6c1bece35713adffd2b24 + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.2.0": version: 2.2.1 resolution: "@ampproject/remapping@npm:2.2.1" @@ -105,6 +256,15 @@ __metadata: languageName: node linkType: hard +"@arcanis/slice-ansi@npm:^1.1.1": + version: 1.1.1 + resolution: "@arcanis/slice-ansi@npm:1.1.1" + dependencies: + grapheme-splitter: ^1.0.4 + checksum: 14ed60cb45750d386c64229ac7bab20e10eedc193503fa4decff764162d329d6d3363ed2cd3debec833186ee54affe4f824f6e8eff531295117fd1ebda200270 + languageName: node + linkType: hard + "@ardatan/sync-fetch@npm:^0.0.1": version: 0.0.1 resolution: "@ardatan/sync-fetch@npm:0.0.1" @@ -3430,10 +3590,34 @@ __metadata: dependencies: "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" + "@backstage/backend-dev-utils": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/config-loader": "workspace:^" + "@backstage/errors": "workspace:^" "@backstage/plugin-events-node": "workspace:^" + "@backstage/plugin-permission-node": "workspace:^" + "@backstage/types": "workspace:^" + "@keyv/memcache": ^1.3.5 + "@keyv/redis": ^2.5.3 + "@opentelemetry/api": ^1.3.0 + better-sqlite3: ^9.0.0 + cron: ^3.0.0 + fs-extra: ^11.2.0 + keyv: ^4.5.2 + knex: ^3.0.0 + lodash: ^4.17.21 + luxon: ^3.0.0 + mysql2: ^3.0.0 + p-limit: ^3.1.0 + pg: ^8.11.3 + pg-connection-string: ^2.3.0 + uuid: ^9.0.0 + wait-for-expect: ^3.0.2 + yn: ^4.0.0 + zod: ^3.22.4 languageName: unknown linkType: soft @@ -3506,7 +3690,6 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/backend-plugin-api@workspace:packages/backend-plugin-api" dependencies: - "@backstage/backend-tasks": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" @@ -3516,8 +3699,10 @@ __metadata: "@backstage/plugin-permission-common": "workspace:^" "@backstage/types": "workspace:^" "@types/express": ^4.17.6 + "@types/luxon": ^3.0.0 express: ^4.17.1 knex: ^3.0.0 + luxon: ^3.0.0 languageName: unknown linkType: soft @@ -3549,7 +3734,6 @@ __metadata: resolution: "@backstage/backend-test-utils@workspace:packages/backend-test-utils" dependencies: "@backstage/backend-app-api": "workspace:^" - "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" @@ -3557,25 +3741,31 @@ __metadata: "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-events-node": "workspace:^" "@backstage/types": "workspace:^" + "@keyv/memcache": ^1.3.5 + "@keyv/redis": ^2.5.3 + "@types/keyv": ^4.2.0 "@types/supertest": ^2.0.8 better-sqlite3: ^9.0.0 cookie: ^0.6.0 express: ^4.17.1 fs-extra: ^11.0.0 + keyv: ^4.5.2 knex: ^3.0.0 msw: ^1.0.0 mysql2: ^3.0.0 pg: ^8.11.3 + pg-connection-string: ^2.3.0 supertest: ^6.1.3 testcontainers: ^10.0.0 textextensions: ^5.16.0 uuid: ^9.0.0 + yn: ^4.0.0 peerDependencies: "@types/jest": "*" languageName: unknown linkType: soft -"@backstage/catalog-client@workspace:^, @backstage/catalog-client@workspace:packages/catalog-client": +"@backstage/catalog-client@^1.6.5, @backstage/catalog-client@workspace:^, @backstage/catalog-client@workspace:packages/catalog-client": version: 0.0.0-use.local resolution: "@backstage/catalog-client@workspace:packages/catalog-client" dependencies: @@ -3588,7 +3778,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/catalog-model@^1.4.3, @backstage/catalog-model@^1.4.5, @backstage/catalog-model@workspace:^, @backstage/catalog-model@workspace:packages/catalog-model": +"@backstage/catalog-model@^1.4.3, @backstage/catalog-model@^1.4.5, @backstage/catalog-model@^1.5.0, @backstage/catalog-model@workspace:^, @backstage/catalog-model@workspace:packages/catalog-model": version: 0.0.0-use.local resolution: "@backstage/catalog-model@workspace:packages/catalog-model" dependencies: @@ -3840,7 +4030,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/config@^1.1.1, @backstage/config@workspace:^, @backstage/config@workspace:packages/config": +"@backstage/config@^1.1.1, @backstage/config@^1.2.0, @backstage/config@workspace:^, @backstage/config@workspace:packages/config": version: 0.0.0-use.local resolution: "@backstage/config@workspace:packages/config" dependencies: @@ -3912,7 +4102,107 @@ __metadata: languageName: unknown linkType: soft -"@backstage/core-components@^0.14.4, @backstage/core-components@workspace:^, @backstage/core-components@workspace:packages/core-components": +"@backstage/core-components@npm:^0.13.10": + version: 0.13.10 + resolution: "@backstage/core-components@npm:0.13.10" + dependencies: + "@backstage/config": ^1.1.1 + "@backstage/core-plugin-api": ^1.8.2 + "@backstage/errors": ^1.2.3 + "@backstage/theme": ^0.5.0 + "@backstage/version-bridge": ^1.0.7 + "@date-io/core": ^1.3.13 + "@material-table/core": ^3.1.0 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.61 + "@react-hookz/web": ^23.0.0 + "@types/react": ^16.13.1 || ^17.0.0 + "@types/react-sparklines": ^1.7.0 + "@types/react-text-truncate": ^0.14.0 + ansi-regex: ^6.0.1 + classnames: ^2.2.6 + d3-selection: ^3.0.0 + d3-shape: ^3.0.0 + d3-zoom: ^3.0.0 + dagre: ^0.8.5 + linkify-react: 4.1.3 + linkifyjs: 4.1.3 + lodash: ^4.17.21 + pluralize: ^8.0.0 + qs: ^6.9.4 + rc-progress: 3.5.1 + react-helmet: 6.1.0 + react-hook-form: ^7.12.2 + react-idle-timer: 5.6.2 + react-markdown: ^8.0.0 + react-sparklines: ^1.7.0 + react-syntax-highlighter: ^15.4.5 + react-text-truncate: ^0.19.0 + react-use: ^17.3.2 + react-virtualized-auto-sizer: ^1.0.11 + react-window: ^1.8.6 + remark-gfm: ^3.0.1 + zen-observable: ^0.10.0 + zod: ^3.22.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: ec2a0d0a27bc4d6b9d4da97f0b4df600148529ee51bf2c8e7d3adc45c3950adac662535d3beabc451db346f2c7e3c412ceaa756fc774012b43dff5e2695f2271 + languageName: node + linkType: hard + +"@backstage/core-components@npm:^0.14.4, @backstage/core-components@npm:^0.14.7": + version: 0.14.7 + resolution: "@backstage/core-components@npm:0.14.7" + dependencies: + "@backstage/config": ^1.2.0 + "@backstage/core-plugin-api": ^1.9.2 + "@backstage/errors": ^1.2.4 + "@backstage/theme": ^0.5.4 + "@backstage/version-bridge": ^1.0.8 + "@date-io/core": ^1.3.13 + "@material-table/core": ^3.1.0 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.61 + "@react-hookz/web": ^24.0.0 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + "@types/react-sparklines": ^1.7.0 + ansi-regex: ^6.0.1 + classnames: ^2.2.6 + d3-selection: ^3.0.0 + d3-shape: ^3.0.0 + d3-zoom: ^3.0.0 + dagre: ^0.8.5 + linkify-react: 4.1.3 + linkifyjs: 4.1.3 + lodash: ^4.17.21 + pluralize: ^8.0.0 + qs: ^6.9.4 + rc-progress: 3.5.1 + react-helmet: 6.1.0 + react-hook-form: ^7.12.2 + react-idle-timer: 5.7.2 + react-markdown: ^8.0.0 + react-sparklines: ^1.7.0 + react-syntax-highlighter: ^15.4.5 + react-use: ^17.3.2 + react-virtualized-auto-sizer: ^1.0.11 + react-window: ^1.8.6 + remark-gfm: ^3.0.1 + zen-observable: ^0.10.0 + zod: ^3.22.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: e3906347c197d741dbff24d20af5cd0117e4bf1667c34a4a3a70d8c0f3c0c92bd768b53c417ca78f8b87560a3623f497938f531be86c8727f83822a11e9aa4e5 + languageName: node + linkType: hard + +"@backstage/core-components@workspace:^, @backstage/core-components@workspace:packages/core-components": version: 0.0.0-use.local resolution: "@backstage/core-components@workspace:packages/core-components" dependencies: @@ -3983,58 +4273,25 @@ __metadata: languageName: unknown linkType: soft -"@backstage/core-components@npm:^0.13.10": - version: 0.13.10 - resolution: "@backstage/core-components@npm:0.13.10" +"@backstage/core-plugin-api@npm:^1.8.2, @backstage/core-plugin-api@npm:^1.9.2": + version: 1.9.2 + resolution: "@backstage/core-plugin-api@npm:1.9.2" dependencies: - "@backstage/config": ^1.1.1 - "@backstage/core-plugin-api": ^1.8.2 - "@backstage/errors": ^1.2.3 - "@backstage/theme": ^0.5.0 - "@backstage/version-bridge": ^1.0.7 - "@date-io/core": ^1.3.13 - "@material-table/core": ^3.1.0 - "@material-ui/core": ^4.12.2 - "@material-ui/icons": ^4.9.1 - "@material-ui/lab": 4.0.0-alpha.61 - "@react-hookz/web": ^23.0.0 - "@types/react": ^16.13.1 || ^17.0.0 - "@types/react-sparklines": ^1.7.0 - "@types/react-text-truncate": ^0.14.0 - ansi-regex: ^6.0.1 - classnames: ^2.2.6 - d3-selection: ^3.0.0 - d3-shape: ^3.0.0 - d3-zoom: ^3.0.0 - dagre: ^0.8.5 - linkify-react: 4.1.3 - linkifyjs: 4.1.3 - lodash: ^4.17.21 - pluralize: ^8.0.0 - qs: ^6.9.4 - rc-progress: 3.5.1 - react-helmet: 6.1.0 - react-hook-form: ^7.12.2 - react-idle-timer: 5.6.2 - react-markdown: ^8.0.0 - react-sparklines: ^1.7.0 - react-syntax-highlighter: ^15.4.5 - react-text-truncate: ^0.19.0 - react-use: ^17.3.2 - react-virtualized-auto-sizer: ^1.0.11 - react-window: ^1.8.6 - remark-gfm: ^3.0.1 - zen-observable: ^0.10.0 - zod: ^3.22.4 + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@backstage/types": ^1.1.1 + "@backstage/version-bridge": ^1.0.8 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + history: ^5.0.0 peerDependencies: react: ^16.13.1 || ^17.0.0 || ^18.0.0 react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: ec2a0d0a27bc4d6b9d4da97f0b4df600148529ee51bf2c8e7d3adc45c3950adac662535d3beabc451db346f2c7e3c412ceaa756fc774012b43dff5e2695f2271 + checksum: 2df505c14853b3b35b8644d66f3e58d235bc6ee7f7b81785ec163aa9f089fc03a6c03e3b191d001b247f19d97063e02e585d67661720a8b6a13ab67a2403c218 languageName: node linkType: hard -"@backstage/core-plugin-api@^1.8.2, @backstage/core-plugin-api@^1.9.2, @backstage/core-plugin-api@workspace:^, @backstage/core-plugin-api@workspace:packages/core-plugin-api": +"@backstage/core-plugin-api@workspace:^, @backstage/core-plugin-api@workspace:packages/core-plugin-api": version: 0.0.0-use.local resolution: "@backstage/core-plugin-api@workspace:packages/core-plugin-api" dependencies: @@ -4181,6 +4438,26 @@ __metadata: languageName: unknown linkType: soft +"@backstage/frontend-plugin-api@npm:^0.6.5": + version: 0.6.5 + resolution: "@backstage/frontend-plugin-api@npm:0.6.5" + dependencies: + "@backstage/core-components": ^0.14.7 + "@backstage/core-plugin-api": ^1.9.2 + "@backstage/types": ^1.1.1 + "@backstage/version-bridge": ^1.0.8 + "@material-ui/core": ^4.12.4 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + lodash: ^4.17.21 + zod: ^3.22.4 + zod-to-json-schema: ^3.21.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: be880b5a3bb86d4e2c32a90ca64265b529901dabcdcbf5a87b5cbdfd68fc347297359550da195034e671e074db9db7c659d663d4fd46ed3836896bd1878fae2f + languageName: node + linkType: hard + "@backstage/frontend-plugin-api@workspace:^, @backstage/frontend-plugin-api@workspace:packages/frontend-plugin-api": version: 0.0.0-use.local resolution: "@backstage/frontend-plugin-api@workspace:packages/frontend-plugin-api" @@ -4244,6 +4521,24 @@ __metadata: languageName: unknown linkType: soft +"@backstage/integration-react@npm:^1.1.27": + version: 1.1.27 + resolution: "@backstage/integration-react@npm:1.1.27" + dependencies: + "@backstage/config": ^1.2.0 + "@backstage/core-plugin-api": ^1.9.2 + "@backstage/integration": ^1.11.0 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@types/react": ^16.13.1 || ^17.0.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: 136bdfeb7c4ba91eb2405a82ae99fd5d935056d7fe77064bf799a8d375a2b5c645e0df7deaee7e1e0ad6fb48dcac94c82e4b29e3d3a00f80555116b1902102b2 + languageName: node + linkType: hard + "@backstage/integration-react@workspace:^, @backstage/integration-react@workspace:packages/integration-react": version: 0.0.0-use.local resolution: "@backstage/integration-react@workspace:packages/integration-react" @@ -4268,6 +4563,23 @@ __metadata: languageName: unknown linkType: soft +"@backstage/integration@npm:^1.11.0": + version: 1.11.0 + resolution: "@backstage/integration@npm:1.11.0" + dependencies: + "@azure/identity": ^4.0.0 + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@octokit/auth-app": ^4.0.0 + "@octokit/rest": ^19.0.3 + cross-fetch: ^4.0.0 + git-url-parse: ^14.0.0 + lodash: ^4.17.21 + luxon: ^3.0.0 + checksum: 57ea46e57da004cdab41e82f558105f78f84f65d58163e93363dc775d42ab29401d3a38390ace012bf388eec57350d432a415d8fed27e57419e21522044fcc33 + languageName: node + linkType: hard + "@backstage/integration@workspace:^, @backstage/integration@workspace:packages/integration": version: 0.0.0-use.local resolution: "@backstage/integration@workspace:packages/integration" @@ -4351,6 +4663,7 @@ __metadata: dependencies: "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" @@ -5268,6 +5581,7 @@ __metadata: resolution: "@backstage/plugin-catalog-backend@workspace:plugins/catalog-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-openapi-utils": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" @@ -5320,7 +5634,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-catalog-common@^1.0.20, @backstage/plugin-catalog-common@workspace:^, @backstage/plugin-catalog-common@workspace:plugins/catalog-common": +"@backstage/plugin-catalog-common@^1.0.20, @backstage/plugin-catalog-common@^1.0.23, @backstage/plugin-catalog-common@workspace:^, @backstage/plugin-catalog-common@workspace:plugins/catalog-common": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-common@workspace:plugins/catalog-common" dependencies: @@ -5429,7 +5743,43 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-catalog-react@^1.11.3, @backstage/plugin-catalog-react@^1.9.3, @backstage/plugin-catalog-react@workspace:^, @backstage/plugin-catalog-react@workspace:plugins/catalog-react": +"@backstage/plugin-catalog-react@npm:^1.11.3, @backstage/plugin-catalog-react@npm:^1.9.3": + version: 1.12.0 + resolution: "@backstage/plugin-catalog-react@npm:1.12.0" + dependencies: + "@backstage/catalog-client": ^1.6.5 + "@backstage/catalog-model": ^1.5.0 + "@backstage/core-components": ^0.14.7 + "@backstage/core-plugin-api": ^1.9.2 + "@backstage/errors": ^1.2.4 + "@backstage/frontend-plugin-api": ^0.6.5 + "@backstage/integration-react": ^1.1.27 + "@backstage/plugin-catalog-common": ^1.0.23 + "@backstage/plugin-permission-common": ^0.7.13 + "@backstage/plugin-permission-react": ^0.4.22 + "@backstage/types": ^1.1.1 + "@backstage/version-bridge": ^1.0.8 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.61 + "@react-hookz/web": ^24.0.0 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + classnames: ^2.2.6 + lodash: ^4.17.21 + material-ui-popup-state: ^1.9.3 + qs: ^6.9.4 + react-use: ^17.2.4 + yaml: ^2.0.0 + zen-observable: ^0.10.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: 8079c6c1f4c5b07df5cfb2873f4c3ba16759a3a1ac36a1e1ff61fb428cece5baca40b5c2bc41a2d402ad9a60e73fd545c0a95eec274189423f40a04551a5f337 + languageName: node + linkType: hard + +"@backstage/plugin-catalog-react@workspace:^, @backstage/plugin-catalog-react@workspace:plugins/catalog-react": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-react@workspace:plugins/catalog-react" dependencies: @@ -5547,6 +5897,7 @@ __metadata: lodash: ^4.17.21 pluralize: ^8.0.0 react-use: ^17.2.4 + swr: ^2.2.5 zen-observable: ^0.10.0 peerDependencies: react: ^16.13.1 || ^17.0.0 || ^18.0.0 @@ -5587,6 +5938,7 @@ __metadata: resolution: "@backstage/plugin-devtools-backend@workspace:plugins/devtools-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" @@ -5771,6 +6123,7 @@ __metadata: dependencies: "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" languageName: unknown linkType: soft @@ -6120,6 +6473,7 @@ __metadata: "@backstage/test-utils": "workspace:^" knex: ^3.0.0 msw: ^1.0.0 + node-fetch: ^2.6.7 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -6268,7 +6622,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-permission-common@workspace:^, @backstage/plugin-permission-common@workspace:plugins/permission-common": +"@backstage/plugin-permission-common@^0.7.13, @backstage/plugin-permission-common@workspace:^, @backstage/plugin-permission-common@workspace:plugins/permission-common": version: 0.0.0-use.local resolution: "@backstage/plugin-permission-common@workspace:plugins/permission-common" dependencies: @@ -6306,6 +6660,23 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-permission-react@npm:^0.4.22": + version: 0.4.22 + resolution: "@backstage/plugin-permission-react@npm:0.4.22" + dependencies: + "@backstage/config": ^1.2.0 + "@backstage/core-plugin-api": ^1.9.2 + "@backstage/plugin-permission-common": ^0.7.13 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + swr: ^2.0.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: c91ad5a336358ae3a2e6030e7ea7a8584735544a14aa6ada061ca0c01ee32b2e16bd2fe24c9327cbdb959eec185bac045b1211b0a03a5c9d45b350f0a6c031ca + languageName: node + linkType: hard + "@backstage/plugin-permission-react@workspace:^, @backstage/plugin-permission-react@workspace:plugins/permission-react": version: 0.0.0-use.local resolution: "@backstage/plugin-permission-react@workspace:plugins/permission-react" @@ -6330,23 +6701,26 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-proxy-backend@workspace:plugins/proxy-backend" dependencies: + "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/config-loader": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/types": "workspace:^" "@types/express": ^4.17.6 "@types/http-proxy-middleware": ^1.0.0 - "@types/supertest": ^2.0.8 "@types/uuid": ^9.0.0 "@types/yup": ^0.32.0 express: ^4.17.1 express-promise-router: ^4.1.0 http-proxy-middleware: ^2.0.0 morgan: ^1.10.0 - msw: ^1.0.0 - supertest: ^6.1.3 + msw: ^2.0.0 + node-fetch: ^2.6.7 uuid: ^9.0.0 winston: ^3.2.1 yaml: ^2.0.0 @@ -6609,6 +6983,7 @@ __metadata: "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" "@backstage/types": "workspace:^" msw: ^2.0.0 + node-fetch: ^2.6.7 yaml: ^2.3.3 languageName: unknown linkType: soft @@ -6759,6 +7134,8 @@ __metadata: "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" + "@backstage/plugin-permission-react": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" @@ -6789,6 +7166,7 @@ __metadata: luxon: ^3.0.0 qs: ^6.9.4 react-use: ^17.2.4 + swr: ^2.0.0 use-immer: ^0.9.0 zen-observable: ^0.10.0 zod: ^3.22.4 @@ -6819,6 +7197,7 @@ __metadata: "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/plugin-scaffolder-react": "workspace:^" @@ -6830,6 +7209,7 @@ __metadata: "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.61 + "@microsoft/fetch-event-source": ^2.0.1 "@react-hookz/web": ^24.0.0 "@rjsf/core": 5.18.2 "@rjsf/material-ui": 5.18.2 @@ -6844,7 +7224,6 @@ __metadata: "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 "@uiw/react-codemirror": ^4.9.3 classnames: ^2.2.6 - event-source-polyfill: ^1.0.31 git-url-parse: ^14.0.0 humanize-duration: ^3.25.1 json-schema: ^0.4.0 @@ -6855,6 +7234,7 @@ __metadata: msw: ^1.0.0 qs: ^6.9.4 react-use: ^17.2.4 + swr: ^2.0.0 yaml: ^2.0.0 zen-observable: ^0.10.0 zod: ^3.22.4 @@ -6894,6 +7274,7 @@ __metadata: dependencies: "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/integration-aws-node": "workspace:^" @@ -6933,6 +7314,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-search-backend-module-pg@workspace:plugins/search-backend-module-pg" dependencies: + "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" @@ -6997,6 +7379,7 @@ __metadata: "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" @@ -7016,6 +7399,7 @@ __metadata: resolution: "@backstage/plugin-search-backend@workspace:plugins/search-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-openapi-utils": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" @@ -7127,6 +7511,7 @@ __metadata: "@backstage/backend-common": "workspace:^" "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" @@ -7245,6 +7630,7 @@ __metadata: resolution: "@backstage/plugin-techdocs-backend@workspace:plugins/techdocs-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" @@ -7401,16 +7787,15 @@ __metadata: "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.61 "@material-ui/styles": ^4.10.0 + "@microsoft/fetch-event-source": ^2.0.1 "@testing-library/dom": ^10.0.0 "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^15.0.0 "@testing-library/user-event": ^14.0.0 "@types/dompurify": ^3.0.0 - "@types/event-source-polyfill": ^1.0.0 "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 canvas: ^2.10.2 dompurify: ^3.0.0 - event-source-polyfill: 1.0.25 git-url-parse: ^14.0.0 jss: ~10.10.0 lodash: ^4.17.21 @@ -7428,6 +7813,7 @@ __metadata: resolution: "@backstage/plugin-user-settings-backend@workspace:plugins/user-settings-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" @@ -7578,7 +7964,23 @@ __metadata: languageName: unknown linkType: soft -"@backstage/theme@^0.5.0, @backstage/theme@workspace:^, @backstage/theme@workspace:packages/theme": +"@backstage/theme@npm:^0.5.0, @backstage/theme@npm:^0.5.4": + version: 0.5.5 + resolution: "@backstage/theme@npm:0.5.5" + dependencies: + "@emotion/react": ^11.10.5 + "@emotion/styled": ^11.10.5 + "@mui/material": ^5.12.2 + peerDependencies: + "@material-ui/core": ^4.12.2 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + checksum: a5ba7b39d41773a4a73a07d1a9a6bcf0815835a196a31c1ec1d5eac61ec801bfe875f31823a6ae6aa5033b21bd04b4fa692fd3fddc71a12e2126b1d222738b34 + languageName: node + linkType: hard + +"@backstage/theme@workspace:^, @backstage/theme@workspace:packages/theme": version: 0.0.0-use.local resolution: "@backstage/theme@workspace:packages/theme" dependencies: @@ -7609,7 +8011,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/version-bridge@^1.0.7, @backstage/version-bridge@workspace:^, @backstage/version-bridge@workspace:packages/version-bridge": +"@backstage/version-bridge@^1.0.7, @backstage/version-bridge@^1.0.8, @backstage/version-bridge@workspace:^, @backstage/version-bridge@workspace:packages/version-bridge": version: 0.0.0-use.local resolution: "@backstage/version-bridge@workspace:packages/version-bridge" dependencies: @@ -7663,14 +8065,15 @@ __metadata: languageName: node linkType: hard -"@changesets/apply-release-plan@npm:^7.0.0": - version: 7.0.0 - resolution: "@changesets/apply-release-plan@npm:7.0.0" +"@changesets/apply-release-plan@npm:^7.0.3": + version: 7.0.3 + resolution: "@changesets/apply-release-plan@npm:7.0.3" dependencies: "@babel/runtime": ^7.20.1 - "@changesets/config": ^3.0.0 + "@changesets/config": ^3.0.1 "@changesets/get-version-range-type": ^0.4.0 "@changesets/git": ^3.0.0 + "@changesets/should-skip-package": ^0.1.0 "@changesets/types": ^6.0.0 "@manypkg/get-packages": ^1.1.3 detect-indent: ^6.0.0 @@ -7680,35 +8083,22 @@ __metadata: prettier: ^2.7.1 resolve-from: ^5.0.0 semver: ^7.5.3 - checksum: ad83f89a3d46cd5249fa960cb0324114532bd5f25e74466d181afd6661273824859d038a12ba587a5e044f9169810e4a6febbb61e23c3819b3b28c00176a8bdf + checksum: 622b1ed7652b072b28bee431d5407ce686cff058caa3db9353848cd1177dc535d352711f6e5f9f819a7331235e72279f7e0f34955da67681c50d051ec30f476a languageName: node linkType: hard -"@changesets/assemble-release-plan@npm:6.0.0": - version: 6.0.0 - resolution: "@changesets/assemble-release-plan@npm:6.0.0" +"@changesets/assemble-release-plan@npm:^6.0.2": + version: 6.0.2 + resolution: "@changesets/assemble-release-plan@npm:6.0.2" dependencies: "@babel/runtime": ^7.20.1 "@changesets/errors": ^0.2.0 - "@changesets/get-dependents-graph": ^2.0.0 + "@changesets/get-dependents-graph": ^2.1.0 + "@changesets/should-skip-package": ^0.1.0 "@changesets/types": ^6.0.0 "@manypkg/get-packages": ^1.1.3 semver: ^7.5.3 - checksum: 0e6d25f25e0e3cc0e92aa8c43f5f496bae9464e2523be4ff81e31b6c9971b63bb1264821a2483c48d451d89d60af1acebe727e7f8c392ed48188a3ff26d0950e - languageName: node - linkType: hard - -"@changesets/assemble-release-plan@patch:@changesets/assemble-release-plan@npm%3A6.0.0#./.yarn/patches/@changesets-assemble-release-plan-npm-6.0.0-f7b3005037.patch::locator=root%40workspace%3A.": - version: 6.0.0 - resolution: "@changesets/assemble-release-plan@patch:@changesets/assemble-release-plan@npm%3A6.0.0#./.yarn/patches/@changesets-assemble-release-plan-npm-6.0.0-f7b3005037.patch::version=6.0.0&hash=43c5e4&locator=root%40workspace%3A." - dependencies: - "@babel/runtime": ^7.20.1 - "@changesets/errors": ^0.2.0 - "@changesets/get-dependents-graph": ^2.0.0 - "@changesets/types": ^6.0.0 - "@manypkg/get-packages": ^1.1.3 - semver: ^7.5.3 - checksum: b7a68e28d03379bdc2a1d7171963990e1b88d0e5efca5f5ed490c0f583d66d0ccbd4c04ebf4e5cd1c38e1346ff0e5c4c16e1b4973cdd97fabdbad0c6996fe016 + checksum: 90019a9bd22ad1368f23f1335d1b7eea5a55b1ed8b9141c9cc2fdab99bda85f9cb3886062a33c078cfb6f08f1a6f165aedbb1d1ce2a63c15ce0bfa7a7c365854 languageName: node linkType: hard @@ -7722,23 +8112,24 @@ __metadata: linkType: hard "@changesets/cli@npm:^2.14.0": - version: 2.27.1 - resolution: "@changesets/cli@npm:2.27.1" + version: 2.27.5 + resolution: "@changesets/cli@npm:2.27.5" dependencies: "@babel/runtime": ^7.20.1 - "@changesets/apply-release-plan": ^7.0.0 - "@changesets/assemble-release-plan": ^6.0.0 + "@changesets/apply-release-plan": ^7.0.3 + "@changesets/assemble-release-plan": ^6.0.2 "@changesets/changelog-git": ^0.2.0 - "@changesets/config": ^3.0.0 + "@changesets/config": ^3.0.1 "@changesets/errors": ^0.2.0 - "@changesets/get-dependents-graph": ^2.0.0 - "@changesets/get-release-plan": ^4.0.0 + "@changesets/get-dependents-graph": ^2.1.0 + "@changesets/get-release-plan": ^4.0.2 "@changesets/git": ^3.0.0 "@changesets/logger": ^0.1.0 "@changesets/pre": ^2.0.0 "@changesets/read": ^0.6.0 + "@changesets/should-skip-package": ^0.1.0 "@changesets/types": ^6.0.0 - "@changesets/write": ^0.3.0 + "@changesets/write": ^0.3.1 "@manypkg/get-packages": ^1.1.3 "@types/semver": ^7.5.0 ansi-colors: ^4.1.3 @@ -7759,22 +8150,22 @@ __metadata: tty-table: ^4.1.5 bin: changeset: bin.js - checksum: 0d030dec7e0ef28626082a257d57f46cdf65edb65a95f5a3511a9d298ca052388d8ab7f9a714943864eddc59148c4afb0b802a9c75b5bea45aade4c0dc7a5fa6 + checksum: 7e0d0aadabcf3028abbb2e079b58135b8e39f78bc174f4fa669cf94482ca3a4ec873e4b4ff9a7e071d83aee592780f0c47bd36a3a12282ab5f13a493c796663f languageName: node linkType: hard -"@changesets/config@npm:^3.0.0": - version: 3.0.0 - resolution: "@changesets/config@npm:3.0.0" +"@changesets/config@npm:^3.0.1": + version: 3.0.1 + resolution: "@changesets/config@npm:3.0.1" dependencies: "@changesets/errors": ^0.2.0 - "@changesets/get-dependents-graph": ^2.0.0 + "@changesets/get-dependents-graph": ^2.1.0 "@changesets/logger": ^0.1.0 "@changesets/types": ^6.0.0 "@manypkg/get-packages": ^1.1.3 fs-extra: ^7.0.1 micromatch: ^4.0.2 - checksum: 31a8c37e38768cf3676d24b7d371009dd1d691f221ecf086b79f0d96dc8e95aa408cda3659eb867a14615ea38a1c2be448bf0655c7570539af57c930ca784051 + checksum: 588a8139f721e2917c9bad842e3a933a8a5216b6a73c6a203ddcb834c274f873eaa780b32b42f89a477159e898ad3c1c93ca0ac28a07401b7b3f56185672a6a8 languageName: node linkType: hard @@ -7787,31 +8178,31 @@ __metadata: languageName: node linkType: hard -"@changesets/get-dependents-graph@npm:^2.0.0": - version: 2.0.0 - resolution: "@changesets/get-dependents-graph@npm:2.0.0" +"@changesets/get-dependents-graph@npm:^2.1.0": + version: 2.1.0 + resolution: "@changesets/get-dependents-graph@npm:2.1.0" dependencies: "@changesets/types": ^6.0.0 "@manypkg/get-packages": ^1.1.3 chalk: ^2.1.0 fs-extra: ^7.0.1 semver: ^7.5.3 - checksum: 6690d3ed36e8a636bc2a985d209bd72ee1100601ccf00850ca1fbe8500af839a3f4e5bd2167858cf11383aa76360f853e481533157060ad882fb56319db3090a + checksum: 1cd9c0f145a20d735ab9e1d615c5bdf52179cd2fc9b8555649d74979d8dbc915f9e85af4e20f7b71da76d3d860cd2090cee4c6ecccac4f5bf42ae11bb245471d languageName: node linkType: hard -"@changesets/get-release-plan@npm:^4.0.0": - version: 4.0.0 - resolution: "@changesets/get-release-plan@npm:4.0.0" +"@changesets/get-release-plan@npm:^4.0.2": + version: 4.0.2 + resolution: "@changesets/get-release-plan@npm:4.0.2" dependencies: "@babel/runtime": ^7.20.1 - "@changesets/assemble-release-plan": ^6.0.0 - "@changesets/config": ^3.0.0 + "@changesets/assemble-release-plan": ^6.0.2 + "@changesets/config": ^3.0.1 "@changesets/pre": ^2.0.0 "@changesets/read": ^0.6.0 "@changesets/types": ^6.0.0 "@manypkg/get-packages": ^1.1.3 - checksum: 57672c1e94f95de8ac65aac969275e0cb225f02aa86b2cef69329fff6e36ba5fde04eadeb6af36f4d8ac41a8fd329028b4df4c23c15c10fd13e026c77463d576 + checksum: 651814230890bf7605f4094332e4842505eb060092fbe65d9c8fa1a44f724ce0086890b8437f2fa96aedfa11c88e622e133d700847dea4c92141c2fbb486460f languageName: node linkType: hard @@ -7885,6 +8276,17 @@ __metadata: languageName: node linkType: hard +"@changesets/should-skip-package@npm:^0.1.0": + version: 0.1.0 + resolution: "@changesets/should-skip-package@npm:0.1.0" + dependencies: + "@babel/runtime": ^7.20.1 + "@changesets/types": ^6.0.0 + "@manypkg/get-packages": ^1.1.3 + checksum: 3c966feae085a41d4ce10a46a1b525f980cec59951f7cf23ea805866b370de5c75fdd45af326e1a5ef502fa4846f821d9df15550c2b606b96e99ed4b967f01c0 + languageName: node + linkType: hard + "@changesets/types@npm:^4.0.1": version: 4.1.0 resolution: "@changesets/types@npm:4.1.0" @@ -7899,16 +8301,16 @@ __metadata: languageName: node linkType: hard -"@changesets/write@npm:^0.3.0": - version: 0.3.0 - resolution: "@changesets/write@npm:0.3.0" +"@changesets/write@npm:^0.3.1": + version: 0.3.1 + resolution: "@changesets/write@npm:0.3.1" dependencies: "@babel/runtime": ^7.20.1 "@changesets/types": ^6.0.0 fs-extra: ^7.0.1 human-id: ^1.0.2 prettier: ^2.7.1 - checksum: 37588eb3ef2af15b3ea09d46864c994780619d20b791ea5b654801a035a3a12540c7f953e6e4f36731678615edc6d1c32f8fe174d599d3e6ce2d68263865788b + checksum: 6df0447e05ededbab71f36e6ad23aa77cf06eb6adda7a8b8e7fb9d6bd5bc93acceb916d55b2a37cb7e93fb05d39a236a0dd7ade5243aae4772885081101d4784 languageName: node linkType: hard @@ -7942,8 +8344,8 @@ __metadata: linkType: hard "@codemirror/language@npm:^6.0.0": - version: 6.10.1 - resolution: "@codemirror/language@npm:6.10.1" + version: 6.10.2 + resolution: "@codemirror/language@npm:6.10.2" dependencies: "@codemirror/state": ^6.0.0 "@codemirror/view": ^6.23.0 @@ -7951,7 +8353,7 @@ __metadata: "@lezer/highlight": ^1.0.0 "@lezer/lr": ^1.0.0 style-mod: ^4.0.0 - checksum: 453bbe122a84795752f29261412b69a8dcfdd7e4369eb7e112bffba36b9e527ad21adff1d3845e0dc44c9ab44eb0c6f823eb6ba790ddd00cc749847574eda779 + checksum: 4e60afb75fb56519f59d9d85e0aa03f0c8d017e0da0f3f8f321baf35a776801fcec9787f3d0c029eba12aa766fba98b0fe86fc3111b43e0812b554184c0e8d67 languageName: node linkType: hard @@ -9482,7 +9884,9 @@ __metadata: resolution: "@internal/plugin-todo-list-backend@workspace:plugins/example-todo-list-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" @@ -9934,11 +10338,11 @@ __metadata: linkType: hard "@keyv/redis@npm:^2.5.3": - version: 2.8.4 - resolution: "@keyv/redis@npm:2.8.4" + version: 2.8.5 + resolution: "@keyv/redis@npm:2.8.5" dependencies: - ioredis: ^5.3.2 - checksum: 088fb439dc900d6c848c187a0a3218f8c80e3d5df0ec94995d2245b701d59c9d99d4ccc2b48b12682e20d1c0653ababc2f7a51a04737c4df539f4dac82eca87b + ioredis: ^5.4.1 + checksum: 87ffec61d31fa9de128ba3e5a7b616535ddbdaa4d92cbc9e1a9fab143adf967135e9cca16e192e8f52cc1ba00ed2a7f10eca9944d7550385530dab95333e81ef languageName: node linkType: hard @@ -10354,6 +10758,13 @@ __metadata: languageName: node linkType: hard +"@microsoft/fetch-event-source@npm:^2.0.1": + version: 2.0.1 + resolution: "@microsoft/fetch-event-source@npm:2.0.1" + checksum: a50e1c0f33220206967266d0a4bbba0703e2793b079d9f6e6bfd48f71b2115964a803e14cf6e902c6fab321edc084f26022334f5eaacc2cec87f174715d41852 + languageName: node + linkType: hard + "@microsoft/microsoft-graph-types@npm:^2.6.0": version: 2.40.0 resolution: "@microsoft/microsoft-graph-types@npm:2.40.0" @@ -14078,8 +14489,8 @@ __metadata: linkType: hard "@rollup/plugin-commonjs@npm:^25.0.0": - version: 25.0.7 - resolution: "@rollup/plugin-commonjs@npm:25.0.7" + version: 25.0.8 + resolution: "@rollup/plugin-commonjs@npm:25.0.8" dependencies: "@rollup/pluginutils": ^5.0.1 commondir: ^1.0.1 @@ -14092,7 +14503,7 @@ __metadata: peerDependenciesMeta: rollup: optional: true - checksum: 052e11839a9edc556eda5dcc759ab816dcc57e9f0f905a1e6e14fff954eaa6b1e2d0d544f5bd18d863993c5eba43d8ac9c19d9bb53b1c3b1213f32cfc9d50b2e + checksum: dd105ee5625fbcaf832c0cf80be0aaf6a86bbd8fe99ff911f9ac4b78c79f26e9e99442b5aa0cc1136b5ddf89ec0b6c5728e5341ac04d687aef1b53063670b395 languageName: node linkType: hard @@ -16931,6 +17342,13 @@ __metadata: languageName: node linkType: hard +"@types/emscripten@npm:^1.39.6": + version: 1.39.10 + resolution: "@types/emscripten@npm:1.39.10" + checksum: 1721da76593f9194e0b7c90a581e2d31c23bd4eb28f93030cd1dc58216cdf1e692c045274f2eedaed29c652c25c9a4dff2e503b11bd1258d07095c009a1956b1 + languageName: node + linkType: hard + "@types/es-aggregate-error@npm:^1.0.2": version: 1.0.2 resolution: "@types/es-aggregate-error@npm:1.0.2" @@ -16974,13 +17392,6 @@ __metadata: languageName: node linkType: hard -"@types/event-source-polyfill@npm:^1.0.0": - version: 1.0.5 - resolution: "@types/event-source-polyfill@npm:1.0.5" - checksum: f506b68710162f2ade1bccbc5691b8c67e5a703e565df2bc0b7b5be2637ba838ef81ec6c10b03248fe4d054386d95a6e827c7aace6e924986c2b9985f77b55de - languageName: node - linkType: hard - "@types/expect@npm:^1.20.4": version: 1.20.4 resolution: "@types/expect@npm:1.20.4" @@ -17346,7 +17757,16 @@ __metadata: languageName: node linkType: hard -"@types/keyv@npm:*, @types/keyv@npm:^3.1.1": +"@types/keyv@npm:*, @types/keyv@npm:^4.2.0": + version: 4.2.0 + resolution: "@types/keyv@npm:4.2.0" + dependencies: + keyv: "*" + checksum: 8713da9382b9346d664866a6cab2f91b0fd479f61379af891303a618e9a2abad6f347adc38a0850540e3f2dad278427de24e7555339264fddb04d1d17d3b50e0 + languageName: node + linkType: hard + +"@types/keyv@npm:^3.1.1": version: 3.1.4 resolution: "@types/keyv@npm:3.1.4" dependencies: @@ -17422,9 +17842,9 @@ __metadata: linkType: hard "@types/lodash@npm:^4.14.151": - version: 4.17.1 - resolution: "@types/lodash@npm:4.17.1" - checksum: 01984d5b44c09ef45258f8ac6d0cf926900624064722d51a020ba179e5d4a293da0068fb278d87dc695586afe7ebd3362ec57f5c0e7c4f6c1fab9d04a80e77f5 + version: 4.17.4 + resolution: "@types/lodash@npm:4.17.4" + checksum: 268e652fd52d49189f155bc89b49bd4535aa44f0b6b0ed9ce7e50318307bda58147c49539d2047f39ca37cf5b5ea38dfb801d0dbcdbc8b019c95c1afc346b05a languageName: node linkType: hard @@ -17632,7 +18052,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^18.17.8": +"@types/node@npm:^18.17.15, @types/node@npm:^18.17.8": version: 18.19.8 resolution: "@types/node@npm:18.19.8" dependencies: @@ -18055,7 +18475,7 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7.3.12, @types/semver@npm:^7.5.0": +"@types/semver@npm:^7.1.0, @types/semver@npm:^7.3.12, @types/semver@npm:^7.5.0": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" checksum: ea6f5276f5b84c55921785a3a27a3cd37afee0111dfe2bcb3e03c31819c197c782598f17f0b150a69d453c9584cd14c4c4d7b9a55d2c5e6cacd4d66fdb3b3663 @@ -18290,6 +18710,13 @@ __metadata: languageName: node linkType: hard +"@types/treeify@npm:^1.0.0": + version: 1.0.3 + resolution: "@types/treeify@npm:1.0.3" + checksum: 777e579b30a916a781e7cbad2b7a76bc5473ff7bfe7167dd6de47f80f4386df5bf3d0dc34170afb75d52e75f6ed61cc109abf2324e093c1f9ecd4e79fec58d0c + languageName: node + linkType: hard + "@types/triple-beam@npm:^1.3.2": version: 1.3.2 resolution: "@types/triple-beam@npm:1.3.2" @@ -18352,9 +18779,9 @@ __metadata: linkType: hard "@types/webpack-env@npm:^1.15.2, @types/webpack-env@npm:^1.15.3": - version: 1.18.4 - resolution: "@types/webpack-env@npm:1.18.4" - checksum: f195b3ae974ac3b631477b57737dad7b6c44ecca86770cf3c29f284e02961c9f2dfc619e3e253d8c23966864cb052b1e8437e9834ede32ac97972e6e2235bb51 + version: 1.18.5 + resolution: "@types/webpack-env@npm:1.18.5" + checksum: 4ca8eb4c44e1e1807c3e245442fce7aaf2816a163056de9436bbac44cc47c8bc5b1c9a330dc05748d6616431b1fb5bd5379733fb1da0b78d03c59f4ec824c184 languageName: node linkType: hard @@ -18445,6 +18872,13 @@ __metadata: languageName: node linkType: hard +"@types/yoga-layout@npm:1.9.2": + version: 1.9.2 + resolution: "@types/yoga-layout@npm:1.9.2" + checksum: dbc3d6ab997d50fe1fcca5dd6822982c8fe586145ab648e0e97c3bc4ebc93d0b40c9edd75febaba374d61f60c1379b639f6be652965c776a901bf1068f2eac87 + languageName: node + linkType: hard + "@types/yup@npm:^0.32.0": version: 0.32.0 resolution: "@types/yup@npm:0.32.0" @@ -19129,6 +19563,144 @@ __metadata: languageName: node linkType: hard +"@yarnpkg/builder@npm:^4.0.0": + version: 4.0.0 + resolution: "@yarnpkg/builder@npm:4.0.0" + dependencies: + "@yarnpkg/cli": ^4.0.0 + "@yarnpkg/core": ^4.0.0 + "@yarnpkg/fslib": ^3.0.0 + chalk: ^3.0.0 + clipanion: ^4.0.0-rc.2 + esbuild: "npm:esbuild-wasm@^0.15.15" + semver: ^7.1.2 + tslib: ^2.4.0 + bin: + builder: ./lib/cli.js + checksum: 2bc2901e410e5951820c2d6fc5365ab15373b89e9f0aabce96cd6385019983d8911d495e95a4f3b927a1e18bc4b8de2e43158d9319e9a56de44d6e1ae4af9897 + languageName: node + linkType: hard + +"@yarnpkg/cli@npm:^4.0.0": + version: 4.1.1 + resolution: "@yarnpkg/cli@npm:4.1.1" + dependencies: + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/fslib": ^3.0.2 + "@yarnpkg/libzip": ^3.0.1 + "@yarnpkg/parsers": ^3.0.0 + "@yarnpkg/plugin-compat": ^4.0.3 + "@yarnpkg/plugin-constraints": ^4.0.2 + "@yarnpkg/plugin-dlx": ^4.0.0 + "@yarnpkg/plugin-essentials": ^4.1.1 + "@yarnpkg/plugin-exec": ^3.0.0 + "@yarnpkg/plugin-file": ^3.0.0 + "@yarnpkg/plugin-git": ^3.0.0 + "@yarnpkg/plugin-github": ^3.0.0 + "@yarnpkg/plugin-http": ^3.0.1 + "@yarnpkg/plugin-init": ^4.0.1 + "@yarnpkg/plugin-interactive-tools": ^4.0.0 + "@yarnpkg/plugin-link": ^3.0.0 + "@yarnpkg/plugin-nm": ^4.0.2 + "@yarnpkg/plugin-npm": ^3.0.1 + "@yarnpkg/plugin-npm-cli": ^4.0.2 + "@yarnpkg/plugin-pack": ^4.0.0 + "@yarnpkg/plugin-patch": ^4.0.1 + "@yarnpkg/plugin-pnp": ^4.0.2 + "@yarnpkg/plugin-pnpm": ^2.0.0 + "@yarnpkg/plugin-stage": ^4.0.0 + "@yarnpkg/plugin-typescript": ^4.0.0 + "@yarnpkg/plugin-version": ^4.0.1 + "@yarnpkg/plugin-workspace-tools": ^4.1.0 + "@yarnpkg/shell": ^4.0.0 + ci-info: ^3.2.0 + clipanion: ^4.0.0-rc.2 + semver: ^7.1.2 + tslib: ^2.4.0 + typanion: ^3.14.0 + peerDependencies: + "@yarnpkg/core": ^4.0.3 + checksum: e16567629195a88b36f744bc6d5b32d7ec5aad7ae37f00607c9ea0175028804ca75cd568a02ae8e28fce431b58fd268a593a13114ad892c2d8ed8f55d2d6768d + languageName: node + linkType: hard + +"@yarnpkg/core@npm:^4.0.0, @yarnpkg/core@npm:^4.0.3": + version: 4.0.3 + resolution: "@yarnpkg/core@npm:4.0.3" + dependencies: + "@arcanis/slice-ansi": ^1.1.1 + "@types/semver": ^7.1.0 + "@types/treeify": ^1.0.0 + "@yarnpkg/fslib": ^3.0.2 + "@yarnpkg/libzip": ^3.0.1 + "@yarnpkg/parsers": ^3.0.0 + "@yarnpkg/shell": ^4.0.0 + camelcase: ^5.3.1 + chalk: ^3.0.0 + ci-info: ^3.2.0 + clipanion: ^4.0.0-rc.2 + cross-spawn: 7.0.3 + diff: ^5.1.0 + dotenv: ^16.3.1 + fast-glob: ^3.2.2 + got: ^11.7.0 + lodash: ^4.17.15 + micromatch: ^4.0.2 + p-limit: ^2.2.0 + semver: ^7.1.2 + strip-ansi: ^6.0.0 + tar: ^6.0.5 + tinylogic: ^2.0.0 + treeify: ^1.1.0 + tslib: ^2.4.0 + tunnel: ^0.0.6 + checksum: 811956ab94e9f4b3601f304312baf816322df124cf85754ef3985110c90935b1b761c8266a06c2c5633f6edc39c8a4ceffeff50f08f036d7adf19612ce3761a3 + languageName: node + linkType: hard + +"@yarnpkg/extensions@npm:^2.0.1": + version: 2.0.1 + resolution: "@yarnpkg/extensions@npm:2.0.1" + peerDependencies: + "@yarnpkg/core": ^4.0.3 + checksum: e578718513822644b0857adece75dd7a7eea5cd4106ccd5cf37bfa11d8ad04051a0f4c339d0077fe6ec7f9a769d5e782f3034f4219e2a4bc3d54087aacbcdaf6 + languageName: node + linkType: hard + +"@yarnpkg/fslib@npm:^3.0.0, @yarnpkg/fslib@npm:^3.0.1, @yarnpkg/fslib@npm:^3.0.2": + version: 3.0.2 + resolution: "@yarnpkg/fslib@npm:3.0.2" + dependencies: + tslib: ^2.4.0 + checksum: b0795df777e43eb1c2bd8aa1d6c45f88b8a1d3922ee8bf264cdca916cc6f9455551a9499761a9c1f585aa576350d84adcc315e497fc031cdbbbc1a5c71942e1b + languageName: node + linkType: hard + +"@yarnpkg/libui@npm:^3.0.0": + version: 3.0.0 + resolution: "@yarnpkg/libui@npm:3.0.0" + dependencies: + tslib: ^2.4.0 + peerDependencies: + ink: ^3.0.8 + react: ^16.8.4 + checksum: 66a0f117ee35736012aa9fbaca68e62ef0c4b1731cff3f851aabb901366aa37102186cdf2917ee73ad8c99e54f78864b9274ef49b7b90552451a8d726c2d08ae + languageName: node + linkType: hard + +"@yarnpkg/libzip@npm:^3.0.0, @yarnpkg/libzip@npm:^3.0.1": + version: 3.0.1 + resolution: "@yarnpkg/libzip@npm:3.0.1" + dependencies: + "@types/emscripten": ^1.39.6 + "@yarnpkg/fslib": ^3.0.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/fslib": ^3.0.2 + checksum: 932b1fc35bb60ca57d033aab7a940f7b1d7bf52a68bfff6c755fe26827c0f1b373fda33e373afabbae98de3ed31ae4f0e9ee2ef24588cd074b75fb814117827d + languageName: node + linkType: hard + "@yarnpkg/lockfile@npm:^1.1.0": version: 1.1.0 resolution: "@yarnpkg/lockfile@npm:1.1.0" @@ -19136,6 +19708,17 @@ __metadata: languageName: node linkType: hard +"@yarnpkg/nm@npm:^4.0.2": + version: 4.0.2 + resolution: "@yarnpkg/nm@npm:4.0.2" + dependencies: + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/fslib": ^3.0.2 + "@yarnpkg/pnp": ^4.0.2 + checksum: 4a9d1a62d33230c84c3396e84e6b8a25e6b6c3ab9f9f36815b7ecf7581f5bf59abc1996f9ccf583627c177a841d09ffa31ad9cdd48ba3d500263bf12ad1ff281 + languageName: node + linkType: hard + "@yarnpkg/parsers@npm:^3.0.0": version: 3.0.0 resolution: "@yarnpkg/parsers@npm:3.0.0" @@ -19146,6 +19729,414 @@ __metadata: languageName: node linkType: hard +"@yarnpkg/plugin-compat@npm:^4.0.3": + version: 4.0.3 + resolution: "@yarnpkg/plugin-compat@npm:4.0.3" + dependencies: + "@yarnpkg/extensions": ^2.0.1 + peerDependencies: + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/plugin-patch": ^4.0.1 + checksum: 0704e4f5d2fd85cf93cbeae88ada3aaec67f390037e166e8dfa04ba1d02780ef169c3d174c2e46b1a8ada5ba3c35dfd5082ff026b638c6ca7b405a6aa7e9453b + languageName: node + linkType: hard + +"@yarnpkg/plugin-constraints@npm:^4.0.2": + version: 4.0.2 + resolution: "@yarnpkg/plugin-constraints@npm:4.0.2" + dependencies: + "@yarnpkg/fslib": ^3.0.1 + clipanion: ^4.0.0-rc.2 + lodash: ^4.17.15 + tau-prolog: ^0.2.66 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.2 + "@yarnpkg/core": ^4.0.2 + checksum: 9d357906336141d7fc8d6a2a7cf6e62ec4d6c8f8f06b3856286f7473dff2f0a03d1b21c28335b13540528a0cfcb9206062f20d2deeca78d3f3e02f41a1a0150e + languageName: node + linkType: hard + +"@yarnpkg/plugin-dlx@npm:^4.0.0": + version: 4.0.0 + resolution: "@yarnpkg/plugin-dlx@npm:4.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + clipanion: ^4.0.0-rc.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.0 + "@yarnpkg/core": ^4.0.0 + checksum: 239d8c8db728183e570735f4bd42c92d65eace219a38a3e7d895680030daf0f4a66004e88b7c3d57575b42b0b39801d31dd60b8d0d127bf713e645433c847ea4 + languageName: node + linkType: hard + +"@yarnpkg/plugin-essentials@npm:^4.1.1": + version: 4.1.1 + resolution: "@yarnpkg/plugin-essentials@npm:4.1.1" + dependencies: + "@yarnpkg/fslib": ^3.0.2 + "@yarnpkg/parsers": ^3.0.0 + ci-info: ^3.2.0 + clipanion: ^4.0.0-rc.2 + enquirer: ^2.3.6 + lodash: ^4.17.15 + micromatch: ^4.0.2 + semver: ^7.1.2 + tslib: ^2.4.0 + typanion: ^3.14.0 + peerDependencies: + "@yarnpkg/cli": ^4.1.1 + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/plugin-git": ^3.0.0 + checksum: 0992309971e90f05b795b04396668b9949db7e532035f4ce395378e698b5145048f1386a451db1978f59f8523636000fde2defa342ff1b3c38457714151119d4 + languageName: node + linkType: hard + +"@yarnpkg/plugin-exec@npm:^3.0.0": + version: 3.0.0 + resolution: "@yarnpkg/plugin-exec@npm:3.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/core": ^4.0.0 + checksum: 66ae5c6b48607a0fa7095bcea4304c384628b537cf02440d2ccc35847aeaeac07d09b7b215870000c048d0ff421a30771c5c57592ae2276c3ca630f7fd57fb2a + languageName: node + linkType: hard + +"@yarnpkg/plugin-file@npm:^3.0.0": + version: 3.0.0 + resolution: "@yarnpkg/plugin-file@npm:3.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + "@yarnpkg/libzip": ^3.0.0 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/core": ^4.0.0 + checksum: 149e57f555666b77eaf8e4035629f28bfbb026d20dc97720b7b8ee01781639ac29db5e27212a6b4980dd67097da4baa6297786c964f1e0321137489cdcb1fd31 + languageName: node + linkType: hard + +"@yarnpkg/plugin-git@npm:^3.0.0": + version: 3.0.0 + resolution: "@yarnpkg/plugin-git@npm:3.0.0" + dependencies: + "@types/semver": ^7.1.0 + "@yarnpkg/fslib": ^3.0.0 + clipanion: ^4.0.0-rc.2 + git-url-parse: ^13.1.0 + lodash: ^4.17.15 + semver: ^7.1.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/core": ^4.0.0 + checksum: d75ed9d339905e81d5ffb8d0cac5aa2cf2e106d322c4dc64acac0e53a00e6c7cd5fd931d7373b1d944d11fd4f4685188e5f445fce34394a627be77eade39b769 + languageName: node + linkType: hard + +"@yarnpkg/plugin-github@npm:^3.0.0": + version: 3.0.0 + resolution: "@yarnpkg/plugin-github@npm:3.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/core": ^4.0.0 + "@yarnpkg/plugin-git": ^3.0.0 + checksum: a8aa25ac79dfb547960446ea00e1afa65f14c5032a926b250f20d8f21e1a8b212aab90e7ae2830655c2a2257b9f989a8222dbdabcbc46be16796fa36e7aa7abe + languageName: node + linkType: hard + +"@yarnpkg/plugin-http@npm:^3.0.1": + version: 3.0.1 + resolution: "@yarnpkg/plugin-http@npm:3.0.1" + dependencies: + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/core": ^4.0.2 + checksum: 928d22cf37ff90501fbbc48ddff162c63e254100fe5f01c46b979458514f14d892f577047e90ced997e4c938edbe4b65b6580f9f5c0390b6b28bce1f0b2f4804 + languageName: node + linkType: hard + +"@yarnpkg/plugin-init@npm:^4.0.1": + version: 4.0.1 + resolution: "@yarnpkg/plugin-init@npm:4.0.1" + dependencies: + "@yarnpkg/fslib": ^3.0.1 + clipanion: ^4.0.0-rc.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.2 + "@yarnpkg/core": ^4.0.2 + checksum: d419f4e021f74b249be87b768ce10256407872d3a2af92dc51e92f3b5f779f2447af45483d76641651bac6e474f5c3fd275e4764422848bfaeb19ce7e50d97f3 + languageName: node + linkType: hard + +"@yarnpkg/plugin-interactive-tools@npm:^4.0.0": + version: 4.0.0 + resolution: "@yarnpkg/plugin-interactive-tools@npm:4.0.0" + dependencies: + "@yarnpkg/libui": ^3.0.0 + algoliasearch: ^4.2.0 + clipanion: ^4.0.0-rc.2 + diff: ^5.1.0 + ink: ^3.0.8 + ink-text-input: ^4.0.1 + react: ^16.13.1 + semver: ^7.1.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.0 + "@yarnpkg/core": ^4.0.0 + "@yarnpkg/plugin-essentials": ^4.0.0 + checksum: 43a50edf1e21582d0fd758077c787228e6145fa235e726bb4b8e2bdbcbd2ec0118ec63bc2ab6d3342ad2c06dc713bc871fa33bbb17de75d0c0c437daad2a666b + languageName: node + linkType: hard + +"@yarnpkg/plugin-link@npm:^3.0.0": + version: 3.0.0 + resolution: "@yarnpkg/plugin-link@npm:3.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/core": ^4.0.0 + checksum: 48bdb0e7ac8f9544999237d90f8b64163c1959d9e6f6a67d14799ef1c6533d5f30c7fa896129ed7b7ac693a9ce111646878ec4f505a39f5494df5db34cc372c4 + languageName: node + linkType: hard + +"@yarnpkg/plugin-nm@npm:^4.0.2": + version: 4.0.2 + resolution: "@yarnpkg/plugin-nm@npm:4.0.2" + dependencies: + "@yarnpkg/fslib": ^3.0.2 + "@yarnpkg/libzip": ^3.0.1 + "@yarnpkg/nm": ^4.0.2 + "@yarnpkg/parsers": ^3.0.0 + "@yarnpkg/plugin-pnp": ^4.0.2 + "@yarnpkg/pnp": ^4.0.2 + "@zkochan/cmd-shim": ^5.1.0 + clipanion: ^4.0.0-rc.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.1.0 + "@yarnpkg/core": ^4.0.3 + checksum: d7fad715a126dee644d3cb9a64fb6aac6ac1e179f08d72853b5247460a1033d81e7070f35bff196b16b0e0816c7a4e1e211f9a596c5af07ff6f84fff4a23cd61 + languageName: node + linkType: hard + +"@yarnpkg/plugin-npm-cli@npm:^4.0.2": + version: 4.0.2 + resolution: "@yarnpkg/plugin-npm-cli@npm:4.0.2" + dependencies: + "@yarnpkg/fslib": ^3.0.2 + clipanion: ^4.0.0-rc.2 + enquirer: ^2.3.6 + micromatch: ^4.0.2 + semver: ^7.1.2 + tslib: ^2.4.0 + typanion: ^3.14.0 + peerDependencies: + "@yarnpkg/cli": ^4.1.0 + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/plugin-npm": ^3.0.1 + "@yarnpkg/plugin-pack": ^4.0.0 + checksum: af338409d242045830276e78740b011afefb0950699cfb04b895b6962c9ba6a2a11e72eddbc04b3a568d5b0cb153ae68e22bbecfd34eedd5559681c18ac7e0c0 + languageName: node + linkType: hard + +"@yarnpkg/plugin-npm@npm:^3.0.1": + version: 3.0.1 + resolution: "@yarnpkg/plugin-npm@npm:3.0.1" + dependencies: + "@yarnpkg/fslib": ^3.0.2 + enquirer: ^2.3.6 + lodash: ^4.17.15 + semver: ^7.1.2 + ssri: ^6.0.1 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/plugin-pack": ^4.0.0 + checksum: 5b415fe84dfc306731c858c99523515766fcfbf75eb8fe17e2d0d2bd4fde525004df6477d71b418f4cc46f711b557feba4854d6216d49a2d1436ff4096c96dac + languageName: node + linkType: hard + +"@yarnpkg/plugin-pack@npm:^4.0.0": + version: 4.0.0 + resolution: "@yarnpkg/plugin-pack@npm:4.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + clipanion: ^4.0.0-rc.2 + micromatch: ^4.0.2 + tar-stream: ^2.0.1 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.0 + "@yarnpkg/core": ^4.0.0 + checksum: 566cd532e83f135d5d4848ed7d392c9136dda1bc38470bf26cbfdf615ed0a3410e9c01373ae42ce747c906734f02b25c75df636f425974a3394232c730f9e250 + languageName: node + linkType: hard + +"@yarnpkg/plugin-patch@npm:^4.0.1": + version: 4.0.1 + resolution: "@yarnpkg/plugin-patch@npm:4.0.1" + dependencies: + "@yarnpkg/fslib": ^3.0.1 + "@yarnpkg/libzip": ^3.0.0 + clipanion: ^4.0.0-rc.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.2 + "@yarnpkg/core": ^4.0.2 + checksum: c534b80facd573ddc6888d7a9b739a309295a600c105415ce6caf9b7ab5f2ca6983ba932fb35acca7de7d17e58fc4396cf28d051624007c97decc3c10c389d57 + languageName: node + linkType: hard + +"@yarnpkg/plugin-pnp@npm:^4.0.0, @yarnpkg/plugin-pnp@npm:^4.0.2": + version: 4.0.2 + resolution: "@yarnpkg/plugin-pnp@npm:4.0.2" + dependencies: + "@yarnpkg/fslib": ^3.0.1 + "@yarnpkg/plugin-stage": ^4.0.0 + "@yarnpkg/pnp": ^4.0.1 + clipanion: ^4.0.0-rc.2 + micromatch: ^4.0.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.2 + "@yarnpkg/core": ^4.0.2 + checksum: 29656bef3ee4c151172f64475a782efde77df3df996c397c34aa633deb9f9e3b562c18c6ed32b8555598adde062b6cd598bc630c28f4648498abf84652a76d6b + languageName: node + linkType: hard + +"@yarnpkg/plugin-pnpm@npm:^2.0.0": + version: 2.0.0 + resolution: "@yarnpkg/plugin-pnpm@npm:2.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + "@yarnpkg/plugin-pnp": ^4.0.0 + "@yarnpkg/plugin-stage": ^4.0.0 + clipanion: ^4.0.0-rc.2 + p-limit: ^2.2.0 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.0 + "@yarnpkg/core": ^4.0.0 + checksum: 5ec39a8f431eda3a8877e286a9ab88c719b3451d1a5009acaaf2370321288d40d8dc34495162eb89573aa8e00bddc6b6563bebfce459f9efe24589cbe5f9d4e9 + languageName: node + linkType: hard + +"@yarnpkg/plugin-stage@npm:^4.0.0": + version: 4.0.0 + resolution: "@yarnpkg/plugin-stage@npm:4.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + clipanion: ^4.0.0-rc.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.0 + "@yarnpkg/core": ^4.0.0 + checksum: b315ef24433151161cc931bf3ad72db08f7a2a611ee4a83da1fa96278384aee6d62c250314bcaabd41d0522b328ad4094a13bcf73d14babb6b03cc2ea7656761 + languageName: node + linkType: hard + +"@yarnpkg/plugin-typescript@npm:^4.0.0": + version: 4.0.0 + resolution: "@yarnpkg/plugin-typescript@npm:4.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + "@yarnpkg/plugin-pack": ^4.0.0 + algoliasearch: ^4.2.0 + semver: ^7.1.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.0 + "@yarnpkg/core": ^4.0.0 + "@yarnpkg/plugin-essentials": ^4.0.0 + checksum: 7ce40990039149acd8e62c6e150f0201638beb13cc78675a734b9024db22a6d6914dce090e805dc476795972897a849c154aa0371262dd8dec9743ac2df53944 + languageName: node + linkType: hard + +"@yarnpkg/plugin-version@npm:^4.0.1": + version: 4.0.1 + resolution: "@yarnpkg/plugin-version@npm:4.0.1" + dependencies: + "@yarnpkg/fslib": ^3.0.2 + "@yarnpkg/libui": ^3.0.0 + "@yarnpkg/parsers": ^3.0.0 + clipanion: ^4.0.0-rc.2 + ink: ^3.0.8 + lodash: ^4.17.15 + react: ^16.13.1 + semver: ^7.1.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.1.0 + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/plugin-git": ^3.0.0 + checksum: d155f675f2298f50c34f864d1598a9de8a10d6168909aa77904941f08212d3d23de0cfb5e6ca80f8ab738a3f5593a0d8046f1f5f7e8c6b3f3d4b2541b6e5625b + languageName: node + linkType: hard + +"@yarnpkg/plugin-workspace-tools@npm:^4.1.0": + version: 4.1.0 + resolution: "@yarnpkg/plugin-workspace-tools@npm:4.1.0" + dependencies: + "@yarnpkg/fslib": ^3.0.2 + clipanion: ^4.0.0-rc.2 + micromatch: ^4.0.2 + p-limit: ^2.2.0 + tslib: ^2.4.0 + typanion: ^3.14.0 + peerDependencies: + "@yarnpkg/cli": ^4.1.0 + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/plugin-git": ^3.0.0 + checksum: 8ee82796e1f86267827bf9fd25b07e63914732817fdcf8debe9d60e8e1ead87f79c8fb7b9bc60a78e097a1920579fb044b8f37aa28a3b5e7902893fc7e712957 + languageName: node + linkType: hard + +"@yarnpkg/pnp@npm:^4.0.1, @yarnpkg/pnp@npm:^4.0.2": + version: 4.0.2 + resolution: "@yarnpkg/pnp@npm:4.0.2" + dependencies: + "@types/node": ^18.17.15 + "@yarnpkg/fslib": ^3.0.2 + checksum: 7efd3702e83495b22452f79e56fc753f71e9f57613acaa775f64ec026b585cc8d3d6e2be6afeed9f6fbaa6034814eb25b70bc8a451c1f860100143d6e0e59c75 + languageName: node + linkType: hard + +"@yarnpkg/shell@npm:^4.0.0": + version: 4.0.0 + resolution: "@yarnpkg/shell@npm:4.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + "@yarnpkg/parsers": ^3.0.0 + chalk: ^3.0.0 + clipanion: ^4.0.0-rc.2 + cross-spawn: 7.0.3 + fast-glob: ^3.2.2 + micromatch: ^4.0.2 + tslib: ^2.4.0 + bin: + shell: ./lib/cli.js + checksum: 8497e278b1d3d0ffe324a3b9c878ca7165bbbe4d182f5ecb02f1bfaaf4dd18c8aaa54c33ee17bb37eb09173816dc4617b70c3fe0925f5fb99749687e2650b7a2 + languageName: node + linkType: hard + +"@zkochan/cmd-shim@npm:^5.1.0": + version: 5.4.1 + resolution: "@zkochan/cmd-shim@npm:5.4.1" + dependencies: + cmd-extension: ^1.0.2 + graceful-fs: ^4.2.10 + is-windows: ^1.0.2 + checksum: 418b4b3a5fb36960f9a96adb6ba3946be0494ab06be1959c8b2805fe37f7ebca9d185108138e30c7ff193eac74eebd10a361eca949a608c9e16afbffd6a48482 + languageName: node + linkType: hard + "@zkochan/retry@npm:^0.2.0": version: 0.2.0 resolution: "@zkochan/retry@npm:0.2.0" @@ -19432,6 +20423,29 @@ __metadata: languageName: node linkType: hard +"algoliasearch@npm:^4.2.0": + version: 4.23.2 + resolution: "algoliasearch@npm:4.23.2" + dependencies: + "@algolia/cache-browser-local-storage": 4.23.2 + "@algolia/cache-common": 4.23.2 + "@algolia/cache-in-memory": 4.23.2 + "@algolia/client-account": 4.23.2 + "@algolia/client-analytics": 4.23.2 + "@algolia/client-common": 4.23.2 + "@algolia/client-personalization": 4.23.2 + "@algolia/client-search": 4.23.2 + "@algolia/logger-common": 4.23.2 + "@algolia/logger-console": 4.23.2 + "@algolia/recommend": 4.23.2 + "@algolia/requester-browser-xhr": 4.23.2 + "@algolia/requester-common": 4.23.2 + "@algolia/requester-node-http": 4.23.2 + "@algolia/transporter": 4.23.2 + checksum: 25a3303a5aff34a049c9d115d140650b4178b4cf2d78b23f4e8bdae7caa5e1a61d22c888a12d162ac22cfeba99e2579b4c17b03ccd33f5b5446c9db8f10ff712 + languageName: node + linkType: hard + "alphanum-sort@npm:^1.0.2": version: 1.0.2 resolution: "alphanum-sort@npm:1.0.2" @@ -20090,6 +21104,13 @@ __metadata: languageName: node linkType: hard +"auto-bind@npm:4.0.0": + version: 4.0.0 + resolution: "auto-bind@npm:4.0.0" + checksum: 00cad71cce5742faccb7dd65c1b55ebc4f45add4b0c9a1547b10b05bab22813230133b0c892c67ba3eb969a4524710c5e43cc45c72898ec84e56f3a596e7a04f + languageName: node + linkType: hard + "autolinker@npm:^3.11.0": version: 3.16.2 resolution: "autolinker@npm:3.16.2" @@ -20116,25 +21137,25 @@ __metadata: linkType: hard "aws-sdk-client-mock-jest@npm:^4.0.0": - version: 4.0.0 - resolution: "aws-sdk-client-mock-jest@npm:4.0.0" + version: 4.0.1 + resolution: "aws-sdk-client-mock-jest@npm:4.0.1" dependencies: expect: ">28.1.3" tslib: ^2.1.0 peerDependencies: - aws-sdk-client-mock: 4.0.0 - checksum: 177d3317d337e501590ce62769b63407ac9fbfacdea0834b47d46f360c51959e32b598a3c341dcaaf66c4c6e369a72beecff9142e7110149b5bc69891ece2b78 + aws-sdk-client-mock: 4.0.1 + checksum: eada697b5be55a070f642e0debface36cd9e886c71eb5453e7732eaf5ab71ffb3b530d6346d3fcee4ae19b66560b6b7fe9e68ccde8f15cff509ed21cf3044a25 languageName: node linkType: hard "aws-sdk-client-mock@npm:^4.0.0": - version: 4.0.0 - resolution: "aws-sdk-client-mock@npm:4.0.0" + version: 4.0.1 + resolution: "aws-sdk-client-mock@npm:4.0.1" dependencies: "@types/sinon": ^10.0.10 sinon: ^16.1.3 tslib: ^2.1.0 - checksum: c5a2768390758093201df91a8b4610287b167ed87522b8b57514c89fd5ccc78cab60b92a2965bac944ed848da0d8a0dd2b660bd4e99b80a5a7116e39ad1745e9 + checksum: ba2e38c05d3dd89b71a305880b9994b60ae8744caf8ad111cb63f6cc65bc7ec15772b6c967e029933eba15b65304a80b1cf6ef5e7267ae49c334a8559125f138 languageName: node linkType: hard @@ -20803,12 +21824,12 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.2, braces@npm:~3.0.2": - version: 3.0.2 - resolution: "braces@npm:3.0.2" +"braces@npm:^3.0.2, braces@npm:^3.0.3, braces@npm:~3.0.2": + version: 3.0.3 + resolution: "braces@npm:3.0.3" dependencies: - fill-range: ^7.0.1 - checksum: e2a8e769a863f3d4ee887b5fe21f63193a891c68b612ddb4b68d82d1b5f3ff9073af066c343e9867a393fe4c2555dcb33e89b937195feb9c1613d259edfcd459 + fill-range: ^7.1.1 + checksum: b95aa0b3bd909f6cd1720ffcf031aeaf46154dd88b4da01f9a1d3f7ea866a79eba76a6d01cbc3c422b2ee5cdc39a4f02491058d5df0d7bf6e6a162a832df1f69 languageName: node linkType: hard @@ -21364,13 +22385,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:5.3.0": - version: 5.3.0 - resolution: "chalk@npm:5.3.0" - checksum: 623922e077b7d1e9dedaea6f8b9e9352921f8ae3afe739132e0e00c275971bdd331268183b2628cf4ab1727c45ea1f28d7e24ac23ce1db1eb653c414ca8a5a80 - languageName: node - linkType: hard - "chalk@npm:^3.0.0": version: 3.0.0 resolution: "chalk@npm:3.0.0" @@ -21381,6 +22395,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:~5.3.0": + version: 5.3.0 + resolution: "chalk@npm:5.3.0" + checksum: 623922e077b7d1e9dedaea6f8b9e9352921f8ae3afe739132e0e00c275971bdd331268183b2628cf4ab1727c45ea1f28d7e24ac23ce1db1eb653c414ca8a5a80 + languageName: node + linkType: hard + "char-regex@npm:^1.0.2": version: 1.0.2 resolution: "char-regex@npm:1.0.2" @@ -21570,7 +22591,7 @@ __metadata: languageName: node linkType: hard -"cli-boxes@npm:^2.2.1": +"cli-boxes@npm:^2.2.0, cli-boxes@npm:^2.2.1": version: 2.2.1 resolution: "cli-boxes@npm:2.2.1" checksum: be79f8ec23a558b49e01311b39a1ea01243ecee30539c880cf14bf518a12e223ef40c57ead0cb44f509bffdffc5c129c746cd50d863ab879385370112af4f585 @@ -21636,6 +22657,16 @@ __metadata: languageName: node linkType: hard +"cli-truncate@npm:^2.1.0": + version: 2.1.0 + resolution: "cli-truncate@npm:2.1.0" + dependencies: + slice-ansi: ^3.0.0 + string-width: ^4.2.0 + checksum: bf1e4e6195392dc718bf9cd71f317b6300dc4a9191d052f31046b8773230ece4fa09458813bf0e3455a5e68c0690d2ea2c197d14a8b85a7b5e01c97f4b5feb5d + languageName: node + linkType: hard + "cli-truncate@npm:^4.0.0": version: 4.0.0 resolution: "cli-truncate@npm:4.0.0" @@ -21667,6 +22698,17 @@ __metadata: languageName: node linkType: hard +"clipanion@npm:^4.0.0-rc.2": + version: 4.0.0-rc.3 + resolution: "clipanion@npm:4.0.0-rc.3" + dependencies: + typanion: ^3.8.0 + peerDependencies: + typanion: "*" + checksum: a5e6201e5a7fdb93dbe5f61d158d3e3d23e3164c38a7c679c7dcc599e10ee339d249c33fd8729ba8ffd0a376206ed14aac2e5472624ecaf79041fac3baa73b9b + languageName: node + linkType: hard + "cliui@npm:7.0.4, cliui@npm:^7.0.2": version: 7.0.4 resolution: "cliui@npm:7.0.4" @@ -21780,6 +22822,13 @@ __metadata: languageName: node linkType: hard +"cmd-extension@npm:^1.0.2": + version: 1.0.2 + resolution: "cmd-extension@npm:1.0.2" + checksum: 4cbcdd53196a3c1db3484f67aa49ed83c0e6069713f60193a94d747cb84050e8e64d688673aa5159cf0184e054cb806ceb6119e45744f721cbd3a09a3e7038cb + languageName: node + linkType: hard + "cmd-shim@npm:^4.0.1": version: 4.1.0 resolution: "cmd-shim@npm:4.1.0" @@ -21803,6 +22852,15 @@ __metadata: languageName: node linkType: hard +"code-excerpt@npm:^3.0.0": + version: 3.0.0 + resolution: "code-excerpt@npm:3.0.0" + dependencies: + convert-to-spaces: ^1.0.1 + checksum: fa3a8ed15967076a43a4093b0c824cf0ada15d9aab12ea3c028851b72a69b56495aac1eadf18c3b6ae4baf0a95bb1e1faa9dbeeb0a2b2b5ae058da23328e9dd8 + languageName: node + linkType: hard + "codemirror-graphql@npm:^2.0.10": version: 2.0.11 resolution: "codemirror-graphql@npm:2.0.11" @@ -21994,17 +23052,10 @@ __metadata: languageName: node linkType: hard -"commander@npm:*, commander@npm:^12.0.0": - version: 12.0.0 - resolution: "commander@npm:12.0.0" - checksum: bce9e243dc008baba6b8d923f95b251ad115e6e7551a15838d7568abebcca0fc832da1800cf37caf37852f35ce4b7fb794ba7a4824b88c5adb1395f9268642df - languageName: node - linkType: hard - -"commander@npm:11.1.0, commander@npm:^11.0.0": - version: 11.1.0 - resolution: "commander@npm:11.1.0" - checksum: fd1a8557c6b5b622c89ecdfde703242ab7db3b628ea5d1755784c79b8e7cb0d74d65b4a262289b533359cd58e1bfc0bf50245dfbcd2954682a6f367c828b79ef +"commander@npm:*, commander@npm:^12.0.0, commander@npm:~12.1.0": + version: 12.1.0 + resolution: "commander@npm:12.1.0" + checksum: 68e9818b00fc1ed9cdab9eb16905551c2b768a317ae69a5e3c43924c2b20ac9bb65b27e1cab36aeda7b6496376d4da908996ba2c0b5d79463e0fb1e77935d514 languageName: node linkType: hard @@ -22015,6 +23066,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:7.2.0, commander@npm:^7.2.0": + version: 7.2.0 + resolution: "commander@npm:7.2.0" + checksum: 53501cbeee61d5157546c0bef0fedb6cdfc763a882136284bed9a07225f09a14b82d2a84e7637edfd1a679fb35ed9502fd58ef1d091e6287f60d790147f68ddc + languageName: node + linkType: hard + "commander@npm:8.3.0, commander@npm:^8.3.0": version: 8.3.0 resolution: "commander@npm:8.3.0" @@ -22029,6 +23087,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^11.0.0": + version: 11.1.0 + resolution: "commander@npm:11.1.0" + checksum: fd1a8557c6b5b622c89ecdfde703242ab7db3b628ea5d1755784c79b8e7cb0d74d65b4a262289b533359cd58e1bfc0bf50245dfbcd2954682a6f367c828b79ef + languageName: node + linkType: hard + "commander@npm:^2.19.0, commander@npm:^2.20.0, commander@npm:^2.7.1": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -22050,13 +23115,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^7.2.0": - version: 7.2.0 - resolution: "commander@npm:7.2.0" - checksum: 53501cbeee61d5157546c0bef0fedb6cdfc763a882136284bed9a07225f09a14b82d2a84e7637edfd1a679fb35ed9502fd58ef1d091e6287f60d790147f68ddc - languageName: node - linkType: hard - "common-ancestor-path@npm:^1.0.1": version: 1.0.1 resolution: "common-ancestor-path@npm:1.0.1" @@ -22379,6 +23437,13 @@ __metadata: languageName: node linkType: hard +"convert-to-spaces@npm:^1.0.1": + version: 1.0.2 + resolution: "convert-to-spaces@npm:1.0.2" + checksum: e73f2ae39eb2b184f0796138eaab9c088b03b94937377d31be5b2282aef6a6ccce6b46f51bd99b3b7dfc70f516e2a6b16c0dd911883bfadf8d1073f462480224 + languageName: node + linkType: hard + "cookie-parser@npm:^1.4.5, cookie-parser@npm:^1.4.6": version: 1.4.6 resolution: "cookie-parser@npm:1.4.6" @@ -22713,6 +23778,17 @@ __metadata: languageName: node linkType: hard +"cross-spawn@npm:7.0.3, cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": + version: 7.0.3 + resolution: "cross-spawn@npm:7.0.3" + dependencies: + path-key: ^3.1.0 + shebang-command: ^2.0.0 + which: ^2.0.1 + checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52 + languageName: node + linkType: hard + "cross-spawn@npm:^5.1.0": version: 5.1.0 resolution: "cross-spawn@npm:5.1.0" @@ -22737,17 +23813,6 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": - version: 7.0.3 - resolution: "cross-spawn@npm:7.0.3" - dependencies: - path-key: ^3.1.0 - shebang-command: ^2.0.0 - which: ^2.0.1 - checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52 - languageName: node - linkType: hard - "crosspath@npm:^2.0.0": version: 2.0.0 resolution: "crosspath@npm:2.0.0" @@ -23343,7 +24408,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:4.3.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, debug@npm:4.3.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.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -24071,10 +25136,10 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^16.0.0, dotenv@npm:^16.0.3": - version: 16.3.1 - resolution: "dotenv@npm:16.3.1" - checksum: 15d75e7279018f4bafd0ee9706593dd14455ddb71b3bcba9c52574460b7ccaf67d5cf8b2c08a5af1a9da6db36c956a04a1192b101ee102a3e0cf8817bbcf3dfd +"dotenv@npm:^16.0.0, dotenv@npm:^16.0.3, dotenv@npm:^16.3.1": + version: 16.4.5 + resolution: "dotenv@npm:16.4.5" + checksum: 301a12c3d44fd49888b74eb9ccf9f07a1f5df43f489e7fcb89647a2edcd84c42d6bc349dc8df099cd18f07c35c7b04685c1a4f3e6a6a9e6b30f8d48c15b7f49c languageName: node linkType: hard @@ -24366,12 +25431,13 @@ __metadata: languageName: node linkType: hard -"enquirer@npm:^2.3.0": - version: 2.3.6 - resolution: "enquirer@npm:2.3.6" +"enquirer@npm:^2.3.0, enquirer@npm:^2.3.6": + version: 2.4.1 + resolution: "enquirer@npm:2.4.1" dependencies: ansi-colors: ^4.1.1 - checksum: 1c0911e14a6f8d26721c91e01db06092a5f7675159f0261d69c403396a385afd13dd76825e7678f66daffa930cfaa8d45f506fb35f818a2788463d022af1b884 + strip-ansi: ^6.0.1 + checksum: f080f11a74209647dbf347a7c6a83c8a47ae1ebf1e75073a808bc1088eb780aa54075bfecd1bcdb3e3c724520edb8e6ee05da031529436b421b71066fcc48cb5 languageName: node linkType: hard @@ -24826,6 +25892,15 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:esbuild-wasm@^0.15.15": + version: 0.15.18 + resolution: "esbuild-wasm@npm:0.15.18" + bin: + esbuild: bin/esbuild + checksum: 8e809acd497ee2170f0bb6bd7296d0b04f5ddfa79fb4ee1fb8431523822805a07b4a4e671efa06af6d4f078a876b10c7504f8d170808c8033c8a086f3075cb61 + languageName: node + linkType: hard + "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -25338,20 +26413,6 @@ __metadata: languageName: node linkType: hard -"event-source-polyfill@npm:1.0.25": - version: 1.0.25 - resolution: "event-source-polyfill@npm:1.0.25" - checksum: ed30428cc80eadfd693d267ba4a72dceaae938174cd116081ce38ad62bfd95f199430be7e8341e6f8f1e29489bbd5cfd4b3f6c8d6d463435623f7f91ae5f71b1 - languageName: node - linkType: hard - -"event-source-polyfill@npm:^1.0.31": - version: 1.0.31 - resolution: "event-source-polyfill@npm:1.0.31" - checksum: 973f226404e2a1b14ed7ef15c718b89e213b41d7cfeeb1c10937fd09229f13904f3d7c3075ab28ccf858c213007559908eecdd577577330352f53a351383dd75 - languageName: node - linkType: hard - "event-target-shim@npm:^5.0.0": version: 5.0.1 resolution: "event-target-shim@npm:5.0.1" @@ -25645,23 +26706,6 @@ __metadata: languageName: unknown linkType: soft -"execa@npm:8.0.1": - version: 8.0.1 - resolution: "execa@npm:8.0.1" - dependencies: - cross-spawn: ^7.0.3 - get-stream: ^8.0.1 - human-signals: ^5.0.0 - is-stream: ^3.0.0 - merge-stream: ^2.0.0 - npm-run-path: ^5.1.0 - onetime: ^6.0.0 - signal-exit: ^4.1.0 - strip-final-newline: ^3.0.0 - checksum: cac1bf86589d1d9b73bdc5dda65c52012d1a9619c44c526891956745f7b366ca2603d29fe3f7460bacc2b48c6eab5d6a4f7afe0534b31473d3708d1265545e1f - languageName: node - linkType: hard - "execa@npm:^1.0.0": version: 1.0.0 resolution: "execa@npm:1.0.0" @@ -25694,6 +26738,23 @@ __metadata: languageName: node linkType: hard +"execa@npm:~8.0.1": + version: 8.0.1 + resolution: "execa@npm:8.0.1" + dependencies: + cross-spawn: ^7.0.3 + get-stream: ^8.0.1 + human-signals: ^5.0.0 + is-stream: ^3.0.0 + merge-stream: ^2.0.0 + npm-run-path: ^5.1.0 + onetime: ^6.0.0 + signal-exit: ^4.1.0 + strip-final-newline: ^3.0.0 + checksum: cac1bf86589d1d9b73bdc5dda65c52012d1a9619c44c526891956745f7b366ca2603d29fe3f7460bacc2b48c6eab5d6a4f7afe0534b31473d3708d1265545e1f + languageName: node + linkType: hard + "exit-hook@npm:^2.2.1": version: 2.2.1 resolution: "exit-hook@npm:2.2.1" @@ -25941,7 +27002,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:3.3.2, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": +"fast-glob@npm:3.3.2, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" dependencies: @@ -26148,6 +27209,13 @@ __metadata: languageName: node linkType: hard +"figgy-pudding@npm:^3.5.1": + version: 3.5.2 + resolution: "figgy-pudding@npm:3.5.2" + checksum: 4090bd66193693dcda605e44d6b8715d8fb5c92a67acd57826e55cf816a342f550d57e5638f822b39366e1b2fdb244e99b3068a37213aa1d6c1bf602b8fde5ae + languageName: node + linkType: hard + "figures@npm:^3.0.0, figures@npm:^3.2.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -26207,12 +27275,12 @@ __metadata: languageName: node linkType: hard -"fill-range@npm:^7.0.1": - version: 7.0.1 - resolution: "fill-range@npm:7.0.1" +"fill-range@npm:^7.1.1": + version: 7.1.1 + resolution: "fill-range@npm:7.1.1" dependencies: to-regex-range: ^5.0.1 - checksum: cc283f4e65b504259e64fd969bcf4def4eb08d85565e906b7d36516e87819db52029a76b6363d0f02d0d532f0033c9603b9e2d943d56ee3b0d4f7ad3328ff917 + checksum: b4abfbca3839a3d55e4ae5ec62e131e2e356bf4859ce8480c64c4876100f4df292a63e5bb1618e1d7460282ca2b305653064f01654474aa35c68000980f17798 languageName: node linkType: hard @@ -27200,6 +28268,13 @@ __metadata: languageName: node linkType: hard +"globalyzer@npm:0.1.0": + version: 0.1.0 + resolution: "globalyzer@npm:0.1.0" + checksum: 419a0f95ba542534fac0842964d31b3dc2936a479b2b1a8a62bad7e8b61054faa9b0a06ad9f2e12593396b9b2621cac93358d9b3071d33723fb1778608d358a1 + languageName: node + linkType: hard + "globby@npm:^11.0.0, globby@npm:^11.0.1, globby@npm:^11.0.3, globby@npm:^11.0.4, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" @@ -27227,6 +28302,13 @@ __metadata: languageName: node linkType: hard +"globrex@npm:^0.1.2": + version: 0.1.2 + resolution: "globrex@npm:0.1.2" + checksum: adca162494a176ce9ecf4dd232f7b802956bb1966b37f60c15e49d2e7d961b66c60826366dc2649093cad5a0d69970cfa8875bd1695b5a1a2f33dcd2aa88da3c + languageName: node + linkType: hard + "goober@npm:^2.0.33": version: 2.1.14 resolution: "goober@npm:2.1.14" @@ -27285,9 +28367,9 @@ __metadata: languageName: node linkType: hard -"got@npm:^11.8.3": - version: 11.8.5 - resolution: "got@npm:11.8.5" +"got@npm:^11.7.0, got@npm:^11.8.3": + version: 11.8.6 + resolution: "got@npm:11.8.6" dependencies: "@sindresorhus/is": ^4.0.0 "@szmarczak/http-timer": ^4.0.5 @@ -27300,7 +28382,7 @@ __metadata: lowercase-keys: ^2.0.0 p-cancelable: ^2.0.0 responselike: ^2.0.0 - checksum: 2de8a1bbda4e9b6b2b72b2d2100bc055a59adc1740529e631f61feb44a8b9a1f9f8590941ed9da9df0090b6d6d0ed8ffee94cd9ac086ec3409b392b33440f7d2 + checksum: bbc783578a8d5030c8164ef7f57ce41b5ad7db2ed13371e1944bef157eeca5a7475530e07c0aaa71610d7085474d0d96222c9f4268d41db333a17e39b463f45d languageName: node linkType: hard @@ -28433,6 +29515,56 @@ __metadata: languageName: node linkType: hard +"ink-text-input@npm:^4.0.1": + version: 4.0.3 + resolution: "ink-text-input@npm:4.0.3" + dependencies: + chalk: ^4.1.0 + type-fest: ^0.15.1 + peerDependencies: + ink: ^3.0.0-3 + react: ^16.5.2 || ^17.0.0 + checksum: 2d309ec8ca386010d467822e317389e3c60b764fd04091df063a45c31f43104fd9f4a4e71a928a2c3c3cca461a9b8a526e90439616760f0f3726507132abbac5 + languageName: node + linkType: hard + +"ink@npm:^3.0.8": + version: 3.2.0 + resolution: "ink@npm:3.2.0" + dependencies: + ansi-escapes: ^4.2.1 + auto-bind: 4.0.0 + chalk: ^4.1.0 + cli-boxes: ^2.2.0 + cli-cursor: ^3.1.0 + cli-truncate: ^2.1.0 + code-excerpt: ^3.0.0 + indent-string: ^4.0.0 + is-ci: ^2.0.0 + lodash: ^4.17.20 + patch-console: ^1.0.0 + react-devtools-core: ^4.19.1 + react-reconciler: ^0.26.2 + scheduler: ^0.20.2 + signal-exit: ^3.0.2 + slice-ansi: ^3.0.0 + stack-utils: ^2.0.2 + string-width: ^4.2.2 + type-fest: ^0.12.0 + widest-line: ^3.1.0 + wrap-ansi: ^6.2.0 + ws: ^7.5.5 + yoga-layout-prebuilt: ^1.9.6 + peerDependencies: + "@types/react": ">=16.8.0" + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 35f1b733b94bf12cc0bf7acb4d3fcba9d961ede15cee9c64a7325606b74cee78e1009eaffbac127f4d7d28e758d8259dea8d0850bfacb991b8d93632f41d3fa2 + languageName: node + linkType: hard + "inline-style-parser@npm:0.1.1": version: 0.1.1 resolution: "inline-style-parser@npm:0.1.1" @@ -28517,9 +29649,9 @@ __metadata: languageName: node linkType: hard -"ioredis@npm:^5.3.2": - version: 5.3.2 - resolution: "ioredis@npm:5.3.2" +"ioredis@npm:^5.4.1": + version: 5.4.1 + resolution: "ioredis@npm:5.4.1" dependencies: "@ioredis/commands": ^1.1.1 cluster-key-slot: ^1.1.0 @@ -28530,7 +29662,7 @@ __metadata: redis-errors: ^1.2.0 redis-parser: ^3.0.0 standard-as-callback: ^2.1.0 - checksum: 9a23559133e862a768778301efb68ae8c2af3c33562174b54a4c2d6574b976e85c75a4c34857991af733e35c48faf4c356e7daa8fb0a3543d85ff1768c8754bc + checksum: 92210294f75800febe7544c27b07e4892480172363b11971aa575be5b68f023bfed4bc858abc9792230c153aa80409047a358f174062c14d17536aa4499fe10b languageName: node linkType: hard @@ -29280,7 +30412,7 @@ __metadata: languageName: node linkType: hard -"is-windows@npm:^1.0.0": +"is-windows@npm:^1.0.0, is-windows@npm:^1.0.2": version: 1.0.2 resolution: "is-windows@npm:1.0.2" checksum: 438b7e52656fe3b9b293b180defb4e448088e7023a523ec21a91a80b9ff8cdb3377ddb5b6e60f7c7de4fa8b63ab56e121b6705fe081b3cf1b828b0a380009ad7 @@ -30859,6 +31991,15 @@ __metadata: languageName: node linkType: hard +"keyv@npm:*, keyv@npm:^4.0.0, keyv@npm:^4.5.2": + version: 4.5.4 + resolution: "keyv@npm:4.5.4" + dependencies: + json-buffer: 3.0.1 + checksum: 74a24395b1c34bd44ad5cb2b49140d087553e170625240b86755a6604cd65aa16efdbdeae5cdb17ba1284a0fbb25ad06263755dbc71b8d8b06f74232ce3cdd72 + languageName: node + linkType: hard + "keyv@npm:^3.0.0": version: 3.1.0 resolution: "keyv@npm:3.1.0" @@ -30868,15 +32009,6 @@ __metadata: languageName: node linkType: hard -"keyv@npm:^4.0.0, keyv@npm:^4.5.2": - version: 4.5.4 - resolution: "keyv@npm:4.5.4" - dependencies: - json-buffer: 3.0.1 - checksum: 74a24395b1c34bd44ad5cb2b49140d087553e170625240b86755a6604cd65aa16efdbdeae5cdb17ba1284a0fbb25ad06263755dbc71b8d8b06f74232ce3cdd72 - languageName: node - linkType: hard - "kind-of@npm:^6.0.2, kind-of@npm:^6.0.3": version: 6.0.3 resolution: "kind-of@npm:6.0.3" @@ -31136,13 +32268,6 @@ __metadata: languageName: node linkType: hard -"lilconfig@npm:3.0.0": - version: 3.0.0 - resolution: "lilconfig@npm:3.0.0" - checksum: a155f1cd24d324ab20dd6974db9ebcf3fb6f2b60175f7c052d917ff8a746b590bc1ee550f6fc3cb1e8716c8b58304e22fe2193febebc0cf16fa86d85e6f896c5 - languageName: node - linkType: hard - "lilconfig@npm:^2.0.3": version: 2.1.0 resolution: "lilconfig@npm:2.1.0" @@ -31150,6 +32275,13 @@ __metadata: languageName: node linkType: hard +"lilconfig@npm:~3.1.1": + version: 3.1.1 + resolution: "lilconfig@npm:3.1.1" + checksum: dc8a4f4afde3f0fac6bd36163cc4777a577a90759b8ef1d0d766b19ccf121f723aa79924f32af5b954f3965268215e046d0f237c41c76e5ef01d4e6d1208a15e + languageName: node + linkType: hard + "lines-and-columns@npm:^1.1.6": version: 1.2.4 resolution: "lines-and-columns@npm:1.2.4" @@ -31184,22 +32316,22 @@ __metadata: linkType: hard "lint-staged@npm:^15.0.0": - version: 15.2.2 - resolution: "lint-staged@npm:15.2.2" + version: 15.2.5 + resolution: "lint-staged@npm:15.2.5" dependencies: - chalk: 5.3.0 - commander: 11.1.0 - debug: 4.3.4 - execa: 8.0.1 - lilconfig: 3.0.0 - listr2: 8.0.1 - micromatch: 4.0.5 - pidtree: 0.6.0 - string-argv: 0.3.2 - yaml: 2.3.4 + chalk: ~5.3.0 + commander: ~12.1.0 + debug: ~4.3.4 + execa: ~8.0.1 + lilconfig: ~3.1.1 + listr2: ~8.2.1 + micromatch: ~4.0.7 + pidtree: ~0.6.0 + string-argv: ~0.3.2 + yaml: ~2.4.2 bin: lint-staged: bin/lint-staged.js - checksum: 031718ad3f839475fb1d41bda34bab4330f25814175808169daa2686ff026e5a667a25c95fdf3cd46dac72f9af2c98852565bb62d920992f5e2d3f730c279760 + checksum: 3025868d965eb401a5ebd903abd70cfebb8dbeb41eea1020c316f9c8c79083ea203f6cef95d32bfa8c9ec5486392b4ed08632ace6a5347b69cf238ba00e178f0 languageName: node linkType: hard @@ -31210,17 +32342,17 @@ __metadata: languageName: node linkType: hard -"listr2@npm:8.0.1": - version: 8.0.1 - resolution: "listr2@npm:8.0.1" +"listr2@npm:~8.2.1": + version: 8.2.1 + resolution: "listr2@npm:8.2.1" dependencies: cli-truncate: ^4.0.0 colorette: ^2.0.20 eventemitter3: ^5.0.1 log-update: ^6.0.0 - rfdc: ^1.3.0 + rfdc: ^1.3.1 wrap-ansi: ^9.0.0 - checksum: 4dfeabfa037b3981d0edbf30789971ba727ba4cfcc13051ceaff7a1b3d26509ef2d946015c65c600b0775ec9d1ef58a81937d94c9c03de464b654f429cc7c3ed + checksum: a37c032850fc01f45cf6144f2b66d0c56a596b708de1acbd52e7c396a2eb188d027ad132c93a0ad946d7932a581dfcfc2e4318bb301926b01877cb4903d09fbd languageName: node linkType: hard @@ -32711,7 +33843,7 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:4.0.5, micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5": +"micromatch@npm:4.0.5": version: 4.0.5 resolution: "micromatch@npm:4.0.5" dependencies: @@ -32721,6 +33853,16 @@ __metadata: languageName: node linkType: hard +"micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5, micromatch@npm:~4.0.7": + version: 4.0.7 + resolution: "micromatch@npm:4.0.7" + dependencies: + braces: ^3.0.3 + picomatch: ^2.3.1 + checksum: 3cde047d70ad80cf60c787b77198d680db3b8c25b23feb01de5e2652205d9c19f43bd81882f69a0fd1f0cde6a7a122d774998aad3271ddb1b8accf8a0f480cf7 + languageName: node + linkType: hard + "miller-rabin@npm:^4.0.0": version: 4.0.1 resolution: "miller-rabin@npm:4.0.1" @@ -33392,8 +34534,8 @@ __metadata: linkType: hard "mysql2@npm:^3.0.0": - version: 3.9.7 - resolution: "mysql2@npm:3.9.7" + version: 3.10.0 + resolution: "mysql2@npm:3.10.0" dependencies: denque: ^2.1.0 generate-function: ^2.3.1 @@ -33403,7 +34545,7 @@ __metadata: named-placeholders: ^1.1.3 seq-queue: ^0.0.5 sqlstring: ^2.3.2 - checksum: 535261d076f840f0966788b3f33a5ff7872e5da321240c2359be5c9e7ec19197ed5f6e01f0bc7beae06dd291d03eb2bde00f474461a578debcb85fcd98e347d3 + checksum: 4306de21317a05fcd4bbf28679e6b61b5e3421f7b8e2813ce7be8a06a8f7f4e09067bbd77507de465aba59a5742d1c48a3ed1038e96fb76935d24e1648d4b9c3 languageName: node linkType: hard @@ -33876,6 +35018,13 @@ __metadata: languageName: node linkType: hard +"node-watch@npm:0.7.3": + version: 0.7.3 + resolution: "node-watch@npm:0.7.3" + checksum: c745482f720613415153b9065383b77d21f2ef60ceabf64f779c1452b1dcbb8d08c71f650b93f3c1d84524371321f92a15fda468745872cacffec8289741d51a + languageName: node + linkType: hard + "nodemailer@npm:^6.9.13": version: 6.9.13 resolution: "nodemailer@npm:6.9.13" @@ -33884,8 +35033,8 @@ __metadata: linkType: hard "nodemon@npm:^3.0.1": - version: 3.1.0 - resolution: "nodemon@npm:3.1.0" + version: 3.1.3 + resolution: "nodemon@npm:3.1.3" dependencies: chokidar: ^3.5.2 debug: ^4 @@ -33899,7 +35048,7 @@ __metadata: undefsafe: ^2.0.5 bin: nodemon: bin/nodemon.js - checksum: 0b721f66ee60d9bf092f6101965bc65769698fa2921d0283d90bbf3f0906aa4f3ac77316682375bd7f09c91679fddb131aa39f9fc839fea57061bbc8e81b60e3 + checksum: ac2fa8865ab292b7ddf66731487acca4b4282b2728361e0de633c0c74cd705d6a0852b52f785c09469d959241d038ba824f50375622e687a2a318be747d9cd9d languageName: node linkType: hard @@ -35213,6 +36362,13 @@ __metadata: languageName: node linkType: hard +"patch-console@npm:^1.0.0": + version: 1.0.0 + resolution: "patch-console@npm:1.0.0" + checksum: 8cd738aa470f2e9463fca35da6a19403384ac555004f698ddd3dfdb69135ab60fe9bd2edd1dbdd8c09d92c0a2190fd0f7337fe48123013baf8ffec8532885a3a + languageName: node + linkType: hard + "patch-package@npm:^8.0.0": version: 8.0.0 resolution: "patch-package@npm:8.0.0" @@ -35592,7 +36748,7 @@ __metadata: languageName: node linkType: hard -"pidtree@npm:0.6.0": +"pidtree@npm:~0.6.0": version: 0.6.0 resolution: "pidtree@npm:0.6.0" bin: @@ -36860,6 +38016,19 @@ __metadata: languageName: node linkType: hard +"qunit@npm:^2.8.0": + version: 2.20.1 + resolution: "qunit@npm:2.20.1" + dependencies: + commander: 7.2.0 + node-watch: 0.7.3 + tiny-glob: 0.2.9 + bin: + qunit: bin/qunit.js + checksum: ee0db04239ff7ae1ff9b67f15ab31e8c4f1e41bb5d6a41f34b19b17f836bc9414e43897516a617d08840eaa2bd71580c456e7e448d81c388dfb58198f0c26de7 + languageName: node + linkType: hard + "raf-schd@npm:^4.0.2": version: 4.0.2 resolution: "raf-schd@npm:4.0.2" @@ -37104,6 +38273,16 @@ __metadata: languageName: node linkType: hard +"react-devtools-core@npm:^4.19.1": + version: 4.28.5 + resolution: "react-devtools-core@npm:4.28.5" + dependencies: + shell-quote: ^1.6.1 + ws: ^7 + checksum: d8e4b32ffcfe1ada5c9f7decffd04afc4707a3d6261953a92b8aed1c8abe15cd57d6eb4ce711f842180a2f5c60d2947209e3c1202f7ea29303ee150c55da59e0 + languageName: node + linkType: hard + "react-dom@npm:^16.13.1": version: 16.14.0 resolution: "react-dom@npm:16.14.0" @@ -37336,6 +38515,19 @@ __metadata: languageName: node linkType: hard +"react-reconciler@npm:^0.26.2": + version: 0.26.2 + resolution: "react-reconciler@npm:0.26.2" + dependencies: + loose-envify: ^1.1.0 + object-assign: ^4.1.1 + scheduler: ^0.20.2 + peerDependencies: + react: ^17.0.2 + checksum: 2ebceace56f547f51eaf142becefef9cca980eae4f42d90ee5a966f54a375f5082d78b71b00c40bbd9bca69e0e0f698c7d4e81cc7373437caa19831fddc1d01b + languageName: node + linkType: hard + "react-redux@npm:^7.1.1": version: 7.2.5 resolution: "react-redux@npm:7.2.5" @@ -37681,6 +38873,17 @@ __metadata: languageName: node linkType: hard +"react@npm:^16.13.1": + version: 16.14.0 + resolution: "react@npm:16.14.0" + dependencies: + loose-envify: ^1.1.0 + object-assign: ^4.1.1 + prop-types: ^15.6.2 + checksum: 8484f3ecb13414526f2a7412190575fc134da785c02695eb92bb6028c930bfe1c238d7be2a125088fec663cc7cda0a3623373c46807cf2c281f49c34b79881ac + languageName: node + linkType: hard + "react@npm:^18.0.2": version: 18.2.0 resolution: "react@npm:18.2.0" @@ -37839,6 +39042,13 @@ __metadata: languageName: node linkType: hard +"readline-sync@npm:1.4.9": + version: 1.4.9 + resolution: "readline-sync@npm:1.4.9" + checksum: cedd48f422bb4c8736cbd44332bea9d3367302d0e2a63c064b97e909bbcd203099d483cee4fff4e78614a1c3c1d5a62f1c63bf2c013483072ec1ff1c8a2c5129 + languageName: node + linkType: hard + "recast@npm:^0.20.3": version: 0.20.4 resolution: "recast@npm:0.20.4" @@ -38489,10 +39699,10 @@ __metadata: languageName: node linkType: hard -"rfdc@npm:^1.3.0": - version: 1.3.0 - resolution: "rfdc@npm:1.3.0" - checksum: fb2ba8512e43519983b4c61bd3fa77c0f410eff6bae68b08614437bc3f35f91362215f7b4a73cbda6f67330b5746ce07db5dd9850ad3edc91271ad6deea0df32 +"rfdc@npm:^1.3.1": + version: 1.3.1 + resolution: "rfdc@npm:1.3.1" + checksum: d5d1e930aeac7e0e0a485f97db1356e388bdbeff34906d206fe524dd5ada76e95f186944d2e68307183fdc39a54928d4426bbb6734851692cfe9195efba58b79 languageName: node linkType: hard @@ -38793,7 +40003,7 @@ __metadata: shx: ^0.3.2 sloc: ^0.3.1 sort-package-json: ^2.8.0 - typescript: ~5.1.0 + typescript: ~5.2.0 languageName: unknown linkType: soft @@ -38981,6 +40191,16 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.20.2": + version: 0.20.2 + resolution: "scheduler@npm:0.20.2" + dependencies: + loose-envify: ^1.1.0 + object-assign: ^4.1.1 + checksum: c4b35cf967c8f0d3e65753252d0f260271f81a81e427241295c5a7b783abf4ea9e905f22f815ab66676f5313be0a25f47be582254db8f9241b259213e999b8fc + languageName: node + linkType: hard + "scheduler@npm:^0.23.0": version: 0.23.0 resolution: "scheduler@npm:0.23.0" @@ -39123,7 +40343,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.4.0, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": +"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.2, semver@npm:^7.1.3, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.4.0, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": version: 7.6.0 resolution: "semver@npm:7.6.0" dependencies: @@ -39376,7 +40596,7 @@ __metadata: languageName: node linkType: hard -"shell-quote@npm:^1.7.3, shell-quote@npm:^1.8.1": +"shell-quote@npm:^1.6.1, shell-quote@npm:^1.7.3, shell-quote@npm:^1.8.1": version: 1.8.1 resolution: "shell-quote@npm:1.8.1" checksum: 5f01201f4ef504d4c6a9d0d283fa17075f6770bfbe4c5850b074974c68062f37929ca61700d95ad2ac8822e14e8c4b990ca0e6e9272e64befd74ce5e19f0736b @@ -39549,6 +40769,17 @@ __metadata: languageName: node linkType: hard +"slice-ansi@npm:^3.0.0": + version: 3.0.0 + resolution: "slice-ansi@npm:3.0.0" + dependencies: + ansi-styles: ^4.0.0 + astral-regex: ^2.0.0 + is-fullwidth-code-point: ^3.0.0 + checksum: 5ec6d022d12e016347e9e3e98a7eb2a592213a43a65f1b61b74d2c78288da0aded781f665807a9f3876b9daa9ad94f64f77d7633a0458876c3a4fdc4eb223f24 + languageName: node + linkType: hard + "slice-ansi@npm:^4.0.0": version: 4.0.0 resolution: "slice-ansi@npm:4.0.0" @@ -40002,6 +41233,15 @@ __metadata: languageName: node linkType: hard +"ssri@npm:^6.0.1": + version: 6.0.2 + resolution: "ssri@npm:6.0.2" + dependencies: + figgy-pudding: ^3.5.1 + checksum: 7c2e5d442f6252559c8987b7114bcf389fe5614bf65de09ba3e6f9a57b9b65b2967de348fcc3acccff9c069adb168140dd2c5fc2f6f4a779e604a27ef1f7d551 + languageName: node + linkType: hard + "ssri@npm:^8.0.0, ssri@npm:^8.0.1": version: 8.0.1 resolution: "ssri@npm:8.0.1" @@ -40043,12 +41283,12 @@ __metadata: languageName: node linkType: hard -"stack-utils@npm:^2.0.3": - version: 2.0.5 - resolution: "stack-utils@npm:2.0.5" +"stack-utils@npm:^2.0.2, stack-utils@npm:^2.0.3": + version: 2.0.6 + resolution: "stack-utils@npm:2.0.6" dependencies: escape-string-regexp: ^2.0.0 - checksum: 76b69da0f5b48a34a0f93c98ee2a96544d2c4ca2557f7eef5ddb961d3bdc33870b46f498a84a7c4f4ffb781df639840e7ebf6639164ed4da5e1aeb659615b9c7 + checksum: 052bf4d25bbf5f78e06c1d5e67de2e088b06871fa04107ca8d3f0e9d9263326e2942c8bedee3545795fc77d787d443a538345eef74db2f8e35db3558c6f91ff7 languageName: node linkType: hard @@ -40268,7 +41508,7 @@ __metadata: languageName: node linkType: hard -"string-argv@npm:0.3.2, string-argv@npm:~0.3.1": +"string-argv@npm:~0.3.1, string-argv@npm:~0.3.2": version: 0.3.2 resolution: "string-argv@npm:0.3.2" checksum: 8703ad3f3db0b2641ed2adbb15cf24d3945070d9a751f9e74a924966db9f325ac755169007233e8985a39a6a292f14d4fee20482989b89b96e473c4221508a0f @@ -40841,7 +42081,7 @@ __metadata: languageName: node linkType: hard -"swr@npm:^2.0.0": +"swr@npm:^2.0.0, swr@npm:^2.2.5": version: 2.2.5 resolution: "swr@npm:2.2.5" dependencies: @@ -40922,7 +42162,7 @@ __metadata: languageName: node linkType: hard -"tar-stream@npm:^2.0.0, tar-stream@npm:^2.1.4, tar-stream@npm:^2.2.0": +"tar-stream@npm:^2.0.0, tar-stream@npm:^2.0.1, tar-stream@npm:^2.1.4, tar-stream@npm:^2.2.0": version: 2.2.0 resolution: "tar-stream@npm:2.2.0" dependencies: @@ -40946,7 +42186,7 @@ __metadata: languageName: node linkType: hard -"tar@npm:^6.0.2, tar@npm:^6.1.0, tar@npm:^6.1.11, tar@npm:^6.1.12, tar@npm:^6.1.2": +"tar@npm:^6.0.2, tar@npm:^6.0.5, tar@npm:^6.1.0, tar@npm:^6.1.11, tar@npm:^6.1.12, tar@npm:^6.1.2": version: 6.2.1 resolution: "tar@npm:6.2.1" dependencies: @@ -40967,6 +42207,16 @@ __metadata: languageName: node linkType: hard +"tau-prolog@npm:^0.2.66": + version: 0.2.81 + resolution: "tau-prolog@npm:0.2.81" + dependencies: + qunit: ^2.8.0 + readline-sync: 1.4.9 + checksum: 9771c020fe37e821f9e5321ef26f70728f885077c5885d4e8b06fd9409205b1d0ad9fd9ac02d66e56e062ce8e8bbc07a9bf8c3791b5023bfe72ed04c598dd3ef + languageName: node + linkType: hard + "tdigest@npm:^0.1.1": version: 0.1.1 resolution: "tdigest@npm:0.1.1" @@ -41231,6 +42481,16 @@ __metadata: languageName: node linkType: hard +"tiny-glob@npm:0.2.9": + version: 0.2.9 + resolution: "tiny-glob@npm:0.2.9" + dependencies: + globalyzer: 0.1.0 + globrex: ^0.1.2 + checksum: aea5801eb6663ddf77ebb74900b8f8bd9dfcfc9b6a1cc8018cb7421590c00bf446109ff45e4b64a98e6c95ddb1255a337a5d488fb6311930e2a95334151ec9c6 + languageName: node + linkType: hard + "tiny-invariant@npm:^1.0.6": version: 1.3.1 resolution: "tiny-invariant@npm:1.3.1" @@ -41245,6 +42505,13 @@ __metadata: languageName: node linkType: hard +"tinylogic@npm:^2.0.0": + version: 2.0.0 + resolution: "tinylogic@npm:2.0.0" + checksum: b966cbb41241a048095fb9e685d5e2020475fdea2c65b4ae51e5dee48964860a4505d987503c004b8a76e96b64c7da2f49954dd36c691d559c315d878ce7da29 + languageName: node + linkType: hard + "tmp@npm:^0.0.33": version: 0.0.33 resolution: "tmp@npm:0.0.33" @@ -41468,6 +42735,13 @@ __metadata: languageName: node linkType: hard +"treeify@npm:^1.1.0": + version: 1.1.0 + resolution: "treeify@npm:1.1.0" + checksum: aa00dded220c1dd052573bd6fc2c52862f09870851a284f0d3650d72bf913ba9b4f6b824f4f1ab81899bae29375f4266b07fe47cbf82343a1efa13cc09ce87af + languageName: node + linkType: hard + "treeverse@npm:^1.0.4": version: 1.0.4 resolution: "treeverse@npm:1.0.4" @@ -41709,6 +42983,13 @@ __metadata: languageName: node linkType: hard +"typanion@npm:^3.14.0, typanion@npm:^3.8.0": + version: 3.14.0 + resolution: "typanion@npm:3.14.0" + checksum: fc0590d02c13c659eb1689e8adf7777e6c00dc911377e44cd36fe1b1271cfaca71547149f12cdc275058c0de5562a14e5273adbae66d47e6e0320e36007f5912 + languageName: node + linkType: hard + "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" @@ -41734,6 +43015,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^0.12.0": + version: 0.12.0 + resolution: "type-fest@npm:0.12.0" + checksum: 407d6c1a6fcc907f6124c37e977ba4966205014787a32a27579da6e47c3b1bd210b68cc1c7764d904c8aa55fb4efa6945586f9b4fae742c63ed026a4559da07d + languageName: node + linkType: hard + "type-fest@npm:^0.13.1": version: 0.13.1 resolution: "type-fest@npm:0.13.1" @@ -41741,6 +43029,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^0.15.1": + version: 0.15.1 + resolution: "type-fest@npm:0.15.1" + checksum: a1a0cdbd7f802d9784324f185df055739e97424ecb60914e9025574a4bc07e4a063c152e4510ebf5989de8a263220de1f6b5cf1b05f0b333dbd5b21d9b4a271b + languageName: node + linkType: hard + "type-fest@npm:^0.20.2": version: 0.20.2 resolution: "type-fest@npm:0.20.2" @@ -41947,6 +43242,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:~5.2.0": + version: 5.2.2 + resolution: "typescript@npm:5.2.2" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 7912821dac4d962d315c36800fe387cdc0a6298dba7ec171b350b4a6e988b51d7b8f051317786db1094bd7431d526b648aba7da8236607febb26cf5b871d2d3c + languageName: node + linkType: hard + "typescript@patch:typescript@~5.0.4#~builtin": version: 5.0.4 resolution: "typescript@patch:typescript@npm%3A5.0.4#~builtin::version=5.0.4&hash=b5f058" @@ -41967,6 +43272,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@~5.2.0#~builtin": + version: 5.2.2 + resolution: "typescript@patch:typescript@npm%3A5.2.2#~builtin::version=5.2.2&hash=f3b441" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 0f4da2f15e6f1245e49db15801dbee52f2bbfb267e1c39225afdab5afee1a72839cd86000e65ee9d7e4dfaff12239d28beaf5ee431357fcced15fb08583d72ca + languageName: node + linkType: hard + "ua-parser-js@npm:^0.7.30": version: 0.7.33 resolution: "ua-parser-js@npm:0.7.33" @@ -43579,7 +44894,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7.4.6": +"ws@npm:^7, ws@npm:^7.4.6, ws@npm:^7.5.5": version: 7.5.9 resolution: "ws@npm:7.5.9" peerDependencies: @@ -43793,13 +45108,6 @@ __metadata: languageName: node linkType: hard -"yaml@npm:2.3.4": - version: 2.3.4 - resolution: "yaml@npm:2.3.4" - checksum: e6d1dae1c6383bcc8ba11796eef3b8c02d5082911c6723efeeb5ba50fc8e881df18d645e64de68e421b577296000bea9c75d6d9097c2f6699da3ae0406c030d8 - languageName: node - linkType: hard - "yaml@npm:^1.10.0, yaml@npm:^1.10.2, yaml@npm:^1.7.2": version: 1.10.2 resolution: "yaml@npm:1.10.2" @@ -43807,12 +45115,12 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.0.0, yaml@npm:^2.0.0-10, yaml@npm:^2.1.1, yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.3.2, yaml@npm:^2.3.3": - version: 2.4.1 - resolution: "yaml@npm:2.4.1" +"yaml@npm:^2.0.0, yaml@npm:^2.0.0-10, yaml@npm:^2.1.1, yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.3.2, yaml@npm:^2.3.3, yaml@npm:~2.4.2": + version: 2.4.2 + resolution: "yaml@npm:2.4.2" bin: yaml: bin.mjs - checksum: 4c391d07a5d5e935e058babb71026c9cdc9a6fd889e35dd91b53cfb0a12691b67c6c5c740858e71345fef18cd9c13c554a6dda9196f59820d769d94041badb0b + checksum: 90dda4485de04367251face9abb5c36927c94e44078f4e958e6468a07e74e7e92f89be20fc49860b6268c51ee5a5fc79ef89197d3f874bf24ef8921cc4ba9013 languageName: node linkType: hard @@ -43889,6 +45197,22 @@ __metadata: languageName: node linkType: hard +"yarn-plugin-backstage@workspace:packages/yarn-plugin": + version: 0.0.0-use.local + resolution: "yarn-plugin-backstage@workspace:packages/yarn-plugin" + dependencies: + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/release-manifests": "workspace:^" + "@yarnpkg/builder": ^4.0.0 + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/fslib": ^3.0.2 + nodemon: ^3.0.1 + semver: ^7.6.0 + languageName: unknown + linkType: soft + "yauzl@npm:^3.0.0": version: 3.1.3 resolution: "yauzl@npm:3.1.3" @@ -43977,6 +45301,15 @@ __metadata: languageName: node linkType: hard +"yoga-layout-prebuilt@npm:^1.9.6": + version: 1.10.0 + resolution: "yoga-layout-prebuilt@npm:1.10.0" + dependencies: + "@types/yoga-layout": 1.9.2 + checksum: 6954c7c7b04c585a1c974391bea4734611adb85702b5e9131549a1d3dc5b94e69bcfea34121cdaeb5e702663bf290fcce5374910128e54d1031503a57c062865 + languageName: node + linkType: hard + "yup@npm:*, yup@npm:^1.0.0": version: 1.4.0 resolution: "yup@npm:1.4.0"