diff --git a/.changeset/add-query-catalog-entities-action.md b/.changeset/add-query-catalog-entities-action.md new file mode 100644 index 0000000000..78fb30d584 --- /dev/null +++ b/.changeset/add-query-catalog-entities-action.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Added `query-catalog-entities` action to the catalog backend actions registry. Supports predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. diff --git a/.changeset/add-who-am-i-action.md b/.changeset/add-who-am-i-action.md new file mode 100644 index 0000000000..5c1008a7e1 --- /dev/null +++ b/.changeset/add-who-am-i-action.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added `who-am-i` action to the auth backend actions registry. Returns the catalog entity and user info for the currently authenticated user. diff --git a/.changeset/bright-items-see.md b/.changeset/bright-items-see.md new file mode 100644 index 0000000000..ea3ba17cdf --- /dev/null +++ b/.changeset/bright-items-see.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Fixed `generate-catalog-info` command failing with "too many arguments" when invoked by lint-staged via the pre-commit hook. diff --git a/.changeset/brown-rings-sort.md b/.changeset/brown-rings-sort.md new file mode 100644 index 0000000000..045f2fbd3b --- /dev/null +++ b/.changeset/brown-rings-sort.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Added interactive support to the `Card` component. Pass `onPress` to make the entire card surface pressable, or `href` to make it navigate to a URL. A transparent overlay handles the interaction while nested buttons and links remain independently clickable. diff --git a/.changeset/bumpy-colts-teach.md b/.changeset/bumpy-colts-teach.md new file mode 100644 index 0000000000..e3457127ce --- /dev/null +++ b/.changeset/bumpy-colts-teach.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Fixed neutral-1 hover & pressed state in light mode. diff --git a/.changeset/bumpy-keys-pay.md b/.changeset/bumpy-keys-pay.md new file mode 100644 index 0000000000..fdd698fd7b --- /dev/null +++ b/.changeset/bumpy-keys-pay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added new `gitlab:group:access` scaffolder action to add or remove users and groups as members of GitLab groups. The action supports specifying members via `userIds` and/or `groupIds` array parameters, configurable access levels (Guest, Reporter, Developer, Maintainer, Owner), and defaults to the 'add' action when not specified. diff --git a/.changeset/by-refs-predicate-backend.md b/.changeset/by-refs-predicate-backend.md new file mode 100644 index 0000000000..a2a0346122 --- /dev/null +++ b/.changeset/by-refs-predicate-backend.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Added support for predicate-based filtering on the `/entities/by-refs` endpoint via the `query` field in the request body. Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. diff --git a/.changeset/by-refs-predicate-client.md b/.changeset/by-refs-predicate-client.md new file mode 100644 index 0000000000..5ed136fbe1 --- /dev/null +++ b/.changeset/by-refs-predicate-client.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': minor +--- + +Added support for the `query` field in `getEntitiesByRefs` requests, enabling predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. diff --git a/.changeset/calm-vans-play.md b/.changeset/calm-vans-play.md new file mode 100644 index 0000000000..0b7571da7b --- /dev/null +++ b/.changeset/calm-vans-play.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Migrated all components from `useStyles` to `useDefinition` hook. Exported `OwnProps` types for each component, enabling better type composition for consumers. + +**Affected components:** Avatar, Checkbox, Container, Dialog, FieldError, FieldLabel, Flex, FullPage, Grid, HeaderPage, Link, Menu, PasswordField, PluginHeader, Popover, RadioGroup, SearchField, Select, Skeleton, Switch, Table, TablePagination, Tabs, TagGroup, Text, TextField, ToggleButton, ToggleButtonGroup, Tooltip, VisuallyHidden diff --git a/.changeset/chatty-wasps-sink.md b/.changeset/chatty-wasps-sink.md new file mode 100644 index 0000000000..7f12c5372c --- /dev/null +++ b/.changeset/chatty-wasps-sink.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Removed the `transition` on `Container` padding to prevent an unwanted animation when the viewport is resized. + +Affected components: Container diff --git a/.changeset/clean-up-own-root-dir.md b/.changeset/clean-up-own-root-dir.md new file mode 100644 index 0000000000..c668073688 --- /dev/null +++ b/.changeset/clean-up-own-root-dir.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-common': patch +--- + +The `findOwnRootDir` utility now searches for the monorepo root by traversing up the directory tree looking for a `package.json` with `workspaces`, instead of assuming a fixed `../..` relative path. If no workspaces root is found during this traversal, `findOwnRootDir` now throws to enforce stricter validation of the repository layout. diff --git a/.changeset/cli-common-runcheck-stdio-ignore.md b/.changeset/cli-common-runcheck-stdio-ignore.md new file mode 100644 index 0000000000..1d2986eaf0 --- /dev/null +++ b/.changeset/cli-common-runcheck-stdio-ignore.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-common': patch +--- + +Fixed `runCheck` to ignore stdio of the spawned process, preventing unwanted output from leaking to the terminal. diff --git a/.changeset/cli-execute-loader.md b/.changeset/cli-execute-loader.md new file mode 100644 index 0000000000..ce9665e01d --- /dev/null +++ b/.changeset/cli-execute-loader.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Internal refactor of CLI modularization, moving individual commands to be implemented with cleye. diff --git a/.changeset/cli-internalize-lib-modules.md b/.changeset/cli-internalize-lib-modules.md new file mode 100644 index 0000000000..f9ef219a3a --- /dev/null +++ b/.changeset/cli-internalize-lib-modules.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Internal refactor to move shared utilities into their consuming modules, reducing cross-module dependencies. diff --git a/.changeset/cli-node-lockfile-tostring.md b/.changeset/cli-node-lockfile-tostring.md new file mode 100644 index 0000000000..bf4fc31225 --- /dev/null +++ b/.changeset/cli-node-lockfile-tostring.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-node': patch +--- + +Added `toString()` method to `Lockfile` for serializing lockfiles back to string format. diff --git a/.changeset/cli-remove-migrate-package-exports.md b/.changeset/cli-remove-migrate-package-exports.md new file mode 100644 index 0000000000..03ad1974d8 --- /dev/null +++ b/.changeset/cli-remove-migrate-package-exports.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': minor +--- + +**BREAKING**: The `migrate package-exports` command has been removed. Use `repo fix` instead. diff --git a/.changeset/cli-report-parser-cleye.md b/.changeset/cli-report-parser-cleye.md new file mode 100644 index 0000000000..f8d9141c66 --- /dev/null +++ b/.changeset/cli-report-parser-cleye.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Updated the CLI report parser to support cleye-style help output, and strip ANSI escape codes from captured output. diff --git a/.changeset/cli-split-loadCliConfig.md b/.changeset/cli-split-loadCliConfig.md new file mode 100644 index 0000000000..d9c3571fe0 --- /dev/null +++ b/.changeset/cli-split-loadCliConfig.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Internal refactor to split `loadCliConfig` into separate implementations for the build and config CLI modules, removing a cross-module dependency. diff --git a/.changeset/cli-versioning-consolidation.md b/.changeset/cli-versioning-consolidation.md new file mode 100644 index 0000000000..114b931c9b --- /dev/null +++ b/.changeset/cli-versioning-consolidation.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Migrated internal versioning utilities to use `@backstage/cli-node` instead of a local implementation. diff --git a/.changeset/cold-dodos-think.md b/.changeset/cold-dodos-think.md new file mode 100644 index 0000000000..021e5f679e --- /dev/null +++ b/.changeset/cold-dodos-think.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-devtools': patch +--- + +Fixed URL encoding of task IDs for the trigger feature (tasks that contained a "/" in their ID were not triggered) diff --git a/.changeset/crazy-ravens-reply.md b/.changeset/crazy-ravens-reply.md new file mode 100644 index 0000000000..d52b62880e --- /dev/null +++ b/.changeset/crazy-ravens-reply.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-techdocs-node': patch +--- + +Added `techdocs.generator.mkdocs.dangerouslyAllowAdditionalKeys` configuration option to explicitly bypass MkDocs configuration key restrictions. This enables support for additional MkDocs configuration keys beyond the default safe allow list, such as the `hooks` key which some MkDocs plugins require. diff --git a/.changeset/cyan-facts-doubt.md b/.changeset/cyan-facts-doubt.md new file mode 100644 index 0000000000..7597a39c6d --- /dev/null +++ b/.changeset/cyan-facts-doubt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-node': patch +--- + +Removed `INHERIT` from the `ALLOWED_MKDOCS_KEYS` set to address a security concern with MkDocs configuration inheritance. diff --git a/.changeset/cyan-falcons-serve.md b/.changeset/cyan-falcons-serve.md new file mode 100644 index 0000000000..82a13b4fb3 --- /dev/null +++ b/.changeset/cyan-falcons-serve.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Removed check for deprecated `bitbucket` integration from `repoPickerValidation` function used by the `RepoUrlPicker`, it now validates the `bitbucketServer` and `bitbucketCloud` integrations instead. diff --git a/.changeset/dark-snakes-nail.md b/.changeset/dark-snakes-nail.md new file mode 100644 index 0000000000..d4eb6f0dd2 --- /dev/null +++ b/.changeset/dark-snakes-nail.md @@ -0,0 +1,21 @@ +--- +'@backstage/ui': minor +--- + +**BREAKING**: Simplified the neutral background prop API for container components. The explicit `neutral-1`, `neutral-2`, `neutral-3`, and `neutral-auto` values have been removed from `ProviderBg`. They are replaced by a single `'neutral'` value that always auto-increments from the parent context, making it impossible to skip or pin to an explicit neutral level. + +**Migration:** + +Replace any explicit `bg="neutral-1"`, `bg="neutral-2"`, `bg="neutral-3"`, or `bg="neutral-auto"` props with `bg="neutral"`. To achieve a specific neutral level in stories or tests, use nested containers — each additional `bg="neutral"` wrapper increments by one level. + +```tsx +// Before +... + +// After + + ... + +``` + +**Affected components:** Box, Flex, Grid, Card, Accordion, Popover, Tooltip, Dialog, Menu diff --git a/.changeset/deep-files-clap.md b/.changeset/deep-files-clap.md new file mode 100644 index 0000000000..a3ab54eb63 --- /dev/null +++ b/.changeset/deep-files-clap.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': minor +--- + +The scheduler service now uses the metrics service to create metrics, providing plugin-scoped attribution. diff --git a/.changeset/deep-moments-deny.md b/.changeset/deep-moments-deny.md new file mode 100644 index 0000000000..21de414350 --- /dev/null +++ b/.changeset/deep-moments-deny.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': minor +--- + +Added support for OpenAPI 3.1 to all `schema openapi` commands. The commands now auto-detect the OpenAPI version from the spec file and use the appropriate generator, supporting both OpenAPI 3.0.x and 3.1.x specifications. diff --git a/.changeset/early-baboons-roll.md b/.changeset/early-baboons-roll.md new file mode 100644 index 0000000000..bdd55ec507 --- /dev/null +++ b/.changeset/early-baboons-roll.md @@ -0,0 +1,16 @@ +--- +'@backstage/ui': minor +--- + +**BREAKING**: Removed `--bui-bg-popover` CSS token. Popover, Tooltip, Menu, and Dialog now use `--bui-bg-app` for their outer shell and `Box bg="neutral-1"` for content areas, providing better theme consistency and eliminating a redundant token. + +**Migration:** + +Replace any usage of `--bui-bg-popover` with `--bui-bg-neutral-1` (for content surfaces) or `--bui-bg-app` (for outer shells): + +```diff +- background: var(--bui-bg-popover); ++ background: var(--bui-bg-neutral-1); +``` + +**Affected components:** Popover, Tooltip, Menu, Dialog diff --git a/.changeset/entity-content-group-api-docs.md b/.changeset/entity-content-group-api-docs.md new file mode 100644 index 0000000000..8eb3e12435 --- /dev/null +++ b/.changeset/entity-content-group-api-docs.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Added default entity content groups for the API docs entity content tabs. The API definition tab defaults to the `documentation` group and the APIs tab defaults to the `development` group. diff --git a/.changeset/entity-content-group-techdocs.md b/.changeset/entity-content-group-techdocs.md new file mode 100644 index 0000000000..a99992301c --- /dev/null +++ b/.changeset/entity-content-group-techdocs.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Added `documentation` as the default entity content group for the TechDocs entity content tab. diff --git a/.changeset/entity-page-group-aliases-and-ordering-catalog.md b/.changeset/entity-page-group-aliases-and-ordering-catalog.md new file mode 100644 index 0000000000..5db7c0fc4b --- /dev/null +++ b/.changeset/entity-page-group-aliases-and-ordering-catalog.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': minor +--- + +Added support for group alias IDs and configurable content ordering on the entity page. Groups can now declare `aliases` so that content targeting an aliased group is included in the group. A new `defaultContentOrder` option (default `title`) controls how content items within each group are sorted, with support for both a page-level default and per-group overrides. diff --git a/.changeset/entity-page-group-aliases-and-ordering.md b/.changeset/entity-page-group-aliases-and-ordering.md new file mode 100644 index 0000000000..35b18063d9 --- /dev/null +++ b/.changeset/entity-page-group-aliases-and-ordering.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +Added `aliases` and `contentOrder` fields to `EntityContentGroupDefinition`, allowing groups to declare alias IDs and control the sort order of their content items. diff --git a/.changeset/facets-predicate-backend.md b/.changeset/facets-predicate-backend.md new file mode 100644 index 0000000000..9ecdef11b6 --- /dev/null +++ b/.changeset/facets-predicate-backend.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Added support for predicate-based filtering on the `/entity-facets` endpoint via a new `POST` method. Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. diff --git a/.changeset/facets-predicate-client.md b/.changeset/facets-predicate-client.md new file mode 100644 index 0000000000..5493d7c82a --- /dev/null +++ b/.changeset/facets-predicate-client.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': minor +--- + +Added support for the `query` field in `getEntityFacets` requests, enabling predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. diff --git a/.changeset/fine-poems-sit.md b/.changeset/fine-poems-sit.md new file mode 100644 index 0000000000..a6a92c4027 --- /dev/null +++ b/.changeset/fine-poems-sit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Removed `@backstage/plugin-scaffolder-backend-module-bitbucket` from `package.json` as the package itself has been deprecated and the code deleted. diff --git a/.changeset/fix-entity-tab-group-ordering.md b/.changeset/fix-entity-tab-group-ordering.md new file mode 100644 index 0000000000..6fab3f77af --- /dev/null +++ b/.changeset/fix-entity-tab-group-ordering.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Fixed entity page tab groups not respecting the ordering from the `groups` configuration. diff --git a/.changeset/fix-form-fields-api-export-react.md b/.changeset/fix-form-fields-api-export-react.md new file mode 100644 index 0000000000..787131280a --- /dev/null +++ b/.changeset/fix-form-fields-api-export-react.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Added back `formFieldsApiRef` and `ScaffolderFormFieldsApi` as alpha exports. diff --git a/.changeset/fix-mcp-error-forwarding.md b/.changeset/fix-mcp-error-forwarding.md new file mode 100644 index 0000000000..fa12e224a9 --- /dev/null +++ b/.changeset/fix-mcp-error-forwarding.md @@ -0,0 +1,7 @@ +--- +'@backstage/backend-defaults': patch +'@backstage/backend-test-utils': patch +'@backstage/plugin-mcp-actions-backend': patch +--- + +Fixed error forwarding in the actions registry so that known errors like `InputError` and `NotFoundError` thrown by actions preserve their original status codes and messages instead of being wrapped in `ForwardedError` and coerced to 500. diff --git a/.changeset/fix-mui-styles-shared-dep.md b/.changeset/fix-mui-styles-shared-dep.md new file mode 100644 index 0000000000..0154c15559 --- /dev/null +++ b/.changeset/fix-mui-styles-shared-dep.md @@ -0,0 +1,5 @@ +--- +'@backstage/module-federation-common': patch +--- + +Fixed the `@mui/material/styles` shared dependency key by removing a trailing slash that caused module resolution failures with MUI package exports. diff --git a/.changeset/fix-scaffolder-secret-redaction-bypass.md b/.changeset/fix-scaffolder-secret-redaction-bypass.md new file mode 100644 index 0000000000..9771c6b80e --- /dev/null +++ b/.changeset/fix-scaffolder-secret-redaction-bypass.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fixed a security vulnerability where secrets could bypass log redaction when transformed through Nunjucks filters in scaffolder templates. diff --git a/.changeset/fix-scm-url-path-traversal.md b/.changeset/fix-scm-url-path-traversal.md new file mode 100644 index 0000000000..e2b6ddab0f --- /dev/null +++ b/.changeset/fix-scm-url-path-traversal.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Fixed a security vulnerability where path traversal sequences in SCM URLs could be used to access unintended API endpoints using server-side integration credentials. diff --git a/.changeset/fix-tab-match-query-params.md b/.changeset/fix-tab-match-query-params.md new file mode 100644 index 0000000000..6e01a202f5 --- /dev/null +++ b/.changeset/fix-tab-match-query-params.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Fixed tab `matchStrategy` matching to ignore query parameters and hash fragments in tab `href` values. Previously, tabs with query params in their `href` (e.g., `/page?group=foo`) would never show as active since matching compared the full `href` string against `location.pathname` which never includes query params. + +**Affected components:** Tabs, PluginHeader diff --git a/.changeset/fix-test-api-pair-inference.md b/.changeset/fix-test-api-pair-inference.md new file mode 100644 index 0000000000..d527a27600 --- /dev/null +++ b/.changeset/fix-test-api-pair-inference.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Fixed type inference of `TestApiPair` when using tuple syntax by wrapping `MockWithApiFactory` in `NoInfer`. diff --git a/.changeset/free-pigs-itch.md b/.changeset/free-pigs-itch.md new file mode 100644 index 0000000000..c1e22f686e --- /dev/null +++ b/.changeset/free-pigs-itch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Added a new `list-scaffolder-tasks` action that allows querying scaffolder tasks with optional ownership filtering and pagination support diff --git a/.changeset/giant-singers-rush.md b/.changeset/giant-singers-rush.md new file mode 100644 index 0000000000..e1f0493caa --- /dev/null +++ b/.changeset/giant-singers-rush.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Fixed handling of the `style` prop on `Button`, `ButtonIcon`, and `ButtonLink` so that it is now correctly forwarded to the underlying element instead of being silently dropped. + +**Affected components:** Button, ButtonIcon, ButtonLink diff --git a/.changeset/github-org-verified-emails.md b/.changeset/github-org-verified-emails.md new file mode 100644 index 0000000000..b825395556 --- /dev/null +++ b/.changeset/github-org-verified-emails.md @@ -0,0 +1,34 @@ +--- +'@backstage/plugin-catalog-backend-module-github': minor +--- + +The default user transformer now prefers organization verified domain emails over the user's public GitHub email when populating the user entity profile. It also strips plus-addressed routing tags that GitHub adds to these emails. + +If you want to retain the old behavior, you can do so with a custom user transformer using the `githubOrgEntityProviderTransformsExtensionPoint`: + +```ts +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { githubOrgEntityProviderTransformsExtensionPoint } from '@backstage/plugin-catalog-backend-module-github-org'; +import { defaultUserTransformer } from '@backstage/plugin-catalog-backend-module-github'; + +export default createBackendModule({ + pluginId: 'catalog', + moduleId: 'github-org-custom-transforms', + register(env) { + env.registerInit({ + deps: { + transforms: githubOrgEntityProviderTransformsExtensionPoint, + }, + async init({ transforms }) { + transforms.setUserTransformer(async (item, ctx) => { + const entity = await defaultUserTransformer(item, ctx); + if (entity && item.email) { + entity.spec.profile!.email = item.email; + } + return entity; + }); + }, + }); + }, +}); +``` diff --git a/.changeset/heavy-baths-mix.md b/.changeset/heavy-baths-mix.md new file mode 100644 index 0000000000..8f5409e317 --- /dev/null +++ b/.changeset/heavy-baths-mix.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Fixed Link variant default from `'body'` to `'body-medium'` to match actual CSS selectors. The previous default did not correspond to a valid variant value. + +**Affected components:** Link diff --git a/.changeset/itchy-words-crash.md b/.changeset/itchy-words-crash.md new file mode 100644 index 0000000000..a37ea74477 --- /dev/null +++ b/.changeset/itchy-words-crash.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Fixed Azure integration config schema visibility annotations to use per-field `@visibility secret` instead of `@deepVisibility secret` on parent objects, so that non-secret fields like `clientId`, `tenantId`, `organizations`, and `managedIdentityClientId` are no longer incorrectly marked as secret. diff --git a/.changeset/khaki-days-arrive.md b/.changeset/khaki-days-arrive.md new file mode 100644 index 0000000000..ac9d97c394 --- /dev/null +++ b/.changeset/khaki-days-arrive.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Fixed scroll overflow in Menu and Select popover content when constrained by viewport height. + +**Affected components:** Menu, Select diff --git a/.changeset/legal-ants-show.md b/.changeset/legal-ants-show.md new file mode 100644 index 0000000000..724f9eabee --- /dev/null +++ b/.changeset/legal-ants-show.md @@ -0,0 +1,10 @@ +--- +'@backstage/backend-dynamic-feature-service': minor +'@backstage/plugin-scaffolder-backend': minor +'@backstage/catalog-client': minor +'@backstage/plugin-catalog-backend': minor +'@backstage/plugin-events-backend': minor +'@backstage/plugin-search-backend': minor +--- + +Migrated OpenAPI schemas to 3.1. diff --git a/.changeset/loud-cups-tickle.md b/.changeset/loud-cups-tickle.md new file mode 100644 index 0000000000..122717f5e8 --- /dev/null +++ b/.changeset/loud-cups-tickle.md @@ -0,0 +1,16 @@ +--- +'@backstage/ui': minor +--- + +**BREAKING**: Data attributes rendered by components are now always lowercase. This affects CSS selectors targeting camelCase data attributes. + +**Migration:** + +Update any custom CSS selectors that target camelCase data attributes to use lowercase instead: + +```diff +- [data-startCollapsed='true'] { ... } ++ [data-startcollapsed='true'] { ... } +``` + +**Affected components:** SearchField diff --git a/.changeset/lucky-baboons-follow.md b/.changeset/lucky-baboons-follow.md new file mode 100644 index 0000000000..0a9453b764 --- /dev/null +++ b/.changeset/lucky-baboons-follow.md @@ -0,0 +1,15 @@ +--- +'@backstage/ui': minor +--- + +**BREAKING**: Removed deprecated types `ComponentDefinition`, `ClassNamesMap`, `DataAttributeValues`, and `DataAttributesMap` from the public API. These were internal styling infrastructure types that have been replaced by the `defineComponent` system. + +**Migration:** + +Remove any direct usage of these types. Component definitions now use `defineComponent()` and their shapes are not part of the public API contract. + +```diff +- import type { ComponentDefinition, ClassNamesMap } from '@backstage/ui'; +``` + +If you were reading `definition.dataAttributes`, use `definition.propDefs` instead — props with `dataAttribute: true` in `propDefs` are the equivalent. diff --git a/.changeset/mean-fans-decide.md b/.changeset/mean-fans-decide.md new file mode 100644 index 0000000000..5fe04beb40 --- /dev/null +++ b/.changeset/mean-fans-decide.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Fixed focus-visible outline styles for Menu and Select components. + +**Affected components:** Menu, Select diff --git a/.changeset/move-clean-pack-to-build.md b/.changeset/move-clean-pack-to-build.md new file mode 100644 index 0000000000..9f1969ec41 --- /dev/null +++ b/.changeset/move-clean-pack-to-build.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Internal refactor of CLI command modules. diff --git a/.changeset/move-utils-to-cli-node.md b/.changeset/move-utils-to-cli-node.md new file mode 100644 index 0000000000..f708dde666 --- /dev/null +++ b/.changeset/move-utils-to-cli-node.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-node': patch +--- + +Added `hasBackstageYarnPlugin` and `SuccessCache` exports, moved from `@backstage/cli`. diff --git a/.changeset/ninety-corners-flash.md b/.changeset/ninety-corners-flash.md new file mode 100644 index 0000000000..f858ec0593 --- /dev/null +++ b/.changeset/ninety-corners-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Migrates existing catalog metrics to use the alpha MetricsService. This release is a 1:1 migration with no breaking changes. diff --git a/.changeset/odd-ears-grab.md b/.changeset/odd-ears-grab.md new file mode 100644 index 0000000000..a683f3e6f5 --- /dev/null +++ b/.changeset/odd-ears-grab.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +The `Select` trigger now automatically adapts its background colour based on the parent background context. + +**Affected components:** Select diff --git a/.changeset/odd-laws-attack.md b/.changeset/odd-laws-attack.md new file mode 100644 index 0000000000..034cb10be3 --- /dev/null +++ b/.changeset/odd-laws-attack.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Merged the internal `PluginHeaderToolbar` component into `PluginHeader`, removing the separate component and its associated types (`PluginHeaderToolbarOwnProps`, `PluginHeaderToolbarProps`) and definition (`PluginHeaderToolbarDefinition`). This is an internal refactor with no changes to the public API of `PluginHeader`. + +**Affected components:** PluginHeader diff --git a/.changeset/pre.json b/.changeset/pre.json index 00dbb0f757..22e669095e 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -210,5 +210,107 @@ "@backstage/plugin-user-settings-backend": "0.4.0", "@backstage/plugin-user-settings-common": "0.1.0" }, - "changesets": [] + "changesets": [ + "add-masked-and-hidden-option", + "add-query-catalog-entities-action", + "add-who-am-i-action", + "blue-moons-crash", + "bright-items-see", + "bright-moons-open", + "brown-towns-find", + "bump-bfj-v9", + "bumpy-colts-teach", + "bumpy-keys-pay", + "by-refs-predicate-backend", + "by-refs-predicate-client", + "calm-vans-play", + "chatty-wasps-sink", + "clean-up-own-root-dir", + "cli-common-cached-paths", + "cli-execute-loader", + "cli-internal-refactor", + "cli-internalize-lib-modules", + "cli-node-lockfile-tostring", + "cli-node-parallel-helpers", + "cli-remove-migrate-package-exports", + "cli-report-parser-cleye", + "cli-split-loadCliConfig", + "cli-translations-export-import", + "cli-versioning-consolidation", + "cold-dodos-think", + "crazy-ravens-reply", + "cyan-falcons-serve", + "dark-snakes-nail", + "deep-files-clap", + "deep-moments-deny", + "dependabot-d2ec7e9", + "early-baboons-roll", + "entity-content-group-api-docs", + "entity-content-group-techdocs", + "entity-page-group-aliases-and-ordering-catalog", + "entity-page-group-aliases-and-ordering", + "facets-predicate-backend", + "facets-predicate-client", + "fancy-ends-turn", + "fine-poems-sit", + "fix-entity-tab-group-ordering", + "fix-form-fields-api-export-react", + "fix-frontend-feature-compat", + "fix-mcp-error-forwarding", + "fix-mui-styles-shared-dep", + "fix-prettier-existence-check", + "fix-tab-match-query-params", + "fix-test-api-pair-inference", + "fluffy-owls-act", + "free-pigs-itch", + "giant-singers-rush", + "github-org-verified-emails", + "heavy-baths-mix", + "itchy-words-crash", + "khaki-days-arrive", + "legal-ants-show", + "long-hairs-throw", + "loud-cups-tickle", + "lucky-baboons-follow", + "mean-fans-decide", + "metal-humans-move", + "migrate-to-target-paths", + "move-clean-pack-to-build", + "move-utils-to-cli-node", + "ninety-corners-flash", + "odd-laws-attack", + "orange-mugs-post-1", + "orange-mugs-post-2", + "pink-terms-know", + "polite-singers-lead", + "pretty-days-taste", + "rare-adults-attack", + "rare-falcons-pump", + "renovate-8b1c21e", + "rich-ducks-ring", + "rude-groups-shout", + "scaffolder-common-required-methods", + "scaffolder-react-api-mock", + "scaffolder-service-ref", + "sharp-ravens-shop", + "silly-shrimps-start", + "silver-pigs-remain", + "six-trees-carry", + "sixty-pianos-begin", + "slick-dogs-pay", + "slow-items-pull", + "stable-translation-plugin-app", + "stable-translation-test-utils", + "stupid-pans-hope", + "swift-flowers-grin", + "swift-ravens-jog", + "tangy-clouds-repeat", + "tiny-zoos-smash", + "tired-bushes-write", + "tired-clocks-repeat", + "twenty-worlds-create", + "two-lies-leave", + "violet-friends-buy", + "wide-pianos-pay" + ] } diff --git a/.changeset/rare-adults-attack.md b/.changeset/rare-adults-attack.md new file mode 100644 index 0000000000..8117f26dea --- /dev/null +++ b/.changeset/rare-adults-attack.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-plugin-api': patch +'@backstage/backend-defaults': patch +--- + +Adds an alpha `MetricsService` to provide a unified interface for metrics instrumentation across Backstage plugins. diff --git a/.changeset/rare-falcons-pump.md b/.changeset/rare-falcons-pump.md new file mode 100644 index 0000000000..78b0f48f8c --- /dev/null +++ b/.changeset/rare-falcons-pump.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Deprecated two processors as they have been moved to the Community Plugins repo with their own backend modules: + +- `AnnotateScmSlugEntityProcessor`: Use `@backstage-community/plugin-catalog-backend-module-annotate-scm-slug` instead +- `CodeOwnersProcessor`: Use `@backstage-community/plugin-catalog-backend-module-codeowners` instead diff --git a/.changeset/rich-ducks-ring.md b/.changeset/rich-ducks-ring.md new file mode 100644 index 0000000000..5f046e31b2 --- /dev/null +++ b/.changeset/rich-ducks-ring.md @@ -0,0 +1,10 @@ +--- +'@backstage/catalog-client': minor +'@backstage/plugin-catalog-backend': minor +--- + +Added predicate-based entity filtering via POST /entities/by-query endpoint. + +Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$hasPrefix`, and (partially) `$contains` operators for expressive entity queries. Integrated into the existing `queryEntities` flow with full cursor-based pagination, permission enforcement, and `totalItems` support. + +The catalog client's `queryEntities()` method automatically routes to the POST endpoint when a `query` predicate is provided. diff --git a/.changeset/scaffolder-common-required-methods.md b/.changeset/scaffolder-common-required-methods.md new file mode 100644 index 0000000000..10a5736259 --- /dev/null +++ b/.changeset/scaffolder-common-required-methods.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-common': minor +--- + +**BREAKING PRODUCERS**: Made `retry`, `listTasks`, `listTemplatingExtensions`, `dryRun`, and `autocomplete` required methods on the `ScaffolderApi` interface. Implementations of `ScaffolderApi` must now provide these methods. diff --git a/.changeset/scaffolder-export-form-fields-api.md b/.changeset/scaffolder-export-form-fields-api.md deleted file mode 100644 index 02ead2d2b2..0000000000 --- a/.changeset/scaffolder-export-form-fields-api.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Added back the `formFieldsApiRef` and `ScaffolderFormFieldsApi` alpha exports that were unintentionally removed. diff --git a/.changeset/scaffolder-react-api-mock.md b/.changeset/scaffolder-react-api-mock.md new file mode 100644 index 0000000000..58561e4e42 --- /dev/null +++ b/.changeset/scaffolder-react-api-mock.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Added `scaffolderApiMock` test utility, exported from `@backstage/plugin-scaffolder-react/testUtils`. diff --git a/.changeset/scaffolder-service-ref.md b/.changeset/scaffolder-service-ref.md new file mode 100644 index 0000000000..250b471f7e --- /dev/null +++ b/.changeset/scaffolder-service-ref.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-node': patch +--- + +Added `scaffolderServiceRef` and `ScaffolderService` interface for backend plugins that need to interact with the scaffolder API using `BackstageCredentials` instead of raw tokens. diff --git a/.changeset/sharp-ravens-shop.md b/.changeset/sharp-ravens-shop.md new file mode 100644 index 0000000000..55a8b0fe41 --- /dev/null +++ b/.changeset/sharp-ravens-shop.md @@ -0,0 +1,10 @@ +--- +'@backstage/integration': major +--- + +**BREAKING** Removed deprecated Azure DevOps, Bitbucket, Gerrit and GitHub code: + +- For Azure DevOps, the long deprecated `token` string and `credential` object have been removed from the `config.d.ts`. Use the `credentials` array object instead. +- For Bitbucket, the long deprecated `bitbucket` object has been removed from the `config.d.ts`. Use the `bitbucketCloud` or `bitbucketServer` objects instead. +- For Gerrit, the `parseGerritGitilesUrl` function has been removed, use `parseGitilesUrlRef` instead. The `buildGerritGitilesArchiveUrl` function has also been removed, use `buildGerritGitilesArchiveUrlFromLocation` instead. +- For GitHub, the `getGitHubRequestOptions` function has been removed. diff --git a/.changeset/silly-shrimps-start.md b/.changeset/silly-shrimps-start.md new file mode 100644 index 0000000000..a6baac94b7 --- /dev/null +++ b/.changeset/silly-shrimps-start.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-node': minor +--- + +**BREAKING** Removed deprecated `bitbucket` integration from being used in the `parseRepoUrl` function. It will use the `bitbucketCloud` or `bitbucketServer` integrations instead. diff --git a/.changeset/silver-pigs-remain.md b/.changeset/silver-pigs-remain.md new file mode 100644 index 0000000000..d6481f2990 --- /dev/null +++ b/.changeset/silver-pigs-remain.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Added proper cursor styles for RadioGroup items. + +**Affected components:** RadioGroup diff --git a/.changeset/six-trees-carry.md b/.changeset/six-trees-carry.md new file mode 100644 index 0000000000..9e21487c23 --- /dev/null +++ b/.changeset/six-trees-carry.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': minor +--- + +**BREAKING** Removed deprecated `BitbucketUrlReader`. Use the `BitbucketCloudUrlReader` or the `BitbucketServerUrlReader` instead. diff --git a/.changeset/slick-dogs-pay.md b/.changeset/slick-dogs-pay.md new file mode 100644 index 0000000000..409670601e --- /dev/null +++ b/.changeset/slick-dogs-pay.md @@ -0,0 +1,7 @@ +--- +'@backstage/create-app': patch +'@backstage/cli-node': patch +'@backstage/cli': patch +--- + +Properly support `package.json` `workspaces` field diff --git a/.changeset/slow-items-pull.md b/.changeset/slow-items-pull.md new file mode 100644 index 0000000000..e3fe6e26c5 --- /dev/null +++ b/.changeset/slow-items-pull.md @@ -0,0 +1,18 @@ +--- +'@backstage/create-app': patch +--- + +Replace deprecated `workspaces.packages` with `workspaces` in `package.json` + +This change is **not** required, but you can edit your main `package.json`, to fit the more modern & more common pattern: + +```diff +- "workspaces": { +- "packages": [ + "workspaces": [ + "packages/*", + "plugins/*" +- ] +- }, + ], +``` diff --git a/.changeset/social-worlds-report.md b/.changeset/social-worlds-report.md new file mode 100644 index 0000000000..58acda40a8 --- /dev/null +++ b/.changeset/social-worlds-report.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +Added `EntityInfoCard` component to `@backstage/plugin-catalog-react` as a BUI-based card wrapper for entity page cards. diff --git a/.changeset/spicy-grapes-share.md b/.changeset/spicy-grapes-share.md new file mode 100644 index 0000000000..c3d7702155 --- /dev/null +++ b/.changeset/spicy-grapes-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': minor +--- + +Registration errors should be forwarded as BackendStartupResult diff --git a/.changeset/swift-ravens-jog.md b/.changeset/swift-ravens-jog.md new file mode 100644 index 0000000000..db92764560 --- /dev/null +++ b/.changeset/swift-ravens-jog.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-mcp-actions-backend': patch +--- + +Adds two new metrics to track MCP server operations and sessions. + +- `mcp.server.operation.duration`: The duration taken to process an individual MCP operation +- `mcp.server.session.duration`: The duration of the MCP session from the perspective of the server diff --git a/.changeset/tangy-clouds-repeat.md b/.changeset/tangy-clouds-repeat.md new file mode 100644 index 0000000000..0999198c21 --- /dev/null +++ b/.changeset/tangy-clouds-repeat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Added a new `list-scaffolder-actions` action that returns all installed scaffolder actions with their schemas and examples diff --git a/.changeset/thirty-kiwis-trade.md b/.changeset/thirty-kiwis-trade.md new file mode 100644 index 0000000000..d88d804dac --- /dev/null +++ b/.changeset/thirty-kiwis-trade.md @@ -0,0 +1,22 @@ +--- +'@backstage/plugin-catalog': major +'@backstage/plugin-org': minor +--- + +Migrated `EntityAboutCard`, `EntityLinksCard`, `EntityLabelsCard`, `GroupProfileCard`, and `UserProfileCard` from MUI/InfoCard to use the new BUI card layout and BUI components where possible. + +**BREAKING**: Removed `variant` prop from EntityAboutCard, EntityUserProfileCard, EntityGroupProfileCard, EntityLabelsCard, EntityLinksCard. Removed `gridSizes` prop from `AboutField`. + +**Migration:** + +Simply delete the obsolete `variant` and `gridSizes` props, e.g: + +```diff +- ++ +``` + +```diff +- ++ +``` diff --git a/.changeset/tiny-zoos-smash.md b/.changeset/tiny-zoos-smash.md new file mode 100644 index 0000000000..883ea4f29a --- /dev/null +++ b/.changeset/tiny-zoos-smash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-common': major +--- + +**BREAKING** Removed deprecated `bitbucket` integration from being registered in the `ScaffolderClient`. Use the `bitbucketCloud` or `bitbucketServer` integrations instead. diff --git a/.changeset/tired-clocks-repeat.md b/.changeset/tired-clocks-repeat.md new file mode 100644 index 0000000000..ceea3be04f --- /dev/null +++ b/.changeset/tired-clocks-repeat.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Fixed `isRequired` prop not being passed to the underlying React Aria field components in TextField, SearchField, and PasswordField. Previously, `isRequired` was consumed locally for the secondary label text but never forwarded, which meant the input elements lacked `aria-required="true"` and React Aria's built-in required validation was not activated. + +**Affected components:** TextField, SearchField, PasswordField diff --git a/.changeset/twenty-worlds-create.md b/.changeset/twenty-worlds-create.md new file mode 100644 index 0000000000..e7e4770938 --- /dev/null +++ b/.changeset/twenty-worlds-create.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Adds a new metrics service mock to be leveraged in tests diff --git a/.changeset/two-lies-leave.md b/.changeset/two-lies-leave.md new file mode 100644 index 0000000000..2d83758940 --- /dev/null +++ b/.changeset/two-lies-leave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +create scaffolder MCP action to dry run a provided scaffolder template diff --git a/.changeset/ui-searchfield-textfield-bg-focus.md b/.changeset/ui-searchfield-textfield-bg-focus.md new file mode 100644 index 0000000000..9f9cc02768 --- /dev/null +++ b/.changeset/ui-searchfield-textfield-bg-focus.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +`SearchField` and `TextField` now automatically adapt their background color based on the parent bg context, stepping up one neutral level (e.g. neutral-1 → neutral-2) when placed on a neutral background. `TextField` also gains a focus ring using the `--bui-ring` token. + +**Affected components:** SearchField, TextField diff --git a/.changeset/violet-friends-buy.md b/.changeset/violet-friends-buy.md new file mode 100644 index 0000000000..a2a620b870 --- /dev/null +++ b/.changeset/violet-friends-buy.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Improved `useBreakpoint` performance by sharing a single set of `matchMedia` listeners across all component instances instead of creating independent listeners per hook call. diff --git a/.changeset/wide-pianos-pay.md b/.changeset/wide-pianos-pay.md new file mode 100644 index 0000000000..1cbcfd993c --- /dev/null +++ b/.changeset/wide-pianos-pay.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Extended `AlertProps`, `ContainerProps`, `DialogBodyProps`, and `FieldLabelProps` with native div element props to allow passing attributes like `aria-*` and `data-*`. + +**Affected components:** Alert, Container, DialogBody, FieldLabel diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md deleted file mode 100644 index 170a4eb868..0000000000 --- a/.claude/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -Follow the instructions at /.github/copilot-instructions.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 120000 index 0000000000..be77ac83a1 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/.cursor/rules/general.mdc b/.cursor/rules/general.mdc deleted file mode 100644 index 3a81a0cb5f..0000000000 --- a/.cursor/rules/general.mdc +++ /dev/null @@ -1,7 +0,0 @@ ---- -description: General project guidelines for Backstage development -globs: -alwaysApply: true ---- - -Follow the instructions at /.github/copilot-instructions.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 725c6c676b..7e5cc36bdf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -12,8 +12,10 @@ yarn.lock @backstage/maintainers @backst /.github @backstage/operations-maintainers /.github/vale @backstage/maintainers @backstage/documentation-maintainers /.patches @backstage/operations-maintainers +/.storybook @backstage/design-system-maintainers /beps/0001-notifications-system @backstage/maintainers @backstage/notifications-maintainers /docs @backstage/maintainers @backstage/documentation-maintainers +/docs-ui @backstage/design-system-maintainers /docs/assets/search @backstage/search-maintainers /docs/auth @backstage/auth-maintainers /docs/backend-system @backstage/framework-maintainers diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2157025b7b..9ded15067a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,49 +1,3 @@ -Backstage is an open platform for building developer portals. This is a TypeScript monorepo using Yarn workspaces. - -## Key Directories - -- `/packages`: Core framework packages (prefixed `@backstage/`) -- `/plugins`: Plugin packages (prefixed `@backstage/plugin-*`) -- `/packages/app` and `/packages/backend`: Example app for local development -- `/packages/app`: Main example app using the new frontend system -- `/packages/app-legacy`: Example app using the old frontend system -- `/docs`: Documentation files - -Packages prefixed with `core-` (e.g., `@backstage/core-plugin-api`) are part of the old frontend system. Packages prefixed with `frontend-` (e.g., `@backstage/frontend-plugin-api`) are part of the new frontend system (NFS). Packages prefixed with `backend-` (e.g., `@backstage/backend-plugin-api`) are part of the backend system. - -## Code Standards - -The following files contain guidelines for the project: - -- `/CONTRIBUTING.md`: comprehensive contribution guidelines. -- `/STYLE.md`: guidelines for code style. -- `/REVIEWING.md`: guidelines for pull requests and writing changesets. -- `/SECURITY.md`: guidelines for security. -- `/docs/architecture-decisions/`: contains the architecture decisions for the project. - -## Development Flow - -Before any of these commands can be run, you need to run `yarn install` in the project root. - -- Build: There is no need to build the project during development, and it is verified automatically in the CI pipeline. -- Test: Use `yarn test --no-watch ` in the project root to run tests. The path can be either a single file or a directory. Always provide a path, avoid running all tests. -- Type checking: Use `yarn tsc` in the project root to run the type checker. -- Code formatting: Use `yarn prettier --write ` to format code. -- Lint: Use `yarn lint --fix` in the project root to run the linter. -- API reports: Before submitting a pull request with changes to any package in the workspace, run `yarn build:api-reports` in the project root to generate API reports for all packages. -- Dev server: Use `yarn start` to run the example app locally (frontend on :3000, backend on :7007). -- Create: Use `yarn new` to scaffold new plugins, packages, or modules. - -You MUST NOT create a release by running `yarn changesets version` or `yarn release` as part of any changes. Releases are created by separate workflows. - -All changes that affect the published version of packages in the `/packages` and `/plugins` directories must be accompanied by a changeset. Only non-private packages require changesets. See the guidelines in `/CONTRIBUTING.md#creating-changesets` for information on how to write good changesets. Changesets are stored in the `/.changeset` directory. Breaking changes must be accompanied by a `minor` version bump for packages below version `1.0.0`, or a `major` version bump for packages at version `1.0.0` or higher. - -Never update ESLint, Prettier, or TypeScript configuration files unless specifically requested. - -## Repository Structure - -See `/docs/contribute/project-structure.md` for a detailed description of the repository structure. - ## Pull Request Reviews When reviewing a pull request you MUST calculate a priority score between 0 and 100, where a higher score indicates higher priority. You MUST include the score as an HTML comment at the very beginning of the "PR Overview" section, using the following format: ``. diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index f6f37b17cf..11df7fc1a0 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -30,6 +30,7 @@ backend's backported backporting BEPs +bg bigint Bigtable Billett @@ -425,6 +426,7 @@ scrollable scrollbar scrollbars sdks +SearchField seb semlas semver @@ -494,6 +496,7 @@ templater Templater templaters Templaters +TextField TFRecord theia Themer @@ -527,6 +530,7 @@ unassign unbreak Unconference undici +ungrouped unicode unmanaged unmount @@ -576,3 +580,4 @@ zsh resizable enums LLMs +cleye diff --git a/.github/workflows/api-breaking-changes.yml b/.github/workflows/api-breaking-changes.yml index a96556bb45..dc63ed3b1b 100644 --- a/.github/workflows/api-breaking-changes.yml +++ b/.github/workflows/api-breaking-changes.yml @@ -33,7 +33,7 @@ jobs: registry-url: https://registry.npmjs.org/ - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: linux-v22 diff --git a/.github/workflows/automate_changeset_feedback.yml b/.github/workflows/automate_changeset_feedback.yml index adfa7d29ec..fadb3efb84 100644 --- a/.github/workflows/automate_changeset_feedback.yml +++ b/.github/workflows/automate_changeset_feedback.yml @@ -34,7 +34,7 @@ jobs: ref: 'refs/pull/${{ github.event.pull_request.number }}/merge' - name: fetch base run: git fetch --depth 1 origin ${{ github.base_ref }} - - uses: backstage/actions/changeset-feedback@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + - uses: backstage/actions/changeset-feedback@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 name: Generate feedback with: diff-ref: 'origin/master' diff --git a/.github/workflows/ci-noop.yml b/.github/workflows/ci-noop.yml index d9851778c6..0fca26a12c 100644 --- a/.github/workflows/ci-noop.yml +++ b/.github/workflows/ci-noop.yml @@ -6,6 +6,7 @@ on: pull_request: paths: - 'microsite/**' + - '!microsite/data/**' - 'beps/**' permissions: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ac6120d28..a3bf6a2eb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,11 @@ name: CI on: # NOTE: If you change these you must update ci-noop.yml as well pull_request: - paths-ignore: - - 'microsite/**' - - 'beps/**' + paths: + - '**' + - '!microsite/**' + - 'microsite/data/**' + - '!beps/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -53,7 +55,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -89,7 +91,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -245,7 +247,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index 73c5202fb3..a9415b24e1 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -14,7 +14,7 @@ jobs: with: egress-policy: audit - - uses: backstage/actions/cron@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + - uses: backstage/actions/cron@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml index ad7029af14..a53004dbdd 100644 --- a/.github/workflows/deploy_docker-image.yml +++ b/.github/workflows/deploy_docker-image.yml @@ -37,7 +37,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/deploy_microsite.yml b/.github/workflows/deploy_microsite.yml index 1de546aaa5..ae9b3656d5 100644 --- a/.github/workflows/deploy_microsite.yml +++ b/.github/workflows/deploy_microsite.yml @@ -65,7 +65,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x @@ -138,7 +138,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index 21f6a92863..34dbcdb781 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -76,7 +76,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/mui-migration-tracker.yml b/.github/workflows/mui-migration-tracker.yml index cff389f20b..2cb5c025e2 100644 --- a/.github/workflows/mui-migration-tracker.yml +++ b/.github/workflows/mui-migration-tracker.yml @@ -32,7 +32,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x diff --git a/.github/workflows/sync_bui-docs.yml b/.github/workflows/sync_bui-docs.yml index 2a12f8f72d..274307415c 100644 --- a/.github/workflows/sync_bui-docs.yml +++ b/.github/workflows/sync_bui-docs.yml @@ -22,7 +22,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml index 9fc82de604..924dd82a40 100644 --- a/.github/workflows/sync_code-formatting.yml +++ b/.github/workflows/sync_code-formatting.yml @@ -25,7 +25,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/sync_pull-requests-scheduled.yml b/.github/workflows/sync_pull-requests-scheduled.yml index 08e6e3b0c0..210fc1b20a 100644 --- a/.github/workflows/sync_pull-requests-scheduled.yml +++ b/.github/workflows/sync_pull-requests-scheduled.yml @@ -75,7 +75,7 @@ jobs: egress-policy: audit - name: Backstage PR automation - uses: backstage/actions/pr-automation@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/pr-automation@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} diff --git a/.github/workflows/sync_pull-requests.yml b/.github/workflows/sync_pull-requests.yml index ca2524fe1c..4d607aab13 100644 --- a/.github/workflows/sync_pull-requests.yml +++ b/.github/workflows/sync_pull-requests.yml @@ -48,7 +48,7 @@ jobs: - name: Backstage PR automation if: steps.context.outputs.pr-number - uses: backstage/actions/pr-automation@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/pr-automation@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index dd145df153..91dffe32c9 100644 --- a/.github/workflows/sync_release-manifest.yml +++ b/.github/workflows/sync_release-manifest.yml @@ -25,7 +25,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index 75ad266a8b..97311c9096 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -24,7 +24,7 @@ jobs: node-version: 22.x registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x diff --git a/.github/workflows/verify_accessibility.yml b/.github/workflows/verify_accessibility.yml index 12456caf8e..0e8f3b7eb7 100644 --- a/.github/workflows/verify_accessibility.yml +++ b/.github/workflows/verify_accessibility.yml @@ -30,7 +30,7 @@ jobs: with: node-version: 22.x - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x - name: run Lighthouse CI diff --git a/.github/workflows/verify_chromatic.yml b/.github/workflows/verify_chromatic.yml index 74ff9595f4..e8905e995f 100644 --- a/.github/workflows/verify_chromatic.yml +++ b/.github/workflows/verify_chromatic.yml @@ -40,7 +40,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: Install dependencies - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_docs-quality.yml b/.github/workflows/verify_docs-quality.yml index af42f25390..6ace7d72a3 100644 --- a/.github/workflows/verify_docs-quality.yml +++ b/.github/workflows/verify_docs-quality.yml @@ -7,7 +7,10 @@ on: - '**.md' jobs: - check-all-files: + check-docs: + permissions: + contents: read + pull-requests: read runs-on: ubuntu-latest steps: @@ -18,18 +21,34 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - # 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 - # also contains an "--config=.github/vale/config.ini" option - - name: generate vale args - id: generate - run: echo "args=$(node scripts/check-docs-quality.js --ci-args)" >> $GITHUB_OUTPUT + - name: Install vale + run: | + VALE_VERSION="3.7.1" + VALE_CHECKSUM="ba4924bf2c5884499f09b02a6eb3318b29df40a3e81701c0804b9b1aefcd9483" + VALE_DIST_DIR="/tmp/vale-dist" + VALE_ARCHIVE="vale_${VALE_VERSION}_Linux_64-bit.tar.gz" - - name: documentation quality check - uses: errata-ai/vale-action@d89dee975228ae261d22c15adcd03578634d429c # v2.1.1 - with: - # This also contains --config=.github/vale/config.ini ... :/ - files: '${{ steps.generate.outputs.args }}' - version: latest + mkdir -p "$VALE_DIST_DIR" + + # Download pinned Vale binary + curl -fsSL -H "Authorization: token $GH_TOKEN" "https://github.com/errata-ai/vale/releases/download/v${VALE_VERSION}/${VALE_ARCHIVE}" -o "$VALE_DIST_DIR/$VALE_ARCHIVE" + + # Verify the integrity of the downloaded archive + echo "$VALE_CHECKSUM $VALE_DIST_DIR/$VALE_ARCHIVE" | sha256sum --check + + mkdir -p "$HOME/.local/bin" + tar -xzf "$VALE_DIST_DIR/$VALE_ARCHIVE" -C "$HOME/.local/bin" vale + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + vale --version env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} + + - name: Get changed files + run: | + gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \ + --paginate -q '.[] | select(.status != "removed") | .filename' > /tmp/pr-files.txt + env: + GH_TOKEN: ${{ github.token }} + + - name: Documentation quality check + run: node scripts/check-docs-quality.js --ci /tmp/pr-files.txt diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index c9a0696054..f959027d86 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -72,7 +72,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_e2e-techdocs.yml b/.github/workflows/verify_e2e-techdocs.yml index 0ea20acc46..0526df7d5c 100644 --- a/.github/workflows/verify_e2e-techdocs.yml +++ b/.github/workflows/verify_e2e-techdocs.yml @@ -46,7 +46,7 @@ jobs: with: node-version: ${{ matrix.node-version }} - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index dc9e7ebd9e..709c20a201 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -80,7 +80,7 @@ jobs: uses: browser-actions/setup-chrome@803ef6dfb4fdf22089c9563225d95e4a515820a0 # latest - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_microsite.yml b/.github/workflows/verify_microsite.yml index 71276ec85d..0d9f857740 100644 --- a/.github/workflows/verify_microsite.yml +++ b/.github/workflows/verify_microsite.yml @@ -70,7 +70,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x @@ -140,7 +140,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7 + uses: backstage/actions/yarn-install@2cd6978b476cbdc39fec48346f8b6ca13199dd6a # v0.7.8 with: cache-prefix: ${{ runner.os }}-v22.x diff --git a/.patches/pr-32996.txt b/.patches/pr-32996.txt new file mode 100644 index 0000000000..2dd0fde602 --- /dev/null +++ b/.patches/pr-32996.txt @@ -0,0 +1 @@ +Fixes the `@mui/material/styles` shared dependency key by removing a trailing slash that caused module resolution failures with MUI package exports. \ No newline at end of file diff --git a/.patches/pr-33004.txt b/.patches/pr-33004.txt new file mode 100644 index 0000000000..5594a76e6b --- /dev/null +++ b/.patches/pr-33004.txt @@ -0,0 +1 @@ +Fixes entity page tab groups not respecting the ordering from the groups configuration. \ No newline at end of file diff --git a/.patches/pr-33034.txt b/.patches/pr-33034.txt new file mode 100644 index 0000000000..4648da00b2 --- /dev/null +++ b/.patches/pr-33034.txt @@ -0,0 +1 @@ +Added back `formFieldsApiRef` and `ScaffolderFormFieldsApi` alpha exports to `@backstage/plugin-scaffolder-react`. This change was incorrectly applied to `@backstage/plugin-scaffolder` in the 1.48.2 release, which is now gone. Note that the API signature of this API has still changed since the 1.47 release. \ No newline at end of file diff --git a/.patches/pr-33057.txt b/.patches/pr-33057.txt new file mode 100644 index 0000000000..3499e84a89 --- /dev/null +++ b/.patches/pr-33057.txt @@ -0,0 +1 @@ +Fixed a type inference issue with the `apis` option of testing utilities in `@backstage/frontend-test-utils`. \ No newline at end of file diff --git a/.storybook/modes.ts b/.storybook/modes.ts index 7087fa35eb..ae4ff76a7c 100644 --- a/.storybook/modes.ts +++ b/.storybook/modes.ts @@ -15,4 +15,19 @@ export const allModes = { themeMode: 'dark', themeName: 'spotify', }, + 'light spotify neutral-1': { + themeMode: 'light', + themeName: 'spotify', + background: 'neutral-1', + }, + 'light spotify neutral-2': { + themeMode: 'light', + themeName: 'spotify', + background: 'neutral-2', + }, + 'light spotify neutral-3': { + themeMode: 'light', + themeName: 'spotify', + background: 'neutral-3', + }, } as const; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 6a0177cafe..9734cee295 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -3,7 +3,7 @@ import addonDocs from '@storybook/addon-docs'; import addonThemes from '@storybook/addon-themes'; import addonLinks from '@storybook/addon-links'; import { definePreview } from '@storybook/react-vite'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { TestApiProvider } from '@backstage/test-utils'; import { AlertDisplay } from '@backstage/core-components'; import { apis } from './support/apis'; @@ -19,6 +19,7 @@ import './storybook.css'; // Custom themes import './themes/spotify.css'; +import { Box } from '../packages/ui/src/components/Box'; export default definePreview({ tags: ['manifest'], @@ -30,8 +31,8 @@ export default definePreview({ toolbar: { icon: 'circlehollow', items: [ - { value: 'light', icon: 'circlehollow', title: 'Light' }, - { value: 'dark', icon: 'circle', title: 'Dark' }, + { value: 'light', icon: 'sun', title: 'Light' }, + { value: 'dark', icon: 'moon', title: 'Dark' }, ], dynamicTitle: true, }, @@ -49,11 +50,26 @@ export default definePreview({ dynamicTitle: true, }, }, + background: { + name: 'Background', + description: 'Global background for components', + defaultValue: 'app', + toolbar: { + icon: 'contrast', + items: [ + { value: 'app', title: 'App Background' }, + { value: 'neutral-1', title: 'Neutral 1 Background' }, + { value: 'neutral-2', title: 'Neutral 2 Background' }, + { value: 'neutral-3', title: 'Neutral 3 Background' }, + ], + }, + }, }, initialGlobals: { themeMode: 'light', themeName: 'backstage', + background: 'app', }, parameters: { @@ -70,7 +86,13 @@ export default definePreview({ options: { storySort: { - order: ['Backstage UI', 'Plugins', 'Layout', 'Navigation'], + order: [ + 'Backstage UI', + 'Guidelines', + 'Plugins', + 'Layout', + 'Navigation', + ], }, }, @@ -114,12 +136,14 @@ export default definePreview({ }, decorators: [ - Story => { + (Story, context) => { const [globals] = useGlobals(); const selectedTheme = globals.themeMode === 'light' ? themes.light : themes.dark; const selectedThemeMode = globals.themeMode || 'light'; const selectedThemeName = globals.themeName || 'backstage'; + const selectedBackground = globals.background || 'app'; + const isFullscreen = context.parameters.layout === 'fullscreen'; useEffect(() => { document.body.removeAttribute('data-theme-mode'); @@ -133,6 +157,8 @@ export default definePreview({ }, [selectedTheme, selectedThemeName]); document.body.style.backgroundColor = 'var(--bui-bg-app)'; + document.body.style.padding = + isFullscreen && selectedBackground !== 'app' ? '1rem' : ''; const docsStoryElements = document.getElementsByClassName('docs-story'); Array.from(docsStoryElements).forEach(element => { (element as HTMLElement).style.backgroundColor = 'var(--bui-bg-app)'; @@ -143,7 +169,19 @@ export default definePreview({ {/* @ts-ignore */} - + {Array.from({ + length: + selectedBackground === 'app' + ? 0 + : parseInt(selectedBackground.split('-')[1], 10), + }).reduce( + children => ( + + {children} + + ), + , + )} ); diff --git a/.storybook/themes/spotify.css b/.storybook/themes/spotify.css index b72683f53e..3cad0fba10 100644 --- a/.storybook/themes/spotify.css +++ b/.storybook/themes/spotify.css @@ -183,27 +183,19 @@ font-weight: var(--bui-font-weight-regular); } - .bui-HeaderToolbar { - padding-top: var(--bui-space-2); - padding-inline: var(--bui-space-2); - } - - .bui-HeaderToolbarWrapper { - border-radius: var(--bui-radius-3); - padding-inline: var(--bui-space-3); + .bui-PluginHeaderToolbarWrapper { + padding: 0; + height: 32px; border: none; + background: none; } - .bui-HeaderToolbarControls { - right: calc(var(--bui-space-3) - 1px); - } - - .bui-HeaderTabsWrapper { - margin-top: var(--bui-space-2); - margin-inline: var(--bui-space-2); - border-radius: var(--bui-radius-3); - padding-inline: var(--bui-space-1); + .bui-PluginHeaderTabsWrapper { + margin: 0; + padding: 0; + margin-left: -8px; border: none; + background: none; } .bui-Input { @@ -213,6 +205,10 @@ .bui-Tag { border-radius: var(--bui-radius-full); } + + .bui-Container { + padding-inline: 0; + } } [data-theme-mode='light'][data-theme-name='spotify'] { diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..001989d9b6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,53 @@ +Backstage is an open platform for building developer portals. This is a TypeScript monorepo using Yarn workspaces. + +## Key Directories + +- `/packages`: Core framework packages (prefixed `@backstage/`) +- `/plugins`: Plugin packages (prefixed `@backstage/plugin-*`) +- `/packages/app` and `/packages/backend`: Example app for local development +- `/packages/app`: Main example app using the new frontend system +- `/packages/app-legacy`: Example app using the old frontend system +- `/docs`: Documentation files + +Packages prefixed with `core-` (e.g., `@backstage/core-plugin-api`) are part of the old frontend system. Packages prefixed with `frontend-` (e.g., `@backstage/frontend-plugin-api`) are part of the new frontend system (NFS). Packages prefixed with `backend-` (e.g., `@backstage/backend-plugin-api`) are part of the backend system. + +## Code Standards + +The following files contain guidelines for the project: + +- `/CONTRIBUTING.md`: comprehensive contribution guidelines. +- `/STYLE.md`: guidelines for code style. +- `/REVIEWING.md`: guidelines for pull requests and writing changesets. +- `/SECURITY.md`: guidelines for security. +- `/docs/architecture-decisions/`: contains the architecture decisions for the project. + +When writing or generating code, always match the existing coding style of each individual package and file. Different packages in the monorepo may have different conventions — consistency within a package is more important than consistency across the repo. + +When writing or generating tests, prefer fewer thorough tests with multiple assertions over many small tests. When using React Testing Library, prefer using `screen` and `.findBy*` queries over `waitFor`, and avoid adding test IDs to the implementation. + +## Development Flow + +Before any of these commands can be run, you need to run `yarn install` in the project root. + +- Build: There is no need to build the project during development, and it is verified automatically in the CI pipeline. +- Test: Use `CI=1 yarn test ` in the project root to run tests. The path can be either a single file or a directory. Always provide a path, avoid running all tests. +- Type checking: Use `yarn tsc` in the project root to run the type checker. Do not try to run it somewhere else than the project root and do not supply any options. +- Code formatting: Use `yarn prettier --write <...paths>` to format code. Run it explicitly for file paths that you know are changed, not for entire folders - otherwise it may change formatting of unrelated files. +- Lint: Use `yarn lint --fix` in the project root to run the linter. +- API reports: Before submitting a pull request with changes to any package in the workspace, run `yarn build:api-reports` in the project root to generate API reports for all packages. +- Dev server: Use `yarn start` to run the example app locally (frontend on :3000, backend on :7007). +- Create: Use `yarn new` to scaffold new plugins, packages, or modules. + +You MUST NOT run builds or create a release by running `yarn build`, `yarn changesets version`, or `yarn release` as part of any changes. Builds and releases are made by separate workflows. + +All changes that affect the published version of packages in the `/packages` and `/plugins` directories must be accompanied by a changeset. Only non-private packages require changesets. See the guidelines in `/CONTRIBUTING.md#creating-changesets` for information on how to write good changesets. Changesets are stored in the `/.changeset` directory and should be created by writing changeset files directly — never use the changeset CLI. Breaking changes must be accompanied by a `minor` version bump for packages below version `1.0.0`, or a `major` version bump for packages at version `1.0.0` or higher. For non-breaking changes that introduce new APIs or features, use `minor` for packages at version `1.0.0` or higher, and `patch` for packages below `1.0.0`. Each changeset message should be relevant to the specific package it targets and written for Backstage adopters as the audience — avoid referencing internal implementation details. If a change spans multiple packages you often need to create separate changesets to make sure they are tailored to each package. + +When creating pull requests, use the template at `/.github/PULL_REQUEST_TEMPLATE.md`. + +Never update ESLint, Prettier, or TypeScript configuration files unless specifically requested. + +Never make changes to the release notes in `/docs/releases` unless explicitly asked. These document past releases and should not be updated based on newer changes. + +## Repository Structure + +See `/docs/contribute/project-structure.md` for a detailed description of the repository structure. diff --git a/OWNERS.md b/OWNERS.md index e09a0a24c3..4d535fc4b2 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -283,6 +283,7 @@ Scope: The Scaffolder frontend and backend plugins, and related tooling. | Carlos Esteban Lopez Jaramillo | VMWare | [luchillo17](https://github.com/luchillo17) | `luchillo17#8777` | | David Tuite | Roadie.io | [dtuite](https://github.com/dtuite) | `David Tuite (roadie.io)#1010` | | Deepankumar Loganathan | | [deepan10](https://github.com/deepan10) | `deepan10` | +| Elaine Mattos | DB Systel | [elaine-mattos](https://github.com/elaine-mattos) | `elaine_mattos` | | Gabriel Dugny | Believe | [GabDug](https://github.com/GabDug) | `GabDug` | | Heikki Hellgren | OP Financial Group | [drodil](https://github.com/drodil) | `deathammer` | | Himanshu Mishra | Harness.io | [OrkoHunter](https://github.com/OrkoHunter) | `OrkoHunter#1520` | diff --git a/app-config.yaml b/app-config.yaml index 08cd0b693c..e88955a39f 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -68,6 +68,7 @@ backend: actions: pluginSources: - catalog + - scaffolder # See README.md in the proxy-backend plugin for information on the configuration format proxy: endpoints: diff --git a/beps/0012-metrics-service/README.md b/beps/0012-metrics-service/README.md index 59096d11d8..87b2059f02 100644 --- a/beps/0012-metrics-service/README.md +++ b/beps/0012-metrics-service/README.md @@ -23,7 +23,6 @@ creation-date: 2025-06-23 - [Integration with OpenTelemetry Auto-Instrumentation](#integration-with-opentelemetry-auto-instrumentation) - [Configuration](#configuration) - [Interface](#interface) - - [Root Metrics Service](#root-metrics-service) - [Plugin Metrics Service](#plugin-metrics-service) - [Example](#example) - [Release Plan](#release-plan) @@ -36,7 +35,7 @@ Add a core `MetricsService` to Backstage's framework to provide a unified interf ## Motivation -While individual plugins may implement their own metrics, there's no standardized approach leading to inconsistent metrics patterns across the ecosystem. For example, both `catalog_entities_count` and `catalog.processed.entities.count` are examples of existing metric patterns. Ideally, these would be standardized to `backstage.plugin.catalog.entities.count` and `backstage.plugin.catalog.entities.processed.total` respectively. +While individual plugins may implement their own metrics, there's no standardized approach leading to inconsistent metrics patterns across the ecosystem and incompatibility with OpenTelemetry semantic conventions. For example, a plugin implementing MCP functionality might incorrectly namespace metrics as `backstage_mcp_client_duration` when OpenTelemetry semantic conventions explicitly define `mcp.client.operation.duration` as the standard. By providing a core metrics service: @@ -45,7 +44,7 @@ By providing a core metrics service: ### Goals -- Plugin-scoped metric namespacing +- Plugin identification via OpenTelemetry Instrumentation Scope - Consistent metrics patterns across all plugins - Aligned with OpenTelemetry industry standards - Provide a familiar interface as other core services @@ -124,9 +123,12 @@ The `MetricsService` **complements** rather than duplicates auto-instrumentation // MetricsService provides (manually): const entityMetrics = metricsService.createCounter('entities.processed.total'); -entityMetrics.add(entities.length, { operation: 'refresh', kind: 'Component' }); +entityMetrics.add(entities.length, { + operation: 'refresh', + 'entity.kind': 'Component', +}); -// Metric is now available as `backstage.plugin.catalog.entities.processed.total` +// Metric is now available as `entities.processed.total` ``` ### Configuration @@ -162,43 +164,21 @@ interface MetricsService { } ``` -#### Root Metrics Service - -The `RootMetricsService` is responsible for providing metrics to other root services and creating both plugin-scoped and core-scoped `MetricsService` instances. - -```ts -interface RootMetricsService { - // note: no config is provided to the root service. - static forRoot(): RootMetricsService; - forPlugin(pluginId: string): MetricsService; - - // final implementation will be similar to - forService(serviceName: string, scope: 'plugin' | 'core'): MetricsService; -} - -export const rootMetricsServiceFactory = createServiceFactory({ - // depends on as little as possible so that it can be initialized as early as possible. - service: rootMetricsServiceRef, - deps: {}, - factory: () => { - return DefaultRootMetricsService.forRoot(); - }, -}); -``` - #### Plugin Metrics Service -Each plugin receives a metrics service that automatically namespaces all metrics to match the naming conventions. +Each plugin receives a metrics service that automatically configures the Instrumentation Scope to identify the plugin. The scope name follows the pattern `backstage-plugin-{pluginId}`. ```ts -const metricsServiceFactory = createServiceFactory({ - service: metricsServiceRef, +export const metricsServiceFactory = createServiceFactory({ + service: coreServices.metrics, deps: { - rootMetrics: coreServices.rootMetrics, pluginMetadata: coreServices.pluginMetadata, }, - factory: ({ rootMetrics, pluginMetadata }) => { - return rootMetrics.forPlugin(pluginMetadata.getId()); + factory: ({ pluginMetadata }) => { + const pluginId = pluginMetadata.getId(); + const scopeName = `backstage-plugin-${pluginId}`; + + return new DefaultMetricsService(scopeName, version, ...); }, }); ``` @@ -248,3 +228,22 @@ entitiesProcessed.add(100); - Plugin authors continue to implement their own metrics as they see fit. - A combined TelemetryService that provides both metrics and tracing. + +### Rejected: Forced Namespace Prefixes + +Prepend `backstage.plugin.{pluginId}.` to all metric names. This was the original proposal but conflicts with OpenTelemetry semantic conventions. + +**Problems:** + +- Makes it impossible to use standard semantic conventions like `mcp.*`, `gen_ai.*`, `http.*` +- Breaks compatibility with industry-standard observability tooling +- Prevents cross-service metric aggregation +- Goes against OpenTelemetry best practices and official guidance + +**Example of conflict:** + +```ts +// Plugin wants to emit: mcp.client.operation.duration +// Framework forces: backstage.plugin.mcp-actions.mcp.client.operation.duration +// This violates the semantic convention and breaks tooling +``` diff --git a/docs-ui/src/app/components/card/components.tsx b/docs-ui/src/app/components/card/components.tsx index d10e4d7bce..c4d1f809c2 100644 --- a/docs-ui/src/app/components/card/components.tsx +++ b/docs-ui/src/app/components/card/components.tsx @@ -7,6 +7,8 @@ import { CardFooter, } from '../../../../../packages/ui/src/components/Card/Card'; import { Text } from '../../../../../packages/ui/src/components/Text/Text'; +import { Button } from '../../../../../packages/ui/src/components/Button/Button'; +import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex'; export const Default = () => { return ( @@ -32,6 +34,70 @@ export const HeaderAndBody = () => { ); }; +export const InteractiveButton = () => { + return ( + {}} + label="View component details" + > + Interactive Card + + Click anywhere on this card to trigger the press handler. + + + + Click to interact + + + + ); +}; + +export const InteractiveLink = () => { + return ( + + Link Card + This card navigates to a URL when clicked. + + + Opens backstage.io + + + + ); +}; + +export const InteractiveWithNestedButtons = () => { + return ( + {}} + label="View plugin details" + > + Card with Actions + + Clicking the card background triggers the card press handler. The + buttons below remain independently interactive. + + + + + + + + + ); +}; + export const WithLongBody = () => { return ( diff --git a/docs-ui/src/app/components/card/page.mdx b/docs-ui/src/app/components/card/page.mdx index 6f410fcd97..8df6eccb40 100644 --- a/docs-ui/src/app/components/card/page.mdx +++ b/docs-ui/src/app/components/card/page.mdx @@ -12,8 +12,18 @@ import { defaultSnippet, headerAndBodySnippet, withLongBodySnippet, + interactiveButtonSnippet, + interactiveLinkSnippet, + interactiveWithNestedButtonsSnippet, } from './snippets'; -import { Default, HeaderAndBody, WithLongBody } from './components'; +import { + Default, + HeaderAndBody, + WithLongBody, + InteractiveButton, + InteractiveLink, + InteractiveWithNestedButtons, +} from './components'; import { PageTitle } from '@/components/PageTitle'; import { Theming } from '@/components/Theming'; import { CardDefinition } from '../../../utils/definitions'; @@ -81,6 +91,49 @@ When body content exceeds the available height, CardBody scrolls while header an code={withLongBodySnippet} /> +## Interactive cards + +Cards can be made interactive without wrapping the entire card in a button or link — which would conflict with any interactive elements inside. Instead, a transparent overlay covers the card surface, and nested buttons and links remain independently clickable above it. + +### Button + +Pass `onPress` and a `label` (used as the accessible name for screen readers) to make the whole card surface pressable. + +} + code={interactiveButtonSnippet} +/> + +### Link + +Pass `href` to make the card surface navigate to a URL. `label` is required — it provides the accessible name for the invisible overlay link read by screen readers. + +} + code={interactiveLinkSnippet} +/> + +### With nested buttons + +Buttons and links inside the card remain independently interactive. Clicking them does not trigger the card's `onPress` handler. + +} + code={interactiveWithNestedButtonsSnippet} +/> + diff --git a/docs-ui/src/app/components/card/props-definition.ts b/docs-ui/src/app/components/card/props-definition.ts index b3e3776648..7e68d890b6 100644 --- a/docs-ui/src/app/components/card/props-definition.ts +++ b/docs-ui/src/app/components/card/props-definition.ts @@ -15,6 +15,25 @@ const optionalChildrenPropDef: Record = { export const cardPropDefs: Record = { ...optionalChildrenPropDef, + onPress: { + type: 'enum', + values: ['() => void'], + responsive: false, + description: + 'Handler called when the card is pressed. Makes the card interactive as a button. Requires label.', + }, + href: { + type: 'string', + responsive: false, + description: + 'URL to navigate to. Makes the card interactive as a link. Mutually exclusive with onPress.', + }, + label: { + type: 'string', + responsive: false, + description: + 'Accessible label announced by screen readers for the interactive overlay. Required when onPress or href is provided.', + }, ...classNamePropDefs, ...stylePropDefs, }; diff --git a/docs-ui/src/app/components/card/snippets.ts b/docs-ui/src/app/components/card/snippets.ts index 6f956e5518..86b9df5e0a 100644 --- a/docs-ui/src/app/components/card/snippets.ts +++ b/docs-ui/src/app/components/card/snippets.ts @@ -17,6 +17,50 @@ export const headerAndBodySnippet = `Body content without a footer `; +export const interactiveButtonSnippet = ` console.log('Card pressed')} + label="View component details" +> + Interactive Card + Click anywhere on this card to trigger the press handler. + Click to interact +`; + +export const interactiveLinkSnippet = ` + Link Card + This card navigates to a URL when clicked. + Opens backstage.io +`; + +export const interactiveWithNestedButtonsSnippet = `import { Button, Flex } from '@backstage/ui'; + + console.log('Card pressed')} + label="View plugin details" +> + Card with Actions + + Clicking the card background triggers the card press handler. + The buttons below remain independently interactive. + + + + + + + +`; + export const withLongBodySnippet = `import { Text } from '@backstage/ui'; diff --git a/docs-ui/src/app/components/link/props-definition.tsx b/docs-ui/src/app/components/link/props-definition.tsx index f0aa95114e..be805d4540 100644 --- a/docs-ui/src/app/components/link/props-definition.tsx +++ b/docs-ui/src/app/components/link/props-definition.tsx @@ -38,7 +38,7 @@ export const linkPropDefs: Record = { 'body-small', 'body-x-small', ], - default: 'body', + default: 'body-medium', responsive: true, description: 'Typography style. Title variants for headings, body for paragraph text.', @@ -56,7 +56,7 @@ export const linkPropDefs: Record = { }, color: { type: 'enum', - values: ['primary', 'secondary', 'danger', 'warning', 'success'], + values: ['primary', 'secondary', 'danger', 'warning', 'success', 'info'], default: 'primary', responsive: true, description: diff --git a/docs-ui/src/app/snippets.ts b/docs-ui/src/app/snippets.ts index e16463a085..4a93c31831 100644 --- a/docs-ui/src/app/snippets.ts +++ b/docs-ui/src/app/snippets.ts @@ -1,19 +1,19 @@ export const surfacesSnippet = ` - + - + `; -export const adaptiveSnippet = ` +export const adaptiveSnippet = ` {/* automatically set background to neutral-2 */} `; -export const customCardSnippet = ` +export const customCardSnippet = ` Hello World `; diff --git a/docs/assets/uiguide/backstage-website-registered-catalog-view.png b/docs/assets/uiguide/backstage-website-registered-catalog-view.png new file mode 100644 index 0000000000..9263eff0c7 Binary files /dev/null and b/docs/assets/uiguide/backstage-website-registered-catalog-view.png differ diff --git a/docs/assets/uiguide/catalog-filter-options.png b/docs/assets/uiguide/catalog-filter-options.png new file mode 100644 index 0000000000..f2a85ab192 Binary files /dev/null and b/docs/assets/uiguide/catalog-filter-options.png differ diff --git a/docs/assets/uiguide/catalog-graph.png b/docs/assets/uiguide/catalog-graph.png new file mode 100644 index 0000000000..59e290b0f0 Binary files /dev/null and b/docs/assets/uiguide/catalog-graph.png differ diff --git a/docs/assets/uiguide/component-name-updated.png b/docs/assets/uiguide/component-name-updated.png new file mode 100644 index 0000000000..602e2e551a Binary files /dev/null and b/docs/assets/uiguide/component-name-updated.png differ diff --git a/docs/assets/uiguide/confirm-unregister-entity.png b/docs/assets/uiguide/confirm-unregister-entity.png new file mode 100644 index 0000000000..89a565ff35 Binary files /dev/null and b/docs/assets/uiguide/confirm-unregister-entity.png differ diff --git a/docs/assets/uiguide/curve-monotone.png b/docs/assets/uiguide/curve-monotone.png new file mode 100644 index 0000000000..52275ec780 Binary files /dev/null and b/docs/assets/uiguide/curve-monotone.png differ diff --git a/docs/assets/uiguide/curve-step-before.png b/docs/assets/uiguide/curve-step-before.png new file mode 100644 index 0000000000..a9aa4c8e6f Binary files /dev/null and b/docs/assets/uiguide/curve-step-before.png differ diff --git a/docs/assets/uiguide/details-for-registered-backstage-website.png b/docs/assets/uiguide/details-for-registered-backstage-website.png new file mode 100644 index 0000000000..00d55026f2 Binary files /dev/null and b/docs/assets/uiguide/details-for-registered-backstage-website.png differ diff --git a/docs/assets/uiguide/details-system-entity.png b/docs/assets/uiguide/details-system-entity.png new file mode 100644 index 0000000000..c667421603 Binary files /dev/null and b/docs/assets/uiguide/details-system-entity.png differ diff --git a/docs/assets/uiguide/enter-name-of-new-component.png b/docs/assets/uiguide/enter-name-of-new-component.png new file mode 100644 index 0000000000..fc6a11a360 Binary files /dev/null and b/docs/assets/uiguide/enter-name-of-new-component.png differ diff --git a/docs/assets/uiguide/enter-url-of-component.png b/docs/assets/uiguide/enter-url-of-component.png new file mode 100644 index 0000000000..29fecb7bfd Binary files /dev/null and b/docs/assets/uiguide/enter-url-of-component.png differ diff --git a/docs/assets/uiguide/entities-owned-by-me.png b/docs/assets/uiguide/entities-owned-by-me.png new file mode 100644 index 0000000000..e0dda5e680 Binary files /dev/null and b/docs/assets/uiguide/entities-owned-by-me.png differ diff --git a/docs/assets/uiguide/entity-actions.png b/docs/assets/uiguide/entity-actions.png new file mode 100644 index 0000000000..3c725758e4 Binary files /dev/null and b/docs/assets/uiguide/entity-actions.png differ diff --git a/docs/assets/uiguide/error-creating-new-component.png b/docs/assets/uiguide/error-creating-new-component.png new file mode 100644 index 0000000000..8c7da95159 Binary files /dev/null and b/docs/assets/uiguide/error-creating-new-component.png differ diff --git a/docs/assets/uiguide/error-message-catalog-info-file-deleted.png b/docs/assets/uiguide/error-message-catalog-info-file-deleted.png new file mode 100644 index 0000000000..7526ba65aa Binary files /dev/null and b/docs/assets/uiguide/error-message-catalog-info-file-deleted.png differ diff --git a/docs/assets/uiguide/example-website-relationships.png b/docs/assets/uiguide/example-website-relationships.png new file mode 100644 index 0000000000..b4fa6e33c7 Binary files /dev/null and b/docs/assets/uiguide/example-website-relationships.png differ diff --git a/docs/assets/uiguide/filter-by-name.png b/docs/assets/uiguide/filter-by-name.png new file mode 100644 index 0000000000..ca0336eadc Binary files /dev/null and b/docs/assets/uiguide/filter-by-name.png differ diff --git a/docs/assets/uiguide/max-depth-1.png b/docs/assets/uiguide/max-depth-1.png new file mode 100644 index 0000000000..d353b8ae23 Binary files /dev/null and b/docs/assets/uiguide/max-depth-1.png differ diff --git a/docs/assets/uiguide/max-depth-infinite.png b/docs/assets/uiguide/max-depth-infinite.png new file mode 100644 index 0000000000..0f72cccb84 Binary files /dev/null and b/docs/assets/uiguide/max-depth-infinite.png differ diff --git a/docs/assets/uiguide/new-tutorial-component-in-software-catalog.png b/docs/assets/uiguide/new-tutorial-component-in-software-catalog.png new file mode 100644 index 0000000000..51ee73c524 Binary files /dev/null and b/docs/assets/uiguide/new-tutorial-component-in-software-catalog.png differ diff --git a/docs/assets/uiguide/portal-with-annotations.png b/docs/assets/uiguide/portal-with-annotations.png new file mode 100644 index 0000000000..a8c9bc535b Binary files /dev/null and b/docs/assets/uiguide/portal-with-annotations.png differ diff --git a/docs/assets/uiguide/review-select-import.png b/docs/assets/uiguide/review-select-import.png new file mode 100644 index 0000000000..bd4a0acd7c Binary files /dev/null and b/docs/assets/uiguide/review-select-import.png differ diff --git a/docs/assets/uiguide/select-advanced-options.png b/docs/assets/uiguide/select-advanced-options.png new file mode 100644 index 0000000000..27fe7c7b16 Binary files /dev/null and b/docs/assets/uiguide/select-advanced-options.png differ diff --git a/docs/assets/uiguide/select-create-for-new-component.png b/docs/assets/uiguide/select-create-for-new-component.png new file mode 100644 index 0000000000..9fbc612ac0 Binary files /dev/null and b/docs/assets/uiguide/select-create-for-new-component.png differ diff --git a/docs/assets/uiguide/select-create-to-make-component.png b/docs/assets/uiguide/select-create-to-make-component.png new file mode 100644 index 0000000000..e86e9398ca Binary files /dev/null and b/docs/assets/uiguide/select-create-to-make-component.png differ diff --git a/docs/assets/uiguide/select-delete-entity.png b/docs/assets/uiguide/select-delete-entity.png new file mode 100644 index 0000000000..6874c5e83d Binary files /dev/null and b/docs/assets/uiguide/select-delete-entity.png differ diff --git a/docs/assets/uiguide/select-edit-icon-for-component.png b/docs/assets/uiguide/select-edit-icon-for-component.png new file mode 100644 index 0000000000..04d693259d Binary files /dev/null and b/docs/assets/uiguide/select-edit-icon-for-component.png differ diff --git a/docs/assets/uiguide/select-example-website.png b/docs/assets/uiguide/select-example-website.png new file mode 100644 index 0000000000..f8f72699eb Binary files /dev/null and b/docs/assets/uiguide/select-example-website.png differ diff --git a/docs/assets/uiguide/select-register-existing-component.png b/docs/assets/uiguide/select-register-existing-component.png new file mode 100644 index 0000000000..8d3f5c7af4 Binary files /dev/null and b/docs/assets/uiguide/select-register-existing-component.png differ diff --git a/docs/assets/uiguide/select-review-create-component.png b/docs/assets/uiguide/select-review-create-component.png new file mode 100644 index 0000000000..5acce27ec6 Binary files /dev/null and b/docs/assets/uiguide/select-review-create-component.png differ diff --git a/docs/assets/uiguide/select-unregister-entity-from-three-dots.png b/docs/assets/uiguide/select-unregister-entity-from-three-dots.png new file mode 100644 index 0000000000..959ec3f544 Binary files /dev/null and b/docs/assets/uiguide/select-unregister-entity-from-three-dots.png differ diff --git a/docs/assets/uiguide/select-view-graph.png b/docs/assets/uiguide/select-view-graph.png new file mode 100644 index 0000000000..79886a82b4 Binary files /dev/null and b/docs/assets/uiguide/select-view-graph.png differ diff --git a/docs/assets/uiguide/setup-nodejs-cat-owner.png b/docs/assets/uiguide/setup-nodejs-cat-owner.png new file mode 100644 index 0000000000..5e8effec72 Binary files /dev/null and b/docs/assets/uiguide/setup-nodejs-cat-owner.png differ diff --git a/docs/assets/uiguide/simplify-off-merge-relations-off.png b/docs/assets/uiguide/simplify-off-merge-relations-off.png new file mode 100644 index 0000000000..d999a7a5c7 Binary files /dev/null and b/docs/assets/uiguide/simplify-off-merge-relations-off.png differ diff --git a/docs/assets/uiguide/simplify-off-merge-relations-on.png b/docs/assets/uiguide/simplify-off-merge-relations-on.png new file mode 100644 index 0000000000..1003f2dd3d Binary files /dev/null and b/docs/assets/uiguide/simplify-off-merge-relations-on.png differ diff --git a/docs/assets/uiguide/simplify-on-merge-relations-off.png b/docs/assets/uiguide/simplify-on-merge-relations-off.png new file mode 100644 index 0000000000..60cbe0107e Binary files /dev/null and b/docs/assets/uiguide/simplify-on-merge-relations-off.png differ diff --git a/docs/assets/uiguide/simplify-on-merge-relations-on.png b/docs/assets/uiguide/simplify-on-merge-relations-on.png new file mode 100644 index 0000000000..d72489b50e Binary files /dev/null and b/docs/assets/uiguide/simplify-on-merge-relations-on.png differ diff --git a/docs/assets/uiguide/successful-create-new-component.png b/docs/assets/uiguide/successful-create-new-component.png new file mode 100644 index 0000000000..658189b7d2 Binary files /dev/null and b/docs/assets/uiguide/successful-create-new-component.png differ diff --git a/docs/assets/uiguide/tutorial-catalog-info-yaml-file.png b/docs/assets/uiguide/tutorial-catalog-info-yaml-file.png new file mode 100644 index 0000000000..55d3d24baf Binary files /dev/null and b/docs/assets/uiguide/tutorial-catalog-info-yaml-file.png differ diff --git a/docs/assets/uiguide/tutorial-component-open-in-catalog.png b/docs/assets/uiguide/tutorial-component-open-in-catalog.png new file mode 100644 index 0000000000..107974c437 Binary files /dev/null and b/docs/assets/uiguide/tutorial-component-open-in-catalog.png differ diff --git a/docs/assets/uiguide/updated-component-name-in-ui.png b/docs/assets/uiguide/updated-component-name-in-ui.png new file mode 100644 index 0000000000..7b5a9e5648 Binary files /dev/null and b/docs/assets/uiguide/updated-component-name-in-ui.png differ diff --git a/docs/backend-system/building-backends/08-migrating.md b/docs/backend-system/building-backends/08-migrating.md index 3aa06216b4..fdbac8e65f 100644 --- a/docs/backend-system/building-backends/08-migrating.md +++ b/docs/backend-system/building-backends/08-migrating.md @@ -1340,105 +1340,3 @@ const backend = createBackend(); backend.add(import('@backstage/plugin-kubernetes-backend')); /* highlight-add-end */ ``` - -### The Plugins in Backstage Repo - -The vast majority of the backend plugins that currently live in the Backstage Repo have been migrated and their respective `README`s have details on how they should be installed using the New Backend System. - -| Package | Role | Migrated | Uses Alpha Export | Link to `README` | -| ------------------------------------------------------------------ | --------------------- | -------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| @backstage-community/plugin-adr-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/adr/plugins/adr-backend/README.md) | -| @backstage-community/plugin-airbrake-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/airbrake/plugins/airbrake-backend/README.md) | -| @backstage/plugin-app-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/app-backend/README.md) | -| @backstage/plugin-auth-backend | backend-plugin | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend/README.md) | -| @backstage/plugin-auth-backend-module-atlassian-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-atlassian-provider/README.md) | -| @backstage/plugin-auth-backend-module-aws-alb-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-aws-alb-provider/README.md) | -| @backstage/plugin-auth-backend-module-gcp-iap-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-gcp-iap-provider/README.md) | -| @backstage/plugin-auth-backend-module-github-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-github-provider/README.md) | -| @backstage/plugin-auth-backend-module-gitlab-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-gitlab-provider/README.md) | -| @backstage/plugin-auth-backend-module-google-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-google-provider/README.md) | -| @backstage/plugin-auth-backend-module-guest-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-guest-provider/README.md) | -| @backstage/plugin-auth-backend-module-microsoft-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-microsoft-provider/README.md) | -| @backstage/plugin-auth-backend-module-oauth2-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-oauth2-provider/README.md) | -| @backstage/plugin-auth-backend-module-oauth2-proxy-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-oauth2-proxy-provider/README.md) | -| @backstage/plugin-auth-backend-module-oidc-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-oidc-provider/README.md) | -| @backstage/plugin-auth-backend-module-okta-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-okta-provider/README.md) | -| @backstage/plugin-auth-backend-module-pinniped-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-pinniped-provider/README.md) | -| @backstage/plugin-auth-backend-module-vmware-cloud-provider | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/auth-backend-module-vmware-cloud-provider/README.md) | -| @backstage-community/plugin-azure-devops-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/azure-devops/plugins/azure-devops-backend/README.md) | -| @backstage-community/plugin-azure-sites-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/azure-sites/plugins/azure-sites-backend/README.md) | -| @backstage-community/plugin-badges-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/badges/plugins/badges-backend/README.md) | -| @backstage-community/plugin-bazaar-backend | backend-plugin | true | true | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/bazaar/plugins/bazaar-backend/README.md) | -| @backstage/plugin-catalog-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/README.md) | -| @backstage/plugin-catalog-backend-module-aws | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-aws/README.md) | -| @backstage/plugin-catalog-backend-module-azure | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-azure/README.md) | -| @backstage/plugin-catalog-backend-module-backstage-openapi | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-backstage-openapi/README.md) | -| @backstage/plugin-catalog-backend-module-bitbucket-cloud | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-bitbucket-cloud/README.md) | -| @backstage/plugin-catalog-backend-module-bitbucket-server | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-bitbucket-server/README.md) | -| @backstage/plugin-catalog-backend-module-gcp | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-gcp/README.md) | -| @backstage/plugin-catalog-backend-module-gerrit | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-gerrit/README.md) | -| @backstage/plugin-catalog-backend-module-github | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-github/README.md) | -| @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 | 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) | -| @backstage/plugin-catalog-backend-module-scaffolder-entity-model | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-scaffolder-entity-model/README.md) | -| @backstage/plugin-catalog-backend-module-unprocessed | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-unprocessed/README.md) | -| @backstage-community/plugin-code-coverage-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/code-coverage/plugins/code-coverage-backend/README.md) | -| @backstage/plugin-devtools-backend | backend-plugin | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/devtools-backend/README.md) | -| @backstage-community/plugin-entity-feedback-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/entity-feedback/plugins/entity-feedback-backend/README.md) | -| @backstage/plugin-events-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend/README.md) | -| @backstage/plugin-events-backend-module-aws-sqs | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-aws-sqs/README.md) | -| @backstage/plugin-events-backend-module-azure | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-azure/README.md) | -| @backstage/plugin-events-backend-module-bitbucket-cloud | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-bitbucket-cloud/README.md) | -| @backstage/plugin-events-backend-module-gerrit | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-gerrit/README.md) | -| @backstage/plugin-events-backend-module-github | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-github/README.md) | -| @backstage/plugin-events-backend-module-gitlab | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-gitlab/README.md) | -| @internal/plugin-todo-list-backend | backend-plugin | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/example-todo-list-backend/README.md) | -| @backstage-community/plugin-explore-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/explore/plugins/explore-backend/README.md) | -| @backstage-community/plugin-jenkins-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/jenkins/plugins/jenkins-backend/README.md) | -| @backstage-community/plugin-kafka-backend | backend-plugin | true | true | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/kafka/plugins/kafka-backend/README.md) | -| @backstage/plugin-kubernetes-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/kubernetes-backend/README.md) | -| @backstage-community/plugin-lighthouse-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/lighthouse/plugins/lighthouse-backend/README.md) | -| @backstage-community/plugin-linguist-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/linguist/plugins/linguist-backend/README.md) | -| @backstage-community/plugin-nomad-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/nomad/plugins/nomad-backend/README.md) | -| @backstage/plugin-notifications-backend | backend-plugin | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/notifications-backend/README.md) | -| @backstage-community/plugin-periskop-backend | backend-plugin | true | true | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/periskop/plugins/periskop-backend/README.md) | -| @backstage/plugin-permission-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/permission-backend/README.md) | -| @backstage/plugin-permission-backend-module-allow-all-policy | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/permission-backend-module-policy-allow-all/README.md) | -| @backstage-community/plugin-playlist-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/playlist/plugins/playlist-backend/README.md) | -| @backstage/plugin-proxy-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/proxy-backend/README.md) | -| @backstage-community/plugin-rollbar-backend | backend-plugin | | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/rollbar/plugins/rollbar-backend/README.md) | -| @backstage/plugin-scaffolder-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/README.md) | -| @backstage/plugin-scaffolder-backend-module-azure | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-azure/README.md) | -| @backstage/plugin-scaffolder-backend-module-bitbucket | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-bitbucket/README.md) | -| @backstage/plugin-scaffolder-backend-module-bitbucket-cloud | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-bitbucket-cloud/README.md) | -| @backstage/plugin-scaffolder-backend-module-bitbucket-server | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-bitbucket-server/README.md) | -| @backstage/plugin-scaffolder-backend-module-confluence-to-markdown | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-confluence-to-markdown/README.md) | -| @backstage/plugin-scaffolder-backend-module-cookiecutter | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-cookiecutter/README.md) | -| @backstage/plugin-scaffolder-backend-module-gerrit | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-gerrit/README.md) | -| @backstage/plugin-scaffolder-backend-module-gitea | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-gitea/README.md) | -| @backstage/plugin-scaffolder-backend-module-github | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-github/README.md) | -| @backstage/plugin-scaffolder-backend-module-gitlab | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-gitlab/README.md) | -| @backstage/plugin-scaffolder-backend-module-rails | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-rails/README.md) | -| @backstage/plugin-scaffolder-backend-module-sentry | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-sentry/README.md) | -| @backstage/plugin-scaffolder-backend-module-yeoman | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend-module-yeoman/README.md) | -| @backstage/plugin-search-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend/README.md) | -| @backstage/plugin-search-backend-module-catalog | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-catalog/README.md) | -| @backstage/plugin-search-backend-module-elasticsearch | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-elasticsearch/README.md) | -| @backstage/plugin-search-backend-module-explore | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-explore/README.md) | -| @backstage/plugin-search-backend-module-pg | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-pg/README.md) | -| @backstage/plugin-search-backend-module-stack-overflow-collator | backend-plugin-module | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow-collator/README.md) | -| @backstage/plugin-search-backend-module-techdocs | backend-plugin-module | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-techdocs/README.md) | -| @backstage/plugin-signals-backend | backend-plugin | true | | [README](https://github.com/backstage/backstage/blob/master/plugins/signals-backend/README.md) | -| @backstage-community/plugin-sonarqube-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/sonarqube/plugins/sonarqube-backend/README.md) | -| @backstage-community/plugin-stack-overflow-backend | backend-plugin | | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/stack-overflow/plugins/stack-overflow-backend/README.md) | -| @backstage-community/plugin-tech-insights-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/tech-insights/plugins/tech-insights-backend/README.md) | -| @backstage-community/plugin-tech-insights-backend-module-jsonfc | backend-plugin-module | true | | [README](https://github.com/backstage/community-plugins/blob/main/workspaces/tech-insights/plugins/tech-insights-backend-module-jsonfc/README.md) | -| @backstage/plugin-techdocs-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/README.md) | -| @backstage-community/plugin-todo-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/todo/plugins/todo-backend/README.md) | -| @backstage/plugin-user-settings-backend | backend-plugin | true | true | [README](https://github.com/backstage/backstage/blob/master/plugins/user-settings-backend/README.md) | -| @backstage-community/plugin-vault-backend | backend-plugin | true | | [README](https://github.com/backstage/community-plugins/blob/master/workspaces/vault/plugins/vault-backend/README.md) | diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 50ebea0e4f..7da7fc1efa 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -24,6 +24,7 @@ import { coreServices } from '@backstage/backend-plugin-api'; - [Identity Service](./identity.md) - Deprecated user authentication service, use the [Auth Service](./auth.md) instead. - [Lifecycle Service](./lifecycle.md) - Registration of plugin startup and shutdown lifecycle hooks. - [Logger Service](./logger.md) - Plugin-level logging. +- [Metrics Service](./metrics.md) - Plugin-scoped metrics instrumentation (alpha). - [Permissions Service](./permissions.md) - Permission system integration for authorization of user actions. - [Plugin Metadata Service](./plugin-metadata.md) - Built-in service for accessing metadata about the current plugin. - [Root Config Service](./root-config.md) - Access to static configuration. diff --git a/docs/backend-system/core-services/metrics.md b/docs/backend-system/core-services/metrics.md new file mode 100644 index 0000000000..6396143fad --- /dev/null +++ b/docs/backend-system/core-services/metrics.md @@ -0,0 +1,175 @@ +--- +id: metrics +title: Metrics Service (alpha) +sidebar_label: Metrics Service (alpha) +description: Documentation for the Metrics service +--- + +The Metrics Service provides a unified interface for emitting application-level metrics from Backstage backend plugins. It wraps the [OpenTelemetry](https://opentelemetry.io/) Meter API and automatically scopes each plugin's metrics using the OpenTelemetry [Instrumentation Scope](https://opentelemetry.io/docs/concepts/instrumentation-scope/), so telemetry backends can identify which plugin produced each metric. + +:::note +This service is currently in **alpha** and is imported from `@backstage/backend-plugin-api/alpha`. The API may change in future releases. +::: + +## Setting up OpenTelemetry + +The Metrics Service does **not** configure the OpenTelemetry SDK itself. You are responsible for initializing the OpenTelemetry Node SDK — including exporters, resource attributes, and views — before starting the Backstage backend. Follow the [tutorial](../../tutorials/setup-opentelemetry.md) for more information. + +## How it Relates to OpenTelemetry Auto-Instrumentation + +The Metrics Service **complements** auto-instrumentation rather than replacing it. Auto-instrumentation captures infrastructure-level signals like HTTP request counts and durations automatically. The Metrics Service is for **application-level metrics** that only your plugin can provide — things like entities processed, tasks completed, or active sessions. + +```ts +// Auto-instrumentation provides automatically: +// http.server.request.duration{method="GET", route="/catalog/entities"} + +// MetricsService provides manually: +const processed = metrics.createCounter('entities.processed.total', { + description: 'Total entities processed during refresh', +}); +processed.add(entities.length, { operation: 'refresh' }); +``` + +## Using the Service + +Since the Metrics Service is an alpha API, the service reference is imported from `@backstage/backend-plugin-api/alpha` instead of `coreServices`. + +```ts +import { createBackendPlugin } from '@backstage/backend-plugin-api'; +import { metricsServiceRef } from '@backstage/backend-plugin-api/alpha'; + +createBackendPlugin({ + pluginId: 'todos', + register(env) { + env.registerInit({ + deps: { + metrics: metricsServiceRef, + }, + async init({ metrics }) { + const todoCount = metrics.createCounter('todos.total', { + description: 'Total number of todos', + }); + + // Later, when adding a todo: + todoCount.add(1, { 'todo.category': 'personal' }); + }, + }); + }, +}); +``` + +## Instrument Types + +The service provides both synchronous and observable (asynchronous) instrument types, matching the OpenTelemetry specification. + +### Synchronous Instruments + +Synchronous instruments are used inline where the measurement occurs. + +| Method | Description | Example Use Case | +| --------------------- | ------------------------------------------------------ | ---------------------------------- | +| `createCounter` | Monotonically increasing sum (non-negative increments) | Total requests, entities processed | +| `createUpDownCounter` | Sum that can increase or decrease | Active connections, queue depth | +| `createHistogram` | Distribution of values (e.g. durations, sizes) | Request latency, payload sizes | +| `createGauge` | Point-in-time value | CPU usage, memory utilization | + +```ts +const counter = metrics.createCounter('todos.completed.total', { + description: 'Total todos completed', +}); +counter.add(1, { 'todo.status': 'completed' }); + +const histogram = metrics.createHistogram('todo.duration', { + description: 'Time spent processing a todo', + unit: 'seconds', + advice: { explicitBucketBoundaries: [0.01, 0.05, 0.1, 0.5, 1, 5] }, +}); +histogram.record(durationInSeconds, { + 'todo.category': 'personal', + 'todo.status': 'completed', +}); + +const upDown = metrics.createUpDownCounter('todos.in_flight', { + description: 'Number of todos currently in flight', +}); +upDown.add(1); +// ... later +upDown.add(-1); +``` + +### Observable Instruments + +Observable instruments use callbacks that are invoked when a metric collection occurs. This is useful for metrics that are expensive to compute or that come from external sources like databases. + +| Method | Description | Example Use Case | +| ------------------------------- | --------------------------------------------------- | -------------------------------------- | +| `createObservableCounter` | Monotonically increasing sum, reported via callback | Total items ingested from external API | +| `createObservableUpDownCounter` | Sum that can go up or down, reported via callback | Connection pool size | +| `createObservableGauge` | Point-in-time value, reported via callback | Row counts, cache hit ratios | + +```ts +const entityCount = metrics.createObservableGauge('catalog.entities.count', { + description: 'Total amount of entities in the catalog', +}); + +entityCount.addCallback(async gauge => { + const results = await getEntityCountsByKind(); + for (const { kind, count } of results) { + gauge.observe(count, { kind }); + } +}); +``` + +## Metric Options + +All `create*` methods accept an optional `MetricOptions` object: + +| Property | Type | Description | +| ------------- | -------- | -------------------------------------------------------------------- | +| `description` | `string` | Human-readable description of the metric | +| `unit` | `string` | Unit of measurement (e.g. `'seconds'`, `'{entity}'`, `'bytes'`) | +| `advice` | `object` | Aggregation hints, such as `explicitBucketBoundaries` for histograms | + +## Type-Safe Attributes + +Metric instruments accept a generic type parameter that constrains the attributes passed to `add`, `record`, or `observe`: + +```ts +interface TodoAttributes { + 'todo.category': string; + 'todo.status': 'completed' | 'in_progress' | 'blocked'; +} + +const completed = metrics.createCounter( + 'todos.completed.total', + { description: 'Total todos completed' }, +); + +// Type-safe attributes are enforced +completed.add(1, { 'todo.category': 'personal', 'todo.status': 'completed' }); +``` + +## Advanced Configuration + +The service reads optional configuration from `app-config.yaml` under `backend.metrics.plugin..meter`. This lets operators override the OpenTelemetry Instrumentation Scope for a specific plugin without code changes. + +:::tip +Each plugin automatically receives a meter named `backstage-plugin-`. You typically won't need to configure it. +::: + +```yaml +backend: + metrics: + plugin: + catalog: + meter: + name: 'custom-catalog-meter' + version: '2.0.0' + schemaUrl: 'https://example.com/schema' +``` + +| Property | Type | Default | Description | +| ----------- | -------- | ----------------------------- | ------------------------------- | +| `name` | `string` | `backstage-plugin-` | Name of the OpenTelemetry meter | +| `version` | `string` | — | Version string for the meter | +| `schemaUrl` | `string` | — | Schema URL for the meter | diff --git a/docs/conf/user-interface/index.md b/docs/conf/user-interface/index.md index cb53ebe81c..107fde3d57 100644 --- a/docs/conf/user-interface/index.md +++ b/docs/conf/user-interface/index.md @@ -156,7 +156,6 @@ These colors form a layered neutral scale for your application backgrounds. `--b | Token Name | Description | | ----------------------------- | ------------------------------------------------------------ | | `--bui-bg-app` | The base background color of your Backstage instance. | -| `--bui-bg-popover` | The background color used for popovers, tooltips, and menus. | | `--bui-bg-neutral-1` | First elevated layer. Use for cards, dialogs, and panels. | | `--bui-bg-neutral-1-hover` | Hover state for elements on neutral-1. | | `--bui-bg-neutral-1-pressed` | Pressed state for elements on neutral-1. | diff --git a/docs/contribute/project-structure.md b/docs/contribute/project-structure.md index f58afedee8..372fb79844 100644 --- a/docs/contribute/project-structure.md +++ b/docs/contribute/project-structure.md @@ -55,12 +55,10 @@ defined in [`package.json`](https://github.com/backstage/backstage/blob/master/package.json): ```json - "workspaces": { - "packages": [ - "packages/*", - "plugins/*" - ] - }, + "workspaces": [ + "packages/*", + "plugins/*" + ], ``` Let's look at them individually. diff --git a/docs/features/software-catalog/api.md b/docs/features/software-catalog/api.md index ca8b8f638d..97f579c5a3 100644 --- a/docs/features/software-catalog/api.md +++ b/docs/features/software-catalog/api.md @@ -210,6 +210,191 @@ if `prevCursor` exists, it can be used to retrieve the previous batch of entitie it isn't possible to change any of [`filter`, `orderField`, `fullTextFilter`] when passing `cursor` as query parameters, as changing any of these properties will affect pagination. If any of `filter`, `orderField`, `fullTextFilter` is specified together with `cursor`, only the latter is taken into consideration. +### `POST /entities/by-query` + +This supports the same features as the `GET` variant, but in a `POST` body to +not have to abide by URL length limits. Additionally, it supports advanced, more +expressive querying format - see below. The response format is identical. + +#### Querying by filter predicate + +You can pass in a filter predicate to select a subset of entities in the +catalog. They are comprised of an optional logical expression tree (using +`$all`, `$any`, `$not`), ending in filter sets that can have custom matchers +(e.g. `$exists`, `$in`, `$hasPrefix`, `$contains`). + +This is an example of what such a filter predicate expression might look like: + +```js +{ + "query": { + "$all": [ + { + "kind": "Component", + "spec.type": { "$in": ["service", "website"] } + }, + { + "$not": { + "metadata.annotations.backstage.io/orphan": "true" + } + } + ] + } +} +``` + +A filter set is an object whose keys are dot separated paths into an object, and +the values are either primitives (string, number, or boolean) or custom matchers +as per below. An example of a simple such filter set is: + +```js +// All of the following must be true for a given entity (there's an +// implicit AND between them) +{ + // The kind field is matched against a literal, case insensitively + "kind": "Component", + // The type field inside the spec is matched using a custom matcher, see below + "spec.type": { "$in": ["service", "website"] } +} +``` + +The root of the query is always an object, whether there is a logic expression +tree or not. Nodes with a single key that starts with a `$` sign have special +meaning. + +- `$not`: Logical negation. + + Its value must be a single expression. Example: + + ```js + // Matches entities that do NOT have kind Component + { + "$not": { + "kind": "Component", + } + } + ``` + + Note that `$not` cannot be used in a right hand side value matcher. + + ```js + // ❌ WRONG + { "kind": { "$not": "Component" } } + // ✅ CORRECT + { "$not": { "kind": "Component" } } + ``` + +- `$all`: Require that all given expressions match each entity. + + Its value must be an array of expressions. Example: + + ```js + // Matches entities that BOTH have kind Component and type website + { + "$all": [ + { "kind": "Component" }, + { "spec.type": "website" } + ] + } + ``` + + An empty array always matches every entity. + +- `$any`: Require that at least one of a set of expressions match a given entity. + + Its value must be an array of expressions. Example: + + ```js + // Matches entities that EITHER have kind Component or type website + { + "$any": [ + { "kind": "Component" }, + { "spec.type": "website" } + ] + } + ``` + + An empty array never matches anything. + +- `$exists`: Assert on the existence of fields. + + Its value is either `true`, meaning that the field must exist on the entity + (no matter what its value), or `false`, meaning that it must not exist. + Example: + + ```js + // Matches entities that DO NOT have that annotation, ignoring what the + // value might be + { + "metadata.annotations.backstage.io/orphan": { + "$exists": false + }, + } + ``` + +- `$in`: Assert that a field has any of a set of primitive values. + + Its value must be an array of string, number, and/or boolean values. Example: + + ```js + // Matches entities whose type is EITHER service or website + { + "spec.type": { + "$in": ["service", "website"] + } + } + ``` + + The matching is case insensitive. An empty array never matches anything. + +- `$hasPrefix`: Assert that a field is a string that starts with a certain prefix text. + + Its value is a string. Example: + + ```js + // Matches entities whose project slug annotation starts with "backstage/" + { + "metadata.annotations.github.com/project-slug": { + "$hasPrefix": "backstage/" + } + } + ``` + + The matching is case insensitive, and captures both exact matches and strings + that start with the given prefix. + +- `$contains`: Assert that an array contains an element that matches the given expression. + + There is only limited support for this matcher. One use case is for relations: + + ```js + { + // Specifically type and (optionally) targetRef supported, and only + // with equality or "$in" for the targetRef + "relations": { + "$contains": { + "type": "ownedBy", + "targetRef": { + "$in": ["user:default/foo", "group:default/bar"] + } + } + } + } + ``` + + The other use case is for arrays where you match with a primitive value, such + as tags. Example: + + ```js + { + // Works for array fields whose items are primitive values + // (typically strings, but numbers and booleans are also supported) + "metadata.tags": { + "$contains": "java" + } + } + ``` + ### `GET /entities` Lists entities. diff --git a/docs/features/software-catalog/catalog-customization.md b/docs/features/software-catalog/catalog-customization.md index d62bb34505..2e984d2942 100644 --- a/docs/features/software-catalog/catalog-customization.md +++ b/docs/features/software-catalog/catalog-customization.md @@ -728,6 +728,49 @@ Notes: - Icons for groups and tabs are resolved via the app's IconsApi. When using a string icon id (for example `"dashboard"`), ensure that the corresponding icon bundles are enabled/installed in your app (see the [IconBundleBlueprint documentation](https://backstage.io/api/stable/variables/_backstage_plugin-app-react.IconBundleBlueprint.html)). - Group icons are only rendered if `showNavItemIcons` is set to `true`. +### Content ordering within groups + +By default, content items within each group are sorted alphabetically by title. You can change this with the `defaultContentOrder` option, which supports two modes: + +- **`title`** (default) — sort alphabetically by the content extension's title (case-insensitive). +- **`natural`** — preserve the natural extension discovery/registration order. + +A page-level `defaultContentOrder` sets the default for all groups, and individual groups can override it with a per-group `contentOrder`: + +```yaml +app: + extensions: + - page:catalog/entity: + config: + # Default content order for all groups + defaultContentOrder: title + + groups: + - documentation: + title: Docs + # Override: keep natural order for this group + contentOrder: natural +``` + +Note that content ordering only applies to content items within groups. Ungrouped tabs (those not matching any group definition) always retain their natural order. + +### Group aliases + +Groups can declare `aliases` — a list of other group IDs that should be treated as equivalent. Any entity content extension targeting an aliased group ID will be included in the aliasing group. This is useful when renaming or merging groups without having to reconfigure individual extensions: + +```yaml +app: + extensions: + - page:catalog/entity: + config: + groups: + - develop: + title: Develop + # Content targeting 'development' will appear in this group + aliases: + - development +``` + ### Overriding or disabling a tab's group (per extension) Each entity content extension (tabs on the entity page) can declare a default `group` in code. You can override or disable this per installation in `app-config.yaml` using the extension's config: diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index ac8baadad5..f9ab41e37a 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -607,7 +607,7 @@ component, but there will always be one ultimate owner. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- | -| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) | +| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownedBy`, and reverse `ownerOf`](well-known-relations.md#ownedby-and-ownerof) | ### `spec.system` [optional] @@ -811,7 +811,7 @@ Template, but there will always be one ultimate owner. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- | -| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) | +| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownedBy`, and reverse `ownerOf`](well-known-relations.md#ownedby-and-ownerof) | ## Kind: API @@ -921,7 +921,7 @@ one ultimate owner. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- | -| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) | +| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownedBy`, and reverse `ownerOf`](well-known-relations.md#ownedby-and-ownerof) | ### `spec.system` [optional] @@ -1121,7 +1121,7 @@ Describes the following entity kind: | `apiVersion` | `backstage.io/v1alpha1` | | `kind` | `Resource` | -A resource describes the infrastructure a system needs to operate, like BigTable +A Resource describes the infrastructure a system needs to operate, like BigTable databases, Pub/Sub topics, S3 buckets or CDNs. Modelling them together with components and systems allows to visualize resource footprint, and create tooling around them. @@ -1164,7 +1164,7 @@ resource, but there will always be one ultimate owner. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- | -| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) | +| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownedBy`, and reverse `ownerOf`](well-known-relations.md#ownedby-and-ownerof) | ### `spec.type` [required] @@ -1262,7 +1262,7 @@ but there will always be one ultimate owner. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- | -| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) | +| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownedBy`, and reverse `ownerOf`](well-known-relations.md#ownedby-and-ownerof) | ### `spec.domain` [optional] @@ -1334,7 +1334,7 @@ but there will always be one ultimate owner. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | ------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------- | -| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownerOf`, and reverse `ownedBy`](well-known-relations.md#ownedby-and-ownerof) | +| [`Group`](#kind-group) (default), [`User`](#kind-user) | Same as this entity, typically `default` | [`ownedBy`, and reverse `ownerOf`](well-known-relations.md#ownedby-and-ownerof) | ### `spec.subdomainOf` [optional] diff --git a/docs/getting-started/create-a-component.md b/docs/getting-started/create-a-component.md index 29ba99db83..b4e5c6ab19 100644 --- a/docs/getting-started/create-a-component.md +++ b/docs/getting-started/create-a-component.md @@ -6,38 +6,92 @@ description: Leverage the scaffolder to start creating components with best prac Audience: Developers -## Summary +## Overview -This guide will walk you through how to use Software Templates to create new components with baked in best practices. +Components can be created in the Software Catalog using software templates. Templates load skeletons of code, which can include some variables, and incorporate your company's best practices. The templates are published to a location, such as GitHub or GitLab. + +The standalone Backstage application includes the `Example Node.js Template`, which is an example template for the scaffolder that creates and registers a simple Node.js service. You can also [create your own templates](../features/software-templates/adding-templates.md). ## Prerequisites -:::note +For this example, the default Node.js template will be used. The template creates a repository in GitHub and adds the necessary files to it so that the component is integrated into the Software Catalog. Because you are creating a repository, you must first create an integration between Backstage and GitHub. -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` +- You should have already [installed a standalone app](./index.md). -::: +- Register the [GitHub Scaffolder Action module](../features/software-templates/builtin-actions.md#installing-action-modules). -You should already have [a standalone app](./index.md). +- [Set up a GitHub Integration](../getting-started/config/authentication.md#setting-up-a-github-integration) with Backstage, using a GitHub Personal Access Token. -You will also need to register the [GitHub Scaffolder Action module](../features/software-templates/builtin-actions.md#installing-action-modules) before moving forward. +## Creating the component -## Creating your component +To create the component: -- Go to `create` and choose to create a website with the `Example Node.js Template` -- Type in a name, let's use `tutorial` and click `Next Step` +1. Select `Create`. -![Software template deployment input screen asking for a name](../assets/getting-started/b-scaffold-1.png) + ![select create for new component](../assets/uiguide/select-create-for-new-component.png) -- You should see the following screen: +2. Select `Service` in the `CATEGORIES` dropdown list. +3. Select the `Owner`. For this example, you can select `guest`. +4. Select `Choose` in the `Example Node.js Template`. -![Software template deployment input screen asking for the GitHub username, and name of the new repo to create](../assets/getting-started/b-scaffold-2.png) + ![setup-nodejs-categories-owners](../assets/uiguide/setup-nodejs-cat-owner.png) -- For host, it should default to github.com -- As owner, type your GitHub username -- For the repository name, type `tutorial`. Go to the next step +5. For this example, enter `tutorial` for the `Name` of the service and select `NEXT`. -- Review the details of this new service, and press `Create` if you want to - deploy it like this. -- You can follow along with the progress, and as soon as every step is - finished, you can take a look at your new service + ![enter name of new component](../assets/uiguide/enter-name-of-new-component.png) + +6. Enter your GitHub user name as the `Owner`. +7. Enter `tutorial` for the `Repository` and select `REVIEW`. + + ![select review create component](../assets/uiguide/select-review-create-component.png) + +8. Review the information and select `CREATE`. + + ![select create to make component](../assets/uiguide/select-create-to-make-component.png) + +If you see an error message, similar to the following, + + ![error creating new component](../assets/uiguide/error-creating-new-component.png) + +Perform the following steps: + +1. Close the Backstage app. +2. Enter `CTRL-C` in the terminal window to stop the Backstage frontend and backend. +3. In the terminal window, enter: + + ``` + export NODE_OPTIONS=--no-node-snapshot + ``` + + > **NOTE:** + > The [no-node-snapshot](../features/software-templates/index.md#prerequisites) `NODE_OPTIONS` environment variable is required in order to use the templates. + +4. Enter `yarn start` to restart the Backstage application. +5. Repeat steps to create the component. + +Otherwise, you can follow along with the progress, and as soon as every step is finished, you can take a look at your new service in either the repository or the Catalog. + +![run of example of create component](../assets/uiguide/successful-create-new-component.png) + +Selecting `REPOSITORY` displays the `catalog-info.yaml`file and other project setup files that were created for the new component in the main branch of the `tutorial` repository. + +The `catalog-info.yaml` file describes the entity for the Software Catalog. [Descriptor Format of Catalog Entities](../features/software-catalog/descriptor-format.md) provides additional information. + +``` + apiVersion: backstage.io/v1alpha1 + kind: Component + metadata: + name: "tutorial" + spec: + type: service + owner: user:guest + lifecycle: experimental +``` + +Selecting `OPEN IN CATALOG` displays details of the new component, such as its relationships, links, and subcomponents. + +![select open in catalog](../assets/uiguide/tutorial-component-open-in-catalog.png) + +Selecting `Home` in the sidebar, displays the new `tutorial` component in the Catalog. + +![new tutorial component in software catalog](../assets/uiguide/new-tutorial-component-in-software-catalog.png) diff --git a/docs/getting-started/filter-catalog.md b/docs/getting-started/filter-catalog.md new file mode 100644 index 0000000000..3e0de8cdeb --- /dev/null +++ b/docs/getting-started/filter-catalog.md @@ -0,0 +1,57 @@ +--- +id: filter-catalog +title: Filtering the Catalog +description: Filtering the Catalog. +--- + +Audience: All + +## Overview + +The Catalog can be filtered by any combination of owner, kind, type, lifecycle, processing status, namespace, and name. [Customize Filters](../features/software-catalog/catalog-customization.md#customize-filters) provides information on how to modify the available filter criteria. + +![Catalog filter options](../assets/uiguide/catalog-filter-options.png) + +The [Technical Overview](../overview/technical-overview.md#software-catalog-system-model) provides a description of the types of entities displayed in the Catalog. + +## Filtering the Catalog + +You can filter the Catalog using a combination of the following: + +- **Filter by name** + + Enter one or more consecutive letters into the `Filter` field. As you type the letters, the entities whose names do not contain that string will be filtered out of the displayed list. + + ![Filter catalog by name](../assets/uiguide/filter-by-name.png) + +- **Filter by kind** + + Use the `Kind` dropdown list to select which kind of entity to show in the list: + + - API + - Component + - Group + - Location + - System + - Template + - User + +- **Filter by Type** + + Use the `Type` dropdown list to select which type of entity to show in the list. The selections available in the dropdown list depend on the kind of entity selected in the `Kind` list, and the types of entity you have registered for that kind. + +- **Filter by Owner** + + Use the `Owner` dropdown to filter the Catalog list by who owns the entity. + +- **Filter by Lifecycle** + + Use the `Lifecycle` dropdown to filter the Catalog list by lifecycle. + +- **Filter by Processing Status** + + Use the `Processing Status` dropdown to restrict the displayed list to only include those entities which are [orphaned](../features/software-catalog/life-of-an-entity.md#orphaning) or [in error](../features/software-catalog/life-of-an-entity.md#errors). + +- **Filter by Namespace** + + Use the `Namespace` dropdown to filter the catalog list by namespace associated with the entity. diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index d7c4ee96c8..5893877a55 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -184,8 +184,14 @@ Choose the correct next steps for your user role, if you're likely to be deployi - Using your Backstage instance - [Logging into Backstage](./logging-in.md) + - [Viewing the Catalog](./viewing-catalog.md) + - [Viewing what you own](./view-what-you-own.md) + - [Viewing entity relationships](./viewing-entity-relationships.md) + - [Filtering the Catalog](./filter-catalog.md) - [Register a component](./register-a-component.md) - [Create a new component](./create-a-component.md) + - [Update a component](./update-a-component.md) + - [Unregistering and deleting a component](./unregister-delete-component.md) Share your experiences, comments, or suggestions with us: [on discord](https://discord.gg/backstage-687207715902193673), file issues for any diff --git a/docs/getting-started/register-a-component.md b/docs/getting-started/register-a-component.md index 4e2076ee8d..1f2fda2871 100644 --- a/docs/getting-started/register-a-component.md +++ b/docs/getting-started/register-a-component.md @@ -8,38 +8,53 @@ Audience: Developers :::note Note Entity files are stored in YAML format, if you are not familiar with YAML, you can learn more about it [here](https://yaml.org). + +[Descriptor Format of Catalog Entities](../features/software-catalog/descriptor-format.md) provides additional information on the format of the YAML entity files. ::: -## Summary +## Overview This guide will walk you through how to pull Backstage data from other locations manually. There are integrations that will automatically do this for you. +When registering a component, you can: + +- Link to an existing entity file: The file is analyzed to determine which entities are defined, and the entities are added to the Scaffolded Backstage App Catalog. For example, `https://github.com/backstage/backstage/blob/master/catalog-info.yaml`. + +- Link to a repository: All `catalog-info.yaml` files are discovered in the repository and their defined entities are added to the Scaffolded Backstage App Catalog. For example, `https://github.com/backstage/backstage`. + + :::note Note + If no entities are found, a Pull Request is created that adds an example `catalog-info.yaml` file to the repository. When the Pull Request is merged, the Scaffolded Backstage App Catalog loads all of the defined entities. + + ::: + ## Prerequisites -You should have already [have a standalone app](./index.md). +- You should have already [installed a standalone app](./index.md). -## 1. Finding our template +## Registering a component -Register a new component, by going to `create` and choose `Register existing component` +To manually register a component in the Software Catalog: - +1. Select `Create`. +2. Select `REGISTER EXISTING COMPONENT`. -![Software template main screen, with a blue button to add an existing component](../assets/getting-started/b-existing-1.png) + ![Select Register existing component.](../assets/uiguide/select-register-existing-component.png) -## 2. Filling out the template +3. Fill out the template. -For repository URL, use `https://github.com/backstage/backstage/blob/master/catalog-info.yaml`. This is used in our [demo site](https://demo.backstage.io) catalog. + The standalone Backstage application includes one template. For this example, enter the repository URL to the entity file, `https://github.com/backstage/backstage/blob/master/catalog-info.yaml`. This is used in the Backstage [demo site](https://demo.backstage.io) catalog. -![Register a new component wizard, asking for an URL to the existing component YAML file](../assets/getting-started/b-existing-2.png) + ![enter url of component entity file.](../assets/uiguide/enter-url-of-component.png) -Hit `Analyze` and review the changes. +4. Select `ANALYZE`. +5. If the changes from `ANALYZE` are correct, select `IMPORT`. -## 3. Import the entity + ![review and select import.](../assets/uiguide/review-select-import.png) -If the changes from `Analyze` are correct, click `Apply`. + If your entity was successfully imported, the details will be displayed. -![Register a new component wizard, showing the metadata for the component YAML we use in this tutorial](../assets/getting-started/b-existing-3.png) + ![details of registered component.](../assets/uiguide/details-for-registered-backstage-website.png) -You should receive a message that your entities have been added. +6. Select `Home` to view your new entity in the Software Catalog. -If you go back to `Home`, you should be able to find `backstage`. You can click it and see the details for this new entity. + ![Backstage website component in software catalog.](../assets/uiguide/backstage-website-registered-catalog-view.png) diff --git a/docs/getting-started/unregister-delete-component.md b/docs/getting-started/unregister-delete-component.md new file mode 100644 index 0000000000..f8034bf3ed --- /dev/null +++ b/docs/getting-started/unregister-delete-component.md @@ -0,0 +1,66 @@ +--- +id: unregister-delete-component +title: Unregistering and deleting a component +description: Unregistering and deleting a component from the catalog +--- + +Audience: Developers + +## Overview + +URLs to YAML files that you registered either using the `Create` button or by adding to your app-config file are both handled by entity providers. + +[Implicit deletion](../features/software-catalog/life-of-an-entity.md#implicit-deletion) occurs when an entity provider issues a deletion of an entity. That entity, as well as the entire tree of entities processed out of it are considered for immediate deletion. + +However, you are also able to manually unregister an entity from the Catalog or perform a direct, [explicit deletion](../features/software-catalog/life-of-an-entity.md#explicit-deletion) of individual entities. + +## Unregistering an entity + +You can unregister an entity so it will not be displayed in the Catalog but still keep its `catalog-info.yaml` file in the repository. This provides the ability to register the entity with the Catalog again in the future. + +To unregister an entity: + +1. In the Catalog, select the entity you want to unregister. In this example, `mytutorial` is being unregistered. + +2. Select the three dots. + +3. Select `Unregister entity` in the dropdown menu. + + ![Screenshot of selecting unregister entity.](../assets/uiguide/select-unregister-entity-from-three-dots.png) + +4. Select `UNREGISTER LOCATION`. The entity is removed from the Catalog. + + ![Screenshot of confirming unregister entity.](../assets/uiguide/confirm-unregister-entity.png) + +## Deleting an entity + +You can also delete an entity from the Catalog. However, this requires that you also delete the `catalog-info.yaml` entity definition file associated with the entity. + +To delete an entity: + +1. Delete the following entity definition files for the entity in the repository: + + - catalog-info.yaml + - index.js + - package.json + +2. In the Backstage App Catalog view, select the entity being deleted. In this example, `mytutorial` is being deleted. + + Since you have deleted the entity definition files, an error is displayed that states the `catalog-info.yaml` file cannot be found. + + ![Screenshot of error finding catalog info yaml file.](../assets/uiguide/error-message-catalog-info-file-deleted.png) + +3. Select the three dots. +4. Select `Unregister entity` in the dropdown menu. + + ![Screenshot of selecting unregister entity.](../assets/uiguide/select-unregister-entity-from-three-dots.png) + +5. Select `ADVANCED OPTIONS`. + + ![Screenshot of selecting advanced options.](../assets/uiguide/select-advanced-options.png) + +6. Select `DELETE ENTITY`. + + ![Screenshot of selecting delete entity.](../assets/uiguide/select-delete-entity.png) + +A confirmation message that the entity has been successfully deleted is briefly displayed. The entity is no longer displayed in the Catalog. diff --git a/docs/getting-started/update-a-component.md b/docs/getting-started/update-a-component.md new file mode 100644 index 0000000000..ffe21bc8b3 --- /dev/null +++ b/docs/getting-started/update-a-component.md @@ -0,0 +1,33 @@ +--- +id: update-a-component +title: Update a Component +description: Update an existing component. +--- + +Audience: Developers + +## Overview + +Components in the Software Catalog are created using a software template. The template generates a `catalog-info.yaml` file in either GitHub or GitLab that defines the entity. To update the component, you must edit its corresponding `catalog-info.yaml` entity definition file. + +## Updating the component + +To update a component using the Backstage UI: + +1. Select the "Edit" icon associated with the component. In this example, the `tutorial` entity is selected. + + ![select edit icon for component](../assets/uiguide/select-edit-icon-for-component.png) + + The associated `catalog-info.yaml` file is displayed. + + ![tutorial component catalog-info.yaml file](../assets/uiguide/tutorial-catalog-info-yaml-file.png) + +2. Make your changes to the YAML file. In this example, the name of the component is changed to `mytutorial`. + + ![component name updated](../assets/uiguide/component-name-updated.png) + +3. Select `Commit changes` to commit your changes to the appropriate branch and go through your normal PR review procedure. + +4. Once the updated `catalog-info.yaml` file has been merged into the branch associated with the component, then you will see the updated information in the Software Catalog. + + ![updated component name in ui](../assets/uiguide/updated-component-name-in-ui.png) diff --git a/docs/getting-started/view-what-you-own.md b/docs/getting-started/view-what-you-own.md new file mode 100644 index 0000000000..fc8b1feeae --- /dev/null +++ b/docs/getting-started/view-what-you-own.md @@ -0,0 +1,22 @@ +--- +id: view-what-you-own +title: Viewing what you own +description: View the entities that you own either directly or through a group +--- + +Audience: All + +You can own entities either directly or through a group that you're part of. + +To view the entities that you own: + +1. Select `Home` in the sidebar. +2. Select `User` in the `Kind` dropdown list. +3. Select your username in the `All Users` list. + +A page is displayed that shows the entities of which you have ownership, either directly, or through a group of which you are a member. You can toggle between showing: + +- `Direct Relations` - entities that you directly own +- `Aggregated Relations` - entities that you own through your group + +![Screenshot of the Backstage portal entities owned by guest user.](../assets/uiguide/entities-owned-by-me.png) diff --git a/docs/getting-started/viewing-catalog.md b/docs/getting-started/viewing-catalog.md new file mode 100644 index 0000000000..acfa470cc2 --- /dev/null +++ b/docs/getting-started/viewing-catalog.md @@ -0,0 +1,85 @@ +--- +id: viewing-catalog +title: Viewing the Catalog +sidebar_label: Viewing the Catalog +description: Viewing the Catalog +--- + +Audience: All + +## Overview + +Initially, when you log into your standalone Backstage App, `Home` is selected in the sidebar, which displays the Catalog in the main panel. + +There are four main entities that you should become familiar with: + +- `Components` - Individual pieces of software that can be tracked in source control and can implement APIs for other components to consume. +- `Resources` - The physical or virtual infrastructure needed to operate a component. +- `Systems` - A collection of resources and components that cooperate to perform a function by exposing one or several public APIs. It hides the resources and private APIs between the components from the consumer. +- `Domains` - A collection of systems that share terminology, domain models, metrics, KPIs, business purpose, or documentation. + +The [Technical Overview](../overview/technical-overview.md#software-catalog-system-model) provides a description of all of the types of entities displayed in the Catalog. + +It should be noted that you can also [create your own kinds of entities](../features/software-catalog/extending-the-model.md#adding-a-new-kind), if you need to model something in your organization that does not map to one of the existing entity types. + +Initially, the Catalog displays registered entities matching the following filter settings: + +- `Kind` - Component +- `Type` - all +- `Owner` - Owned +- `Lifecycle` - list of [lifecycle](../features/software-catalog/descriptor-format.md#speclifecycle-required-1) values of entities in the Catalog +- `Processing Status` - normal +- `Namespace` - The ID of a [namespace](../features/software-catalog/descriptor-format.md#namespace-optional) to which the entity belongs + +You can change the initial setting for the [Owner](../features/software-catalog/catalog-customization.md#initially-selected-filter) and [Kind](../features/software-catalog/catalog-customization.md#initially-selected-kind) filters. + +## Informational columns for each entity + +For each kind of entity, a set of columns display information regarding the entity. For example, the default set of information for a `Component` is: + +- `Name` - the name of the component +- `System` - an optional field that references the system to which the component belongs +- `Owner` - the owner of the component +- `Type` - common types are as follows, but you can [create a new type](../features/software-catalog/extending-the-model.md#adding-a-new-type-of-an-existing-kind) to meet your organization's needs + - `service` - a backend service, typically exposing an API + - `website` - a website + - `library` - a software library, such as an npm module or a Java library +- `Lifecycle` + - `experimental` - an experiment or early, non-production component, signaling + that users may not prefer to consume it over other more established + components, or that there are low or no reliability guarantees + - `production` - an established, owned, maintained component + - `deprecated` - a component that is at the end of its lifecycle, and may + disappear at a later point in time +- `Description` - an optional field that describes the component. +- `Tags` - an optional field that can be used for searching +- `Actions` - see [Catalog Actions](#catalog-actions) + +You can modify the columns associated with each kind of entity, following the instructions in [Customize Columns](../features/software-catalog/catalog-customization.md#customize-columns). + +## Catalog Actions + +For each entity, there are a set of actions that are available. + +![Screenshot explaining entity actions.](../assets/uiguide/entity-actions.png) + +From left to right, the actions are: + +- View - View the `catalog-info.yaml` file that defines the entity. +- Edit - Edit the `catalog-info.yaml` file that defines the entity. See [Updating a Component](../getting-started/update-a-component.md) +- Star - Designate the entity as a favorite. You can [filter](../getting-started/filter-catalog.md) the catalog for starred entities. + +[Customize Actions](../features/software-catalog/catalog-customization.md#customize-actions) describes how you can modify the actions that are displayed. + +## Viewing entity details + +Selecting an entity in the main panel displays details of the entity. The type of details depends on the type of entity. For example, selecting a Component, such as `example-website`, displays the following details: + +- `About` - Metadata for the entity, such as description, owner, tags, and domain. +- `Relations` - see [Viewing entity relationships](../getting-started/viewing-entity-relationships.md) +- `Links` - any links associated with the entity +- `Has subcomponents` - An entity reference to another component of which the component is a part + +Selecting a System, such as `examples`, displays `About`, `Relations`, and `Links` similar to a Component, but it also includes `Has components`, `APIs` and `Has Resources`. + +![Details of system entity.](../assets/uiguide/details-system-entity.png) diff --git a/docs/getting-started/viewing-entity-relationships.md b/docs/getting-started/viewing-entity-relationships.md new file mode 100644 index 0000000000..c791ab5bc7 --- /dev/null +++ b/docs/getting-started/viewing-entity-relationships.md @@ -0,0 +1,98 @@ +--- +id: viewing-entity-relationships +title: Viewing entity relationships +description: View the relationships between the entities in the Catalog +--- + +Audience: All + +Each of the entities in the Catalog has various relationships to each other. For example, the demo data includes an API that provides data to a website. `guests` is the owner of the API and the website, and anyone signed in as part of the `guests` group can maintain them. + +To see these relationships: + +1. Select the name of the component in the main panel, in this example, `example-website`. + + ![Screenshot of the Backstage portal with example-website selected.](../assets/uiguide/select-example-website.png) + + A page is displayed that includes a `Relations` section. This section displays the selected entity and any other types of entities to which it is related. Each relationship is also designated, such as `hasPart/partOf` and `apiProvidedBy/providesApi`. [Well-known Relations between Catalog Entities](../features/software-catalog/well-known-relations.md) describes the most common relationships, but you can also [create your own relationships](../features/software-catalog/extending-the-model.md#adding-a-new-relation-type). + +2. Selecting any of the related entities allows you to drill down further through the system model. + + ![Screenshot of the example-website Relations panel.](../assets/uiguide/example-website-relationships.png) + +## Filtering the relationships + +You can also view the relationships in the [Catalog Graph](../features/software-catalog/creating-the-catalog-graph.md). This view allows you to filter what entities and relationships to display. + +To display the Catalog Graph: + +1. Select the name of the component in the main panel, in this example, `example-website`. + + A page is displayed that includes a `Relations` section. + +2. Select `View graph` in the `Relations` section. + + ![Select View graph.](../assets/uiguide/select-view-graph.png) + + The `Catalog Graph` is displayed. + + ![Screenshot of Catalog Graph.](../assets/uiguide/catalog-graph.png) + +### Setting the filters + +The `Catalog Graph` automatically reflects any changes you make to the filter settings. You can set the following filters: + +- `MAX DEPTH` + + - `MAX DEPTH` = 1 + + ![Max Depth at 1](../assets/uiguide/max-depth-1.png) + + - `MAX DEPTH` = infinite + + ![Max Depth at infinite](../assets/uiguide/max-depth-infinite.png) + +- `KINDS` - select what kinds of entities you want to view, default is all kinds +- `RELATIONS` - select which relationships you want to view, default is all relationships +- `Direction` - orientation in which you want to view the entity and its associated nodes + - Top to bottom + - Bottom to top + - Left to right + - Right to left +- `Curve` + + - `Curve` = Monotone + + ![Curve at Monotone.](../assets/uiguide/curve-monotone.png) + + - `Curve` = Step Before + + ![Curve at Step Before.](../assets/uiguide/curve-step-before.png) + +You can also toggle: + +- `Simplified` + - On = simple view + - Off = detailed view +- `Merge relations` + + - On = You see the relationship from the selected entity to the nodes and from the nodes to the selected entity. + - Off = You only see relations from the selected entity to its nodes. + + The following graphics illustrate the view of the nodes and relationships, based on the combination of the settings of `Simplified` and `Merge relations`. + + - `Simplified` = On and `Merge Relations` = On + + ![Simplified is On and Merge Relations is On.](../assets/uiguide/simplify-on-merge-relations-on.png) + + - `Simplified` = On and `Merge Relations` = Off + + ![Simplified is On and Merge Relations is Off.](../assets/uiguide/simplify-on-merge-relations-off.png) + + - `Simplified` = Off and `Merge Relations` = On + + ![Simplified is Off and Merge Relations is On.](../assets/uiguide/simplify-off-merge-relations-on.png) + + - `Simplified` = Off and `Merge Relations` = Off + + ![Simplified is Off and Merge Relations is Off.](../assets/uiguide/simplify-off-merge-relations-off.png) diff --git a/docs/landing-page/doc-landing-page.md b/docs/landing-page/doc-landing-page.md index 8697144bd8..98a0ad9fb9 100644 --- a/docs/landing-page/doc-landing-page.md +++ b/docs/landing-page/doc-landing-page.md @@ -33,13 +33,26 @@ description: Documentation landing page. Configure, Deploy, & Upgrade.

@@ -60,7 +73,7 @@ description: Documentation landing page.
  • Search
  • Software Catalog
  • Software Templates (aka Scaffolder)
  • -
  • TechDocs
  • +
  • TechDocs - a docs-like-code solution
  • diff --git a/docs/openapi/01-getting-started.md b/docs/openapi/01-getting-started.md index 875a369dbf..c300ac1370 100644 --- a/docs/openapi/01-getting-started.md +++ b/docs/openapi/01-getting-started.md @@ -26,7 +26,14 @@ This tutorial assumes that you're already familiar with the following, 1. How to build a Backstage plugin. 2. `Express.js` and `Typescript` -3. OpenAPI 3.0 schemas +3. OpenAPI 3.1 schemas + +:::note OpenAPI Version Support +Backstage supports both OpenAPI 3.0 and 3.1 specifications. If you have existing OpenAPI 3.0 specs, we recommend that you migrate them to 3.1. The main changes are: + +- Replace `nullable: true` with `type: ['string', 'null']` or use `anyOf`/`oneOf` +- Remove `allowReserved` from path parameters (only valid on query/cookie parameters in 3.1) + ::: ### Setting up diff --git a/docs/plugins/integrating-plugin-into-software-catalog.md b/docs/plugins/integrating-plugin-into-software-catalog.md index 911d74b019..417b932451 100644 --- a/docs/plugins/integrating-plugin-into-software-catalog.md +++ b/docs/plugins/integrating-plugin-into-software-catalog.md @@ -94,7 +94,7 @@ const systemPage = ( - + diff --git a/docs/releases/v1.49.0-next.0-changelog.md b/docs/releases/v1.49.0-next.0-changelog.md new file mode 100644 index 0000000000..fcfc092d5e --- /dev/null +++ b/docs/releases/v1.49.0-next.0-changelog.md @@ -0,0 +1,2362 @@ +# Release v1.49.0-next.0 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.49.0-next.0](https://backstage.github.io/upgrade-helper/?to=1.49.0-next.0) + +## @backstage/cli-common@0.2.0-next.0 + +### Minor Changes + +- 56bd494: Added `targetPaths` and `findOwnPaths` as replacements for `findPaths`, with a cleaner separation between target project paths and package-relative paths. + + To migrate existing `findPaths` usage: + + ```ts + // Before + import { findPaths } from '@backstage/cli-common'; + const paths = findPaths(__dirname); + + // After — for target project paths (cwd-based): + import { targetPaths } from '@backstage/cli-common'; + // paths.targetDir → targetPaths.dir + // paths.targetRoot → targetPaths.rootDir + // paths.resolveTarget('src') → targetPaths.resolve('src') + // paths.resolveTargetRoot('yarn.lock') → targetPaths.resolveRoot('yarn.lock') + + // After — for package-relative paths: + import { findOwnPaths } from '@backstage/cli-common'; + const own = findOwnPaths(__dirname); + // paths.ownDir → own.dir + // paths.ownRoot → own.rootDir + // paths.resolveOwn('config/jest.js') → own.resolve('config/jest.js') + // paths.resolveOwnRoot('tsconfig.json') → own.resolveRoot('tsconfig.json') + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.7 + +## @backstage/integration@1.21.0-next.0 + +### Minor Changes + +- d933f62: Add configurable throttling and retry mechanism for GitLab integration. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-catalog-backend@3.5.0-next.0 + +### Minor Changes + +- bf71677: Added opentelemetry metrics for SCM events: + + - `catalog.events.scm.messages` with attribute `eventType`: Counter for the number of SCM events actually received by the catalog backend. The `eventType` is currently either `location` or `repository`. + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- fbf382f: Minor internal optimisation +- 1ee5b28: Migrates existing catalog metrics to use the alpha MetricsService. This release is a 1:1 migration with no breaking changes. +- 3181973: Changed the `search` table foreign key to point to `final_entities` instead of `refresh_state` +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-catalog-node@2.1.0-next.0 + +### Minor Changes + +- bf71677: Added the ability for SCM events subscribers to mark the fact that they have taken actions based on events, which produces output metrics: + + - `catalog.events.scm.actions` with attribute `action`: Counter for the number of actions actually taken by catalog internals or other subscribers, based on SCM events. The `action` is currently either `create`, `delete`, `refresh`, or `move`. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/backend-test-utils@1.11.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-notifications-backend-module-slack@0.4.0-next.0 + +### Minor Changes + +- 749ba60: Add an extension for custom Slack message layouts + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.24-next.0 + +## @backstage/app-defaults@1.7.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/backend-app-api@1.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/backend-defaults@0.15.3-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- d933f62: Add configurable throttling and retry mechanism for GitLab integration. +- b99158a: Fixed `yarn backstage-cli config:check --strict --config app-config.yaml` config validation error by adding + an optional `default` type discriminator to PostgreSQL connection configuration, + allowing `config:check` to properly validate `default` connection configurations. +- 1ee5b28: Adds an alpha `MetricsService` to provide a unified interface for metrics instrumentation across Backstage plugins. +- Updated dependencies + - @backstage/cli-node@0.2.19-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-app-api@1.5.1-next.0 + - @backstage/backend-dev-utils@0.1.7 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/backend-dynamic-feature-service@0.7.10-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/cli-node@0.2.19-next.0 + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/plugin-catalog-backend@3.5.0-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-app-node@0.1.43-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-events-backend@0.5.12-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/backend-openapi-utils@0.6.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/backend-plugin-api@1.7.1-next.0 + +### Patch Changes + +- 1ee5b28: Adds an alpha `MetricsService` to provide a unified interface for metrics instrumentation across Backstage plugins. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/backend-test-utils@1.11.1-next.0 + +### Patch Changes + +- 1ee5b28: Adds a new metrics service mock to be leveraged in tests +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-app-api@1.5.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/catalog-client@1.13.1-next.0 + +### Patch Changes + +- d2494d6: Minor update to catalog client docs +- Updated dependencies + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + +## @backstage/cli@0.35.5-next.0 + +### Patch Changes + +- 246877a: Updated dependency `bfj` to `^9.0.2`. + +- bba2e49: Internal refactor to use new concurrency utilities from `@backstage/cli-node`. + +- fd50cb3: Added `translations export` and `translations import` commands for managing translation files. + + The `translations export` command discovers all `TranslationRef` definitions across frontend plugin dependencies and exports their default messages as JSON files. The `translations import` command generates `TranslationResource` wiring code from translated JSON files, ready to be plugged into the app. + + Both commands support a `--pattern` option for controlling the message file layout, for example `--pattern '{lang}/{id}.json'` for language-based directory grouping. + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. + +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. + +- 092b41f: Updated dependency `webpack` to `~5.105.0`. + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/cli-node@0.2.19-next.0 + - @backstage/eslint-plugin@0.2.2-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/module-federation-common@0.1.0 + - @backstage/release-manifests@0.0.13 + - @backstage/types@1.2.2 + +## @backstage/cli-node@0.2.19-next.0 + +### Patch Changes + +- 06c2015: Added `runConcurrentTasks` and `runWorkerQueueThreads` utilities, moved from the `@backstage/cli` internal code. +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/codemods@0.1.55-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + +## @backstage/config-loader@1.10.9-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/core-app-api@1.19.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + +## @backstage/core-compat-api@0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + +## @backstage/core-components@0.18.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/theme@0.7.2 + - @backstage/version-bridge@1.0.12 + +## @backstage/core-plugin-api@1.12.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + +## @backstage/create-app@0.7.10-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + +## @backstage/dev-utils@1.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + +## @backstage/eslint-plugin@0.2.2-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 + +## @backstage/frontend-app-api@0.15.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + +## @backstage/frontend-defaults@0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-app@0.4.1-next.0 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-app-api@0.15.1-next.0 + +## @backstage/frontend-dynamic-feature-loader@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/config@1.3.6 + - @backstage/module-federation-common@0.1.0 + +## @backstage/frontend-plugin-api@0.14.2-next.0 + +### Patch Changes + +- 9c81af9: Made the `pluginId` property optional in the `FrontendFeature` type, allowing plugins published against older versions of the framework to be used without type errors. +- Updated dependencies + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + +## @backstage/frontend-test-utils@0.5.1-next.0 + +### Patch Changes + +- 909c742: Switched `MockTranslationApi` and related test utility imports from `@backstage/core-plugin-api/alpha` to the stable `@backstage/frontend-plugin-api` export. The `TranslationApi` type in the API report is now sourced from a single package. This has no effect on runtime behavior. +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-app@0.4.1-next.0 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/integration-react@1.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @backstage/repo-tools@0.16.6-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- 2a51546: Fixed prettier existence checks in OpenAPI commands to use `fs.pathExists` instead of checking the resolved path string, which was always truthy. +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- 18a946c: Updated `@microsoft/api-extractor` to `7.57.3` and added tests for `getTsDocConfig` +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/cli-node@0.2.19-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + +## @techdocs/cli@1.10.6-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-techdocs-node@1.14.3-next.0 + +## @backstage/test-utils@1.7.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/ui@0.12.1-next.0 + +### Patch Changes + +- a1f4bee: Made Accordion a `bg` provider so nested components like Button auto-increment their background level. Updated `useDefinition` to resolve `bg` `propDef` defaults for provider components. + +- 8909359: Fixed focus-visible outline styles for Menu and Select components. + + **Affected components:** Menu, Select + +- 0f462f8: Improved type safety in `useDefinition` by centralizing prop resolution and strengthening the `BgPropsConstraint` to require that `bg` provider components declare `children` as a required prop in their OwnProps type. + +- 8909359: Added proper cursor styles for RadioGroup items. + + **Affected components:** RadioGroup + +- Updated dependencies + - @backstage/version-bridge@1.0.12 + +## @backstage/plugin-api-docs@0.13.5-next.0 + +### Patch Changes + +- 9c9d425: Fixed invisible text in parameter input fields when using dark mode in OpenAPI definition pages +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-app@0.4.1-next.0 + +### Patch Changes + +- 909c742: Switched translation API imports (`translationApiRef`, `appLanguageApiRef`) from the alpha `@backstage/core-plugin-api/alpha` path to the stable `@backstage/frontend-plugin-api` export. This has no effect on runtime behavior. +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-app-backend@0.5.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-app-node@0.1.43-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-app-node@0.1.43-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + +## @backstage/plugin-app-react@0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @backstage/plugin-app-visualizer@0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @backstage/plugin-auth@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/errors@1.2.7 + - @backstage/theme@0.7.2 + +## @backstage/plugin-auth-backend@0.27.1-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- 619be54: Update migrations to be reversible +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.4.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-auth0-provider@0.3.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.4.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.27.1-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-bitbucket-server-provider@0.2.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.4.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.4.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-github-provider@0.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-google-provider@0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-guest-provider@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.4.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.4.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.27.1-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-okta-provider@0.2.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-openshift-provider@0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.3.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.5.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-auth-node@0.6.14-next.0 + +## @backstage/plugin-auth-node@0.6.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-auth-react@0.1.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + +## @backstage/plugin-bitbucket-cloud-common@0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + +## @backstage/plugin-catalog@1.33.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-catalog-backend-module-aws@0.4.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-kubernetes-common@0.9.10 + +## @backstage/plugin-catalog-backend-module-azure@0.3.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-bitbucket-cloud-common@0.3.8-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-gcp@0.3.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-kubernetes-common@0.9.10 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-github@0.12.3-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-github@0.12.3-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.8.1-next.0 + +### Patch Changes + +- d933f62: Add configurable throttling and retry mechanism for GitLab integration. +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-gitlab@0.8.1-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/plugin-catalog-backend@3.5.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-catalog-backend-module-ldap@0.12.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-logs@0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.5.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-msgraph@0.9.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.6.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.13 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-catalog-graph@0.5.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-catalog-import@0.13.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-catalog-react@2.0.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/frontend-test-utils@0.5.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.13 + - @backstage/plugin-devtools-react@0.1.2-next.0 + +## @backstage/plugin-config-schema@0.1.78-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-devtools@0.1.37-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/plugin-devtools-common@0.1.22 + - @backstage/plugin-devtools-react@0.1.2-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-devtools-backend@0.5.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-devtools-common@0.1.22 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-devtools-react@0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @backstage/plugin-events-backend@0.5.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-aws-sqs@0.4.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-azure@0.2.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-bitbucket-server@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-gerrit@0.2.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-github@0.4.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-gitlab@0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-google-pubsub@0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-module-kafka@0.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-backend-test-utils@0.1.53-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-events-node@0.4.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-gateway-backend@1.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + +## @backstage/plugin-home@0.9.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-home-react@0.1.36-next.0 + +## @backstage/plugin-home-react@0.1.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @backstage/plugin-kubernetes@0.12.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-react@0.5.17-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-kubernetes-backend@0.21.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-node@0.4.2-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-kubernetes-cluster@0.0.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-react@0.5.17-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-kubernetes-node@0.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-kubernetes-common@0.9.10 + +## @backstage/plugin-kubernetes-react@0.5.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-kubernetes-common@0.9.10 + +## @backstage/plugin-mcp-actions-backend@0.1.10-next.0 + +### Patch Changes + +- dc81af1: Adds two new metrics to track MCP server operations and sessions. + + - `mcp.server.operation.duration`: The duration taken to process an individual MCP operation + - `mcp.server.session.duration`: The duration of the MCP session from the perspective of the server + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-mui-to-bui@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + +## @backstage/plugin-notifications@0.5.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/theme@0.7.2 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-signals-react@0.0.20-next.0 + +## @backstage/plugin-notifications-backend@0.6.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.24-next.0 + - @backstage/plugin-signals-node@0.1.29-next.0 + +## @backstage/plugin-notifications-backend-module-email@0.3.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.24-next.0 + +## @backstage/plugin-notifications-node@0.2.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-signals-node@0.1.29-next.0 + +## @backstage/plugin-org@0.6.50-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-org-react@0.1.48-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @backstage/plugin-permission-backend@0.7.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-permission-node@0.10.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-permission-react@0.4.41-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-proxy-backend@0.6.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-proxy-node@0.1.13-next.0 + +## @backstage/plugin-proxy-node@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + +## @backstage/plugin-scaffolder@1.35.5-next.0 + +### Patch Changes + +- bd5b842: Added a new `ui:autoSelect` option to the EntityPicker field that controls whether an entity is automatically selected when the field loses focus. When set to `false`, the field will remain empty if the user closes it without explicitly selecting an entity, preventing unintentional selections. Defaults to `true` for backward compatibility. +- ee87720: Added back the `formFieldsApiRef` and `ScaffolderFormFieldsApi` alpha exports that were unintentionally removed. +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + - @backstage/plugin-scaffolder-react@1.19.8-next.0 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-scaffolder-backend@3.1.4-next.0 + +### Patch Changes + +- 4e39e63: Removed unused dependencies +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-azure@0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.4-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.19-next.0 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-bitbucket-cloud-common@0.3.8-next.0 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.3.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.3.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-github@0.9.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.11.4-next.0 + +### Patch Changes + +- 5730c8e: Added `maskedAndHidden` option to `gitlab:projectVariable:create` and `publish:gitlab` action to support creating GitLab project variables that are both masked and hidden. Updated gitbeaker to version 43.8.0 for proper type support. +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-notifications-node@0.2.24-next.0 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-rails@0.5.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.4.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + - @backstage/plugin-scaffolder-node-test-utils@0.3.9-next.0 + +## @backstage/plugin-scaffolder-common@1.7.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-scaffolder-node@0.12.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + +## @backstage/plugin-scaffolder-node-test-utils@0.3.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-test-utils@1.11.1-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + +## @backstage/plugin-scaffolder-react@1.19.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-scaffolder-common@1.7.7-next.0 + +## @backstage/plugin-search@1.6.2-next.0 + +### Patch Changes + +- d5eb954: Fixes the search component not registering the first search on navigate to the search page. +- Updated dependencies + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend@2.0.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend-module-catalog@0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend-module-elasticsearch@1.8.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.20 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend-module-explore@0.3.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend-module-pg@0.5.53-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.3.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend-module-techdocs@0.4.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-techdocs-node@1.14.3-next.0 + +## @backstage/plugin-search-backend-node@1.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-react@1.10.5-next.0 + +### Patch Changes + +- d5eb954: Fixes the search component not registering the first search on navigate to the search page. +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-signals@0.0.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/plugin-signals-react@0.0.20-next.0 + +## @backstage/plugin-signals-backend@0.3.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-signals-node@0.1.29-next.0 + +## @backstage/plugin-signals-node@0.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-signals-react@0.0.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-techdocs@1.17.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-techdocs-addons-test-utils@2.0.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/plugin-techdocs@1.17.1-next.0 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-techdocs-backend@2.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/plugin-catalog-node@2.1.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-client@1.13.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-techdocs-node@1.14.3-next.0 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-techdocs-node@1.14.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-techdocs-common@0.1.1 + +## @backstage/plugin-techdocs-react@1.3.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-techdocs-common@0.1.1 + +## @backstage/plugin-user-settings@0.9.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/plugin-signals-react@0.0.20-next.0 + - @backstage/plugin-user-settings-common@0.1.0 + +## @backstage/plugin-user-settings-backend@0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-signals-node@0.1.29-next.0 + - @backstage/plugin-user-settings-common@0.1.0 + +## example-app@0.0.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-search@1.6.2-next.0 + - @backstage/plugin-api-docs@0.13.5-next.0 + - @backstage/cli@0.35.5-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-scaffolder@1.35.5-next.0 + - @backstage/plugin-app@0.4.1-next.0 + - @backstage/plugin-app-visualizer@0.2.1-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/plugin-techdocs@1.17.1-next.0 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-auth@0.1.6-next.0 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-catalog-graph@0.5.8-next.0 + - @backstage/plugin-catalog-import@0.13.11-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + - @backstage/plugin-devtools@0.1.37-next.0 + - @backstage/plugin-home@0.9.3-next.0 + - @backstage/plugin-home-react@0.1.36-next.0 + - @backstage/plugin-kubernetes@0.12.17-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.35-next.0 + - @backstage/plugin-notifications@0.5.15-next.0 + - @backstage/plugin-org@0.6.50-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-scaffolder-react@1.19.8-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-signals@0.0.29-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + - @backstage/plugin-user-settings@0.9.1-next.0 + +## app-example-plugin@0.0.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + +## example-app-legacy@0.2.119-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-search@1.6.2-next.0 + - @backstage/plugin-api-docs@0.13.5-next.0 + - @backstage/cli@0.35.5-next.0 + - @backstage/plugin-scaffolder@1.35.5-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/plugin-mui-to-bui@0.2.5-next.0 + - @backstage/plugin-techdocs@1.17.1-next.0 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-catalog-graph@0.5.8-next.0 + - @backstage/plugin-catalog-import@0.13.11-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + - @backstage/plugin-devtools@0.1.37-next.0 + - @backstage/plugin-home@0.9.3-next.0 + - @backstage/plugin-home-react@0.1.36-next.0 + - @backstage/plugin-kubernetes@0.12.17-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.35-next.0 + - @backstage/plugin-notifications@0.5.15-next.0 + - @backstage/plugin-org@0.6.50-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-scaffolder-react@1.19.8-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-signals@0.0.29-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + - @backstage/plugin-user-settings@0.9.1-next.0 + +## example-backend@0.0.48-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/plugin-auth-backend@0.27.1-next.0 + - @backstage/plugin-catalog-backend@3.5.0-next.0 + - @backstage/plugin-scaffolder-backend@3.1.4-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-mcp-actions-backend@0.1.10-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-app-backend@0.5.12-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.5.1-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.17-next.0 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.5-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.12-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.2.20-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.18-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.9-next.0 + - @backstage/plugin-devtools-backend@0.5.15-next.0 + - @backstage/plugin-events-backend@0.5.12-next.0 + - @backstage/plugin-events-backend-module-google-pubsub@0.2.1-next.0 + - @backstage/plugin-kubernetes-backend@0.21.2-next.0 + - @backstage/plugin-notifications-backend@0.6.3-next.0 + - @backstage/plugin-permission-backend@0.7.10-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.17-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-proxy-backend@0.6.11-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.7-next.0 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.20-next.0 + - @backstage/plugin-search-backend@2.0.13-next.0 + - @backstage/plugin-search-backend-module-catalog@0.3.13-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.8.1-next.0 + - @backstage/plugin-search-backend-module-explore@0.3.12-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.4.12-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-signals-backend@0.3.13-next.0 + - @backstage/plugin-techdocs-backend@2.1.6-next.0 + +## e2e-test@0.2.38-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/create-app@0.7.10-next.0 + - @backstage/errors@1.2.7 + +## @internal/frontend@0.0.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + +## @internal/scaffolder@0.0.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-scaffolder-react@1.19.8-next.0 + +## techdocs-cli-embedded-app@0.2.118-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/cli@0.35.5-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-techdocs@1.17.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## yarn-plugin-backstage@0.0.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/errors@1.2.7 + - @backstage/release-manifests@0.0.13 + +## @internal/plugin-todo-list@1.0.49-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @internal/plugin-todo-list-backend@1.0.48-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 diff --git a/docs/releases/v1.49.0-next.1-changelog.md b/docs/releases/v1.49.0-next.1-changelog.md new file mode 100644 index 0000000000..7110b3d0d5 --- /dev/null +++ b/docs/releases/v1.49.0-next.1-changelog.md @@ -0,0 +1,1567 @@ +# Release v1.49.0-next.1 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.49.0-next.1](https://backstage.github.io/upgrade-helper/?to=1.49.0-next.1) + +## @backstage/integration@2.0.0-next.1 + +### Major Changes + +- 527cf88: **BREAKING** Removed deprecated Azure DevOps, Bitbucket, Gerrit and GitHub code: + + - For Azure DevOps, the long deprecated `token` string and `credential` object have been removed from the `config.d.ts`. Use the `credentials` array object instead. + - For Bitbucket, the long deprecated `bitbucket` object has been removed from the `config.d.ts`. Use the `bitbucketCloud` or `bitbucketServer` objects instead. + - For Gerrit, the `parseGerritGitilesUrl` function has been removed, use `parseGitilesUrlRef` instead. The `buildGerritGitilesArchiveUrl` function has also been removed, use `buildGerritGitilesArchiveUrlFromLocation` instead. + - For GitHub, the `getGitHubRequestOptions` function has been removed. + +### Patch Changes + +- 993a598: Fixed Azure integration config schema visibility annotations to use per-field `@visibility secret` instead of `@deepVisibility secret` on parent objects, so that non-secret fields like `clientId`, `tenantId`, `organizations`, and `managedIdentityClientId` are no longer incorrectly marked as secret. +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-common@2.0.0-next.1 + +### Major Changes + +- 527cf88: **BREAKING** Removed deprecated `bitbucket` integration from being registered in the `ScaffolderClient`. Use the `bitbucketCloud` or `bitbucketServer` integrations instead. + +### Minor Changes + +- f598909: **BREAKING PRODUCERS**: Made `retry`, `listTasks`, `listTemplatingExtensions`, `dryRun`, and `autocomplete` required methods on the `ScaffolderApi` interface. Implementations of `ScaffolderApi` must now provide these methods. + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/backend-defaults@0.16.0-next.1 + +### Minor Changes + +- 0e7d8f9: The scheduler service now uses the metrics service to create metrics, providing plugin-scoped attribution. +- 527cf88: **BREAKING** Removed deprecated `BitbucketUrlReader`. Use the `BitbucketCloudUrlReader` or the `BitbucketServerUrlReader` instead. + +### Patch Changes + +- 62f0a53: Fixed error forwarding in the actions registry so that known errors like `InputError` and `NotFoundError` thrown by actions preserve their original status codes and messages instead of being wrapped in `ForwardedError` and coerced to 500. +- Updated dependencies + - @backstage/cli-node@0.2.19-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/backend-app-api@1.5.1-next.0 + - @backstage/backend-dev-utils@0.1.7 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/backend-dynamic-feature-service@0.8.0-next.1 + +### Minor Changes + +- 0fbcf23: Migrated OpenAPI schemas to 3.1. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.5.0-next.1 + - @backstage/cli-common@0.2.0-next.1 + - @backstage/cli-node@0.2.19-next.1 + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/plugin-events-backend@0.6.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-app-node@0.1.43-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/catalog-client@1.14.0-next.1 + +### Minor Changes + +- 972f686: Added support for the `query` field in `getEntitiesByRefs` requests, enabling predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. +- 56c908e: Added support for the `query` field in `getEntityFacets` requests, enabling predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. +- 0fbcf23: Migrated OpenAPI schemas to 3.1. +- 51e23eb: Added predicate-based entity filtering via POST /entities/by-query endpoint. + + Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$hasPrefix`, and (partially) `$contains` operators for expressive entity queries. Integrated into the existing `queryEntities` flow with full cursor-based pagination, permission enforcement, and `totalItems` support. + + The catalog client's `queryEntities()` method automatically routes to the POST endpoint when a `query` predicate is provided. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + +## @backstage/cli@0.36.0-next.1 + +### Minor Changes + +- b36a60d: **BREAKING**: The `migrate package-exports` command has been removed. Use `repo fix` instead. + +### Patch Changes + +- 0d2d0f2: Internal refactor of CLI modularization, moving individual commands to be implemented with cleye. +- 2fcba39: Internal refactor to move shared utilities into their consuming modules, reducing cross-module dependencies. +- c85ac86: Internal refactor to split `loadCliConfig` into separate implementations for the build and config CLI modules, removing a cross-module dependency. +- 61cb976: Migrated internal versioning utilities to use `@backstage/cli-node` instead of a local implementation. +- 825c81d: Internal refactor of CLI command modules. +- a9d23c4: Properly support `package.json` `workspaces` field +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + - @backstage/cli-node@0.2.19-next.1 + - @backstage/module-federation-common@0.1.2-next.0 + - @backstage/integration@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/errors@1.2.7 + - @backstage/eslint-plugin@0.2.2-next.0 + - @backstage/release-manifests@0.0.13 + - @backstage/types@1.2.2 + +## @backstage/repo-tools@0.17.0-next.1 + +### Minor Changes + +- 0fbcf23: Added support for OpenAPI 3.1 to all `schema openapi` commands. The commands now auto-detect the OpenAPI version from the spec file and use the appropriate generator, supporting both OpenAPI 3.0.x and 3.1.x specifications. + +### Patch Changes + +- 426edbe: Fixed `generate-catalog-info` command failing with "too many arguments" when invoked by lint-staged via the pre-commit hook. +- d5779e5: Updated the CLI report parser to support cleye-style help output, and strip ANSI escape codes from captured output. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + - @backstage/cli-node@0.2.19-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/errors@1.2.7 + +## @backstage/ui@0.13.0-next.1 + +### Minor Changes + +- 768f09d: **BREAKING**: Simplified the neutral background prop API for container components. The explicit `neutral-1`, `neutral-2`, `neutral-3`, and `neutral-auto` values have been removed from `ProviderBg`. They are replaced by a single `'neutral'` value that always auto-increments from the parent context, making it impossible to skip or pin to an explicit neutral level. + + **Migration:** + + Replace any explicit `bg="neutral-1"`, `bg="neutral-2"`, `bg="neutral-3"`, or `bg="neutral-auto"` props with `bg="neutral"`. To achieve a specific neutral level in stories or tests, use nested containers — each additional `bg="neutral"` wrapper increments by one level. + + ```tsx + // Before + ... + + // After + + ... + + ``` + + **Affected components:** Box, Flex, Grid, Card, Accordion, Popover, Tooltip, Dialog, Menu + +- b42fcdc: **BREAKING**: Removed `--bui-bg-popover` CSS token. Popover, Tooltip, Menu, and Dialog now use `--bui-bg-app` for their outer shell and `Box bg="neutral-1"` for content areas, providing better theme consistency and eliminating a redundant token. + + **Migration:** + + Replace any usage of `--bui-bg-popover` with `--bui-bg-neutral-1` (for content surfaces) or `--bui-bg-app` (for outer shells): + + ```diff + - background: var(--bui-bg-popover); + + background: var(--bui-bg-neutral-1); + ``` + + **Affected components:** Popover, Tooltip, Menu, Dialog + +- bd3a76e: **BREAKING**: Data attributes rendered by components are now always lowercase. This affects CSS selectors targeting camelCase data attributes. + + **Migration:** + + Update any custom CSS selectors that target camelCase data attributes to use lowercase instead: + + ```diff + - [data-startCollapsed='true'] { ... } + + [data-startcollapsed='true'] { ... } + ``` + + **Affected components:** SearchField + +- 95702ab: **BREAKING**: Removed deprecated types `ComponentDefinition`, `ClassNamesMap`, `DataAttributeValues`, and `DataAttributesMap` from the public API. These were internal styling infrastructure types that have been replaced by the `defineComponent` system. + + **Migration:** + + Remove any direct usage of these types. Component definitions now use `defineComponent()` and their shapes are not part of the public API contract. + + ```diff + - import type { ComponentDefinition, ClassNamesMap } from '@backstage/ui'; + ``` + + If you were reading `definition.dataAttributes`, use `definition.propDefs` instead — props with `dataAttribute: true` in `propDefs` are the equivalent. + +### Patch Changes + +- 58224d3: Fixed neutral-1 hover & pressed state in light mode. + +- 95702ab: Migrated all components from `useStyles` to `useDefinition` hook. Exported `OwnProps` types for each component, enabling better type composition for consumers. + + **Affected components:** Avatar, Checkbox, Container, Dialog, FieldError, FieldLabel, Flex, FullPage, Grid, HeaderPage, Link, Menu, PasswordField, PluginHeader, Popover, RadioGroup, SearchField, Select, Skeleton, Switch, Table, TablePagination, Tabs, TagGroup, Text, TextField, ToggleButton, ToggleButtonGroup, Tooltip, VisuallyHidden + +- 4c2c350: Removed the `transition` on `Container` padding to prevent an unwanted animation when the viewport is resized. + + Affected components: Container + +- d4fa5b4: Fixed tab `matchStrategy` matching to ignore query parameters and hash fragments in tab `href` values. Previously, tabs with query params in their `href` (e.g., `/page?group=foo`) would never show as active since matching compared the full `href` string against `location.pathname` which never includes query params. + + **Affected components:** Tabs, PluginHeader + +- 36987db: Fixed handling of the `style` prop on `Button`, `ButtonIcon`, and `ButtonLink` so that it is now correctly forwarded to the underlying element instead of being silently dropped. + + **Affected components:** Button, ButtonIcon, ButtonLink + +- 95702ab: Fixed Link variant default from `'body'` to `'body-medium'` to match actual CSS selectors. The previous default did not correspond to a valid variant value. + + **Affected components:** Link + +- 9027b10: Fixed scroll overflow in Menu and Select popover content when constrained by viewport height. + + **Affected components:** Menu, Select + +- 4105a78: Merged the internal `PluginHeaderToolbar` component into `PluginHeader`, removing the separate component and its associated types (`PluginHeaderToolbarOwnProps`, `PluginHeaderToolbarProps`) and definition (`PluginHeaderToolbarDefinition`). This is an internal refactor with no changes to the public API of `PluginHeader`. + + **Affected components:** PluginHeader + +- b303857: Fixed `isRequired` prop not being passed to the underlying React Aria field components in TextField, SearchField, and PasswordField. Previously, `isRequired` was consumed locally for the secondary label text but never forwarded, which meant the input elements lacked `aria-required="true"` and React Aria's built-in required validation was not activated. + + **Affected components:** TextField, SearchField, PasswordField + +- cd3cb0f: Improved `useBreakpoint` performance by sharing a single set of `matchMedia` listeners across all component instances instead of creating independent listeners per hook call. + +- 36987db: Extended `AlertProps`, `ContainerProps`, `DialogBodyProps`, and `FieldLabelProps` with native div element props to allow passing attributes like `aria-*` and `data-*`. + + **Affected components:** Alert, Container, DialogBody, FieldLabel + +- Updated dependencies + - @backstage/version-bridge@1.0.12 + +## @backstage/plugin-catalog@1.34.0-next.1 + +### Minor Changes + +- 4d58894: Added support for group alias IDs and configurable content ordering on the entity page. Groups can now declare `aliases` so that content targeting an aliased group is included in the group. A new `defaultContentOrder` option (default `title`) controls how content items within each group are sorted, with support for both a page-level default and per-group overrides. + +### Patch Changes + +- 07ba746: Fixed entity page tab groups not respecting the ordering from the `groups` configuration. +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-compat-api@0.5.9-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-catalog-backend@3.5.0-next.1 + +### Minor Changes + +- a6b2819: Added `query-catalog-entities` action to the catalog backend actions registry. Supports predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. +- 972f686: Added support for predicate-based filtering on the `/entities/by-refs` endpoint via the `query` field in the request body. Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. +- 56c908e: Added support for predicate-based filtering on the `/entity-facets` endpoint via a new `POST` method. Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. +- 0fbcf23: Migrated OpenAPI schemas to 3.1. +- 51e23eb: Added predicate-based entity filtering via POST /entities/by-query endpoint. + + Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$hasPrefix`, and (partially) `$contains` operators for expressive entity queries. Integrated into the existing `queryEntities` flow with full cursor-based pagination, permission enforcement, and `totalItems` support. + + The catalog client's `queryEntities()` method automatically routes to the POST endpoint when a `query` predicate is provided. + +### Patch Changes + +- 72747b4: Deprecated two processors as they have been moved to the Community Plugins repo with their own backend modules: + + - `AnnotateScmSlugEntityProcessor`: Use `@backstage-community/plugin-catalog-backend-module-annotate-scm-slug` instead + - `CodeOwnersProcessor`: Use `@backstage-community/plugin-catalog-backend-module-codeowners` instead + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-catalog-backend-module-github@0.13.0-next.1 + +### Minor Changes + +- b11c2cd: The default user transformer now prefers organization verified domain emails over the user's public GitHub email when populating the user entity profile. It also strips plus-addressed routing tags that GitHub adds to these emails. + + If you want to retain the old behavior, you can do so with a custom user transformer using the `githubOrgEntityProviderTransformsExtensionPoint`: + + ```ts + import { createBackendModule } from '@backstage/backend-plugin-api'; + import { githubOrgEntityProviderTransformsExtensionPoint } from '@backstage/plugin-catalog-backend-module-github-org'; + import { defaultUserTransformer } from '@backstage/plugin-catalog-backend-module-github'; + + export default createBackendModule({ + pluginId: 'catalog', + moduleId: 'github-org-custom-transforms', + register(env) { + env.registerInit({ + deps: { + transforms: githubOrgEntityProviderTransformsExtensionPoint, + }, + async init({ transforms }) { + transforms.setUserTransformer(async (item, ctx) => { + const entity = await defaultUserTransformer(item, ctx); + if (entity && item.email) { + entity.spec.profile!.email = item.email; + } + return entity; + }); + }, + }); + }, + }); + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-react@2.1.0-next.1 + +### Minor Changes + +- 4d58894: Added `aliases` and `contentOrder` fields to `EntityContentGroupDefinition`, allowing groups to declare alias IDs and control the sort order of their content items. + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/frontend-test-utils@0.5.1-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-compat-api@0.5.9-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-events-backend@0.6.0-next.1 + +### Minor Changes + +- 0fbcf23: Migrated OpenAPI schemas to 3.1. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-scaffolder-backend@3.2.0-next.1 + +### Minor Changes + +- c9b11eb: Added a new `list-scaffolder-tasks` action that allows querying scaffolder tasks with optional ownership filtering and pagination support +- 0fbcf23: Migrated OpenAPI schemas to 3.1. +- 7695dd2: Added a new `list-scaffolder-actions` action that returns all installed scaffolder actions with their schemas and examples + +### Patch Changes + +- e27bd4e: Removed `@backstage/plugin-scaffolder-backend-module-bitbucket` from `package.json` as the package itself has been deprecated and the code deleted. +- ccc20cf: create scaffolder MCP action to dry run a provided scaffolder template +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-scaffolder-node@0.13.0-next.1 + +### Minor Changes + +- e27bd4e: **BREAKING** Removed deprecated `bitbucket` integration from being used in the `parseRepoUrl` function. It will use the `bitbucketCloud` or `bitbucketServer` integrations instead. + +### Patch Changes + +- f598909: Added `scaffolderServiceRef` and `ScaffolderService` interface for backend plugins that need to interact with the scaffolder API using `BackstageCredentials` instead of raw tokens. +- Updated dependencies + - @backstage/backend-test-utils@1.11.1-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-search-backend@2.1.0-next.1 + +### Minor Changes + +- 0fbcf23: Migrated OpenAPI schemas to 3.1. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/backend-test-utils@1.11.1-next.1 + +### Patch Changes + +- 62f0a53: Fixed error forwarding in the actions registry so that known errors like `InputError` and `NotFoundError` thrown by actions preserve their original status codes and messages instead of being wrapped in `ForwardedError` and coerced to 500. +- Updated dependencies + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/backend-app-api@1.5.1-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/cli-common@0.2.0-next.1 + +### Patch Changes + +- e44b6a9: The `findOwnRootDir` utility now searches for the monorepo root by traversing up the directory tree looking for a `package.json` with `workspaces`, instead of assuming a fixed `../..` relative path. If no workspaces root is found during this traversal, `findOwnRootDir` now throws to enforce stricter validation of the repository layout. +- Updated dependencies + - @backstage/errors@1.2.7 + +## @backstage/cli-node@0.2.19-next.1 + +### Patch Changes + +- 61cb976: Added `toString()` method to `Lockfile` for serializing lockfiles back to string format. +- 3c811bf: Added `hasBackstageYarnPlugin` and `SuccessCache` exports, moved from `@backstage/cli`. +- a9d23c4: Properly support `package.json` `workspaces` field +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/core-compat-api@0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + +## @backstage/create-app@0.7.10-next.1 + +### Patch Changes + +- a9d23c4: Properly support `package.json` `workspaces` field + +- ebd4630: Replace deprecated `workspaces.packages` with `workspaces` in `package.json` + + This change is **not** required, but you can edit your main `package.json`, to fit the more modern & more common pattern: + + ```diff + - "workspaces": { + - "packages": [ + "workspaces": [ + "packages/*", + "plugins/*" + - ] + - }, + ], + ``` + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + +## @backstage/dev-utils@1.1.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + +## @backstage/frontend-dynamic-feature-loader@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/module-federation-common@0.1.2-next.0 + - @backstage/config@1.3.6 + - @backstage/frontend-plugin-api@0.14.2-next.0 + +## @backstage/frontend-test-utils@0.5.1-next.1 + +### Patch Changes + +- 479282f: Fixed type inference of `TestApiPair` when using tuple syntax by wrapping `MockWithApiFactory` in `NoInfer`. +- Updated dependencies + - @backstage/plugin-app@0.4.1-next.1 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/integration-react@1.2.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @backstage/module-federation-common@0.1.2-next.0 + +### Patch Changes + +- 0cb5646: Fixed the `@mui/material/styles` shared dependency key by removing a trailing slash that caused module resolution failures with MUI package exports. +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @techdocs/cli@1.10.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + - @backstage/plugin-techdocs-node@1.14.3-next.1 + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + +## @backstage/plugin-api-docs@0.13.5-next.1 + +### Patch Changes + +- 30e08df: Added default entity content groups for the API docs entity content tabs. The API definition tab defaults to the `documentation` group and the APIs tab defaults to the `development` group. +- Updated dependencies + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-app@0.4.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-app-visualizer@0.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + +## @backstage/plugin-auth-backend@0.27.1-next.1 + +### Patch Changes + +- 1ccad86: Added `who-am-i` action to the auth backend actions registry. Returns the catalog entity and user info for the currently authenticated user. +- Updated dependencies + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-auth-node@0.6.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-bitbucket-cloud-common@0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + +## @backstage/plugin-catalog-backend-module-aws@0.4.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-kubernetes-common@0.9.10 + +## @backstage/plugin-catalog-backend-module-azure@0.3.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-bitbucket-cloud-common@0.3.8-next.1 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-github@0.13.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.8.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.5.0-next.1 + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-catalog-graph@0.5.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-catalog-import@0.13.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-catalog-node@2.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/backend-test-utils@1.11.1-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-devtools@0.1.37-next.1 + +### Patch Changes + +- afabb37: Fixed URL encoding of task IDs for the trigger feature (tasks that contained a "/" in their ID were not triggered) +- Updated dependencies + - @backstage/core-compat-api@0.5.9-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-devtools-common@0.1.22 + - @backstage/plugin-devtools-react@0.1.2-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-events-backend-module-github@0.4.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + +## @backstage/plugin-home@0.9.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-compat-api@0.5.9-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-home-react@0.1.36-next.0 + +## @backstage/plugin-kubernetes@0.12.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-react@0.5.17-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-kubernetes-backend@0.21.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-node@0.4.2-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## @backstage/plugin-kubernetes-cluster@0.0.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/plugin-kubernetes-common@0.9.10 + - @backstage/plugin-kubernetes-react@0.5.17-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-mcp-actions-backend@0.1.10-next.1 + +### Patch Changes + +- 62f0a53: Fixed error forwarding in the actions registry so that known errors like `InputError` and `NotFoundError` thrown by actions preserve their original status codes and messages instead of being wrapped in `ForwardedError` and coerced to 500. +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-mui-to-bui@0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/theme@0.7.2 + +## @backstage/plugin-notifications-backend-module-email@0.3.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/plugin-notifications-node@0.2.24-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-common@0.2.1 + +## @backstage/plugin-notifications-node@0.2.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-notifications-common@0.2.1 + - @backstage/plugin-signals-node@0.1.29-next.0 + +## @backstage/plugin-org@0.6.50-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-common@1.1.8 + +## @backstage/plugin-org-react@0.1.48-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + +## @backstage/plugin-scaffolder@1.35.5-next.1 + +### Patch Changes + +- e27bd4e: Removed check for deprecated `bitbucket` integration from `repoPickerValidation` function used by the `RepoUrlPicker`, it now validates the `bitbucketServer` and `bitbucketCloud` integrations instead. +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/plugin-scaffolder-react@1.19.8-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/types@1.2.2 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-azure@0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.3.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-bitbucket-cloud-common@0.3.8-next.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.3.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.3.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-github@0.9.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.11.4-next.1 + +### Patch Changes + +- 0c1726a: Added new `gitlab:group:access` scaffolder action to add or remove users and groups as members of GitLab groups. The action supports specifying members via `userIds` and/or `groupIds` array parameters, configurable access levels (Guest, Reporter, Developer, Maintainer, Owner), and defaults to the 'add' action when not specified. +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.1.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/plugin-notifications-node@0.2.24-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-notifications-common@0.2.1 + +## @backstage/plugin-scaffolder-backend-module-rails@0.5.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.3.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.4.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-node-test-utils@0.3.9-next.1 + +## @backstage/plugin-scaffolder-node-test-utils@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@1.11.1-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-scaffolder-react@1.19.8-next.1 + +### Patch Changes + +- 004b5c1: Added back `formFieldsApiRef` and `ScaffolderFormFieldsApi` as alpha exports. +- f598909: Added `scaffolderApiMock` test utility, exported from `@backstage/plugin-scaffolder-react/testUtils`. +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/frontend-test-utils@0.5.1-next.1 + - @backstage/plugin-scaffolder-common@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## @backstage/plugin-search@1.6.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-search-react@1.10.5-next.0 + +## @backstage/plugin-search-backend-module-catalog@0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-search-backend-module-techdocs@0.4.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-techdocs-node@1.14.3-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## @backstage/plugin-techdocs@1.17.1-next.1 + +### Patch Changes + +- 30e08df: Added `documentation` as the default entity content group for the TechDocs entity content tab. +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-techdocs-common@0.1.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-techdocs-addons-test-utils@2.0.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.17.1-next.1 + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-techdocs-backend@2.1.6-next.1 + +### Patch Changes + +- cb7c6b1: Added `techdocs.generator.mkdocs.dangerouslyAllowAdditionalKeys` configuration option to explicitly bypass MkDocs configuration key restrictions. This enables support for additional MkDocs configuration keys beyond the default safe allow list, such as the `hooks` key which some MkDocs plugins require. +- Updated dependencies + - @backstage/catalog-client@1.14.0-next.1 + - @backstage/plugin-techdocs-node@1.14.3-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-catalog-node@2.1.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## @backstage/plugin-techdocs-node@1.14.3-next.1 + +### Patch Changes + +- cb7c6b1: Added `techdocs.generator.mkdocs.dangerouslyAllowAdditionalKeys` configuration option to explicitly bypass MkDocs configuration key restrictions. This enables support for additional MkDocs configuration keys beyond the default safe allow list, such as the `hooks` key which some MkDocs plugins require. +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-techdocs-common@0.1.1 + +## @backstage/plugin-user-settings@0.9.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/plugin-signals-react@0.0.20-next.0 + - @backstage/plugin-user-settings-common@0.1.0 + +## example-app@0.0.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/cli@0.36.0-next.1 + - @backstage/plugin-devtools@0.1.37-next.1 + - @backstage/plugin-scaffolder@1.35.5-next.1 + - @backstage/plugin-api-docs@0.13.5-next.1 + - @backstage/plugin-techdocs@1.17.1-next.1 + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/plugin-scaffolder-react@1.19.8-next.1 + - @backstage/plugin-app@0.4.1-next.1 + - @backstage/plugin-app-visualizer@0.2.1-next.1 + - @backstage/plugin-catalog-graph@0.5.8-next.1 + - @backstage/plugin-catalog-import@0.13.11-next.1 + - @backstage/plugin-home@0.9.3-next.1 + - @backstage/plugin-org@0.6.50-next.1 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-compat-api@0.5.9-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-auth@0.1.6-next.0 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + - @backstage/plugin-home-react@0.1.36-next.0 + - @backstage/plugin-kubernetes@0.12.17-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.35-next.1 + - @backstage/plugin-notifications@0.5.15-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-search@1.6.2-next.1 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-signals@0.0.29-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + - @backstage/plugin-user-settings@0.9.1-next.1 + +## example-app-legacy@0.2.119-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/cli@0.36.0-next.1 + - @backstage/plugin-devtools@0.1.37-next.1 + - @backstage/plugin-scaffolder@1.35.5-next.1 + - @backstage/plugin-api-docs@0.13.5-next.1 + - @backstage/plugin-techdocs@1.17.1-next.1 + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/plugin-scaffolder-react@1.19.8-next.1 + - @backstage/plugin-mui-to-bui@0.2.5-next.1 + - @backstage/plugin-catalog-graph@0.5.8-next.1 + - @backstage/plugin-catalog-import@0.13.11-next.1 + - @backstage/plugin-home@0.9.3-next.1 + - @backstage/plugin-org@0.6.50-next.1 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + - @backstage/plugin-home-react@0.1.36-next.0 + - @backstage/plugin-kubernetes@0.12.17-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.35-next.1 + - @backstage/plugin-notifications@0.5.15-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-search@1.6.2-next.1 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-signals@0.0.29-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + - @backstage/plugin-user-settings@0.9.1-next.1 + +## example-backend@0.0.48-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.5.0-next.1 + - @backstage/plugin-auth-backend@0.27.1-next.1 + - @backstage/plugin-techdocs-backend@2.1.6-next.1 + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/plugin-scaffolder-backend@3.2.0-next.1 + - @backstage/plugin-mcp-actions-backend@0.1.10-next.1 + - @backstage/plugin-events-backend@0.6.0-next.1 + - @backstage/plugin-search-backend@2.1.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/plugin-kubernetes-backend@0.21.2-next.1 + - @backstage/plugin-search-backend-module-catalog@0.3.13-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.4.12-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-app-backend@0.5.12-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.5.1-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.17-next.0 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.5-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.12-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.2.20-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.18-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.9-next.0 + - @backstage/plugin-devtools-backend@0.5.15-next.0 + - @backstage/plugin-events-backend-module-google-pubsub@0.2.1-next.0 + - @backstage/plugin-notifications-backend@0.6.3-next.0 + - @backstage/plugin-permission-backend@0.7.10-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.17-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-proxy-backend@0.6.11-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.7-next.1 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.20-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.8.1-next.0 + - @backstage/plugin-search-backend-module-explore@0.3.12-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-signals-backend@0.3.13-next.0 + +## techdocs-cli-embedded-app@0.2.118-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/cli@0.36.0-next.1 + - @backstage/plugin-techdocs@1.17.1-next.1 + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 diff --git a/microsite/blog/2026-02-25-get-a-jump-on-contribfest.mdx b/microsite/blog/2026-02-25-get-a-jump-on-contribfest.mdx new file mode 100644 index 0000000000..67941fa436 --- /dev/null +++ b/microsite/blog/2026-02-25-get-a-jump-on-contribfest.mdx @@ -0,0 +1,48 @@ +--- +title: 'Get a jump on ContribFest with the new web app' +author: Elaine de Mattos Silva Bezerra, DB Systel GmbH, Heikki Hellgren, OP Financial Group & André Wanlin, Spotify +--- + +![Get a jump on ContribFest with the new web app](assets/2026-02-25/backstage-contribfest-kubecon-web-app-header.png) + +Become a Contrib Champ and join us at ContribFest, where commits become legendary! + +We are once again hosting ContribFest at KubeCon + CloudNativeCon. This time around, it's taking place in Amsterdam on March 26, 2026, at 13:45 CET — make sure to [add it to your schedule](https://kccnceu2026.sched.com/event/2EF7v/contribfest-supercharge-your-open-source-impact-backstage-contribfest-live-andre-wanlin-emma-indal-spotify-heikki-hellgren-op-financial-group-elaine-bezerra-db-systel-gmbh?iframe=no). Learn more about what to expect below and get started now by exploring the new [ContribFest web app](https://contribfest.backstage.io/). + +{/* truncate */} + +## Introducing the ContribFest web app + +We're excited to announce the new ContribFest web app: [https://contribfest.backstage.io/](https://contribfest.backstage.io/). The app simplifies local setup and helps you quickly find good issues to work on from the curated list pre-selected by your ContribFest co-hosts. + +You'll see that the app is broken down into five sections: + +- [Welcome](https://contribfest.backstage.io/): This is where you'll find links to all the things, including the session's slide deck, assignment sheet, the Backstage and Community Plugins repositories, and their respective contribution guides. +- [Getting Started](https://contribfest.backstage.io/getting-started/): Whether you are new to Backstage or an old hat, use this handy checklist to help you get your local environment set up for contributing, including all the commands. (Make sure you check all the boxes, you never know what might happen! 😉) +- [Curated Issues](https://contribfest.backstage.io/issues/): This is what you come to the session for: finding an issue that speaks to you and contributing towards it. This section has a list of issues that we've curated — and filters, so you can slice and dice the list to find the perfect issue to work on. +- [Contrib Champs](https://contribfest.backstage.io/contrib-champs/): We've hosted three other ContribFests in the past — this is where you'll find merged PRs from those sessions, a place to celebrate contributions. Make sure to tag your PRs with “ContribFest”, and maybe your name will show up here one day, too! 🏆 +- [Hall of Hosts](https://contribfest.backstage.io/hall-of-hosts/): ContribFest would not take place without the various community members who have stepped up to help co-host the sessions. This is where you'll see an honor roll of past co-hosts. 🙏 + +## About those Contrib Champs + +The goals of the Backstage ContribFest sessions are many — foster community, work with experts, etc. — but it's pretty obvious that contributions are the most important. It's in the name after all. Here are a few past contributions that we wanted to share to give you an idea of what that looks like: + +- [#27694](https://github.com/backstage/backstage/pull/27694) by [hyb175](https://github.com/hyb175) — Add Pagination to Tech Docs Table: for those with lots of entities with TechDocs, this is a massive performance improvement. +- [#29470](https://github.com/backstage/backstage/pull/29470) by [ioboi](https://github.com/ioboi) — Openshift Auth provider: this allows those using OpenShift to use it to sign into their Backstage instance. +- [#31770](https://github.com/backstage/backstage/pull/31770) by [theZMC](https://github.com/theZMC) — Render HTML in GitHub-flavored Markdown: with this change in place, HTML will now render correctly in the MarkdownContent component when you are using the GitHub-flavored Markdown mode. + +Check out the [Contrib Champs page](https://contribfest.backstage.io/contrib-champs/) to see the full list! + +## Using Dev Containers + +Along with the new ContribFest web app, we are also looking to use Dev Containers this time around to help streamline the session for those who'd like to use that option to get started. On the [Getting Started page](https://contribfest.backstage.io/getting-started/), pick the Dev Containers radio button and then follow the checklist. To give you a quick preview, you'll need to have the following installed: + +- Git, you'll need this to be able to pull down the code +- Docker Desktop (or Docker Engine on Linux) +- VS Code with the Dev Containers extension or IntelliJ IDEA Ultimate + +Check out our [Dev Containers tutorial](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/devcontainer.md) for a deeper dive into the subject. + +## Amsterdam, here we come! + +On behalf of the Backstage ContribFest co-host team, thank you for following along. We're looking forward to meeting you in Amsterdam and working together on your contributions. Please be sure to introduce yourself! diff --git a/microsite/blog/assets/2026-02-25/backstage-contribfest-kubecon-web-app-header.png b/microsite/blog/assets/2026-02-25/backstage-contribfest-kubecon-web-app-header.png new file mode 100644 index 0000000000..21e726aeb7 Binary files /dev/null and b/microsite/blog/assets/2026-02-25/backstage-contribfest-kubecon-web-app-header.png differ diff --git a/microsite/data/plugins/balajisiva-backstage-plugin-template-builder.yaml b/microsite/data/plugins/balajisiva-backstage-plugin-template-builder.yaml new file mode 100644 index 0000000000..6b8227c629 --- /dev/null +++ b/microsite/data/plugins/balajisiva-backstage-plugin-template-builder.yaml @@ -0,0 +1,11 @@ +--- +title: Template Builder +author: Balaji Sivasubramanian +authorUrl: https://github.com/balajisiva +category: Tooling +description: Visual editor for creating and managing Backstage scaffolder templates with live YAML preview, flow visualization, and GitHub integration +documentation: https://github.com/balajisiva/backstage-template-builder#readme +iconUrl: https://raw.githubusercontent.com/balajisiva/backstage-template-builder/main/plugins/backstage-template-builder/icon.svg +npmPackageName: '@balajisiva/backstage-plugin-template-builder' +addedDate: '2026-02-16' +status: active diff --git a/microsite/data/plugins/n8n.yaml b/microsite/data/plugins/n8n.yaml new file mode 100644 index 0000000000..7898c1efe4 --- /dev/null +++ b/microsite/data/plugins/n8n.yaml @@ -0,0 +1,11 @@ +--- +title: n8n +author: André Ahlert Junior +authorUrl: https://github.com/andreahlert +category: Other +description: n8n workflow automation — view workflows and execution history on Backstage entity pages. +documentation: https://github.com/backstage/community-plugins/tree/main/workspaces/n8n/plugins/n8n +iconUrl: /img/logo-gradient-on-dark.svg +npmPackageName: '@backstage-community/plugin-n8n' +addedDate: '2026-02-15' +status: active diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index edbbcbe10c..7e4e41591b 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -69,8 +69,16 @@ export default { ]), sidebarElementWithIndex({ label: 'Using Backstage' }, [ 'getting-started/logging-in', - 'getting-started/register-a-component', - 'getting-started/create-a-component', + 'getting-started/viewing-catalog', + 'getting-started/view-what-you-own', + 'getting-started/viewing-entity-relationships', + 'getting-started/filter-catalog', + sidebarElementWithIndex({ label: 'Managing Components' }, [ + 'getting-started/register-a-component', + 'getting-started/create-a-component', + 'getting-started/update-a-component', + 'getting-started/unregister-delete-component', + ]), ]), 'overview/support', 'getting-started/keeping-backstage-updated', @@ -521,6 +529,7 @@ export default { 'backend-system/core-services/identity', 'backend-system/core-services/lifecycle', 'backend-system/core-services/logger', + 'backend-system/core-services/metrics', 'backend-system/core-services/permissions', 'backend-system/core-services/permissions-registry', 'backend-system/core-services/plugin-metadata', diff --git a/mkdocs.yml b/mkdocs.yml index c18d74494a..618f77a59f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,6 +13,7 @@ plugins: nav: - Overview: - What is Backstage?: 'overview/what-is-backstage.md' + - Technical overview: 'overview/technical-overview.md' - Architecture overview: 'overview/architecture-overview.md' - Project Roadmap: 'overview/roadmap.md' - Vision: 'overview/vision.md' @@ -37,8 +38,15 @@ nav: - Kubernetes: 'deployment/k8s.md' - Using Backstage: - Logging in: 'getting-started/logging-in.md' - - Register a component: 'getting-started/register-a-component.md' - - Create a component: 'getting-started/create-a-component.md' + - Viewing the Catalog: 'getting-started/viewing-catalog.md' + - Viewing what you own: 'getting-started/view-what-you-own.md' + - Viewing entity relationships: 'getting-started/viewing-entity-relationships.md' + - Filtering the Catalog: 'getting-started/filter-catalog.md' + - Managing Components: + - Register a component: 'getting-started/register-a-component.md' + - Create a component: 'getting-started/create-a-component.md' + - Update a component: 'getting-started/update-a-component.md' + - Unregister/delete a component: 'getting-started/unregister-delete-component.md' - Keeping Backstage Updated: 'getting-started/keeping-backstage-updated.md' - Core Features: - Software Catalog: diff --git a/package.json b/package.json index a0a530adb8..ac79d5976c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.48.0", + "version": "1.49.0-next.1", "backstage": { "cli": { "new": { @@ -16,12 +16,10 @@ "type": "git", "url": "https://github.com/backstage/backstage" }, - "workspaces": { - "packages": [ - "packages/*", - "plugins/*" - ] - }, + "workspaces": [ + "packages/*", + "plugins/*" + ], "scripts": { "build-storybook": "storybook build --output-dir dist-storybook", "build-storybook:chromatic": "STORYBOOK_STORY_SET=chromatic storybook build --stats-json --output-dir dist-storybook", diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index a5516a33a9..e7bfdf0c89 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/app-defaults +## 1.7.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-permission-react@0.4.41-next.0 + ## 1.7.5 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 71b5da64d7..b35830dbd1 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/app-defaults", - "version": "1.7.5", + "version": "1.7.6-next.0", "description": "Provides the default wiring of a Backstage App", "backstage": { "role": "web-library" diff --git a/packages/app-example-plugin/CHANGELOG.md b/packages/app-example-plugin/CHANGELOG.md index a23772bb3b..f0784d88d2 100644 --- a/packages/app-example-plugin/CHANGELOG.md +++ b/packages/app-example-plugin/CHANGELOG.md @@ -1,5 +1,13 @@ # app-example-plugin +## 0.0.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/core-components@0.18.8-next.0 + ## 0.0.32 ### Patch Changes diff --git a/packages/app-example-plugin/package.json b/packages/app-example-plugin/package.json index 2bdfd931fd..93f2954355 100644 --- a/packages/app-example-plugin/package.json +++ b/packages/app-example-plugin/package.json @@ -1,6 +1,6 @@ { "name": "app-example-plugin", - "version": "0.0.32", + "version": "0.0.33-next.0", "description": "Backstage internal example plugin", "backstage": { "role": "frontend-plugin", diff --git a/packages/app-legacy/CHANGELOG.md b/packages/app-legacy/CHANGELOG.md index 2212011ecd..2ce716d27e 100644 --- a/packages/app-legacy/CHANGELOG.md +++ b/packages/app-legacy/CHANGELOG.md @@ -1,5 +1,93 @@ # example-app-legacy +## 0.2.119-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/cli@0.36.0-next.1 + - @backstage/plugin-devtools@0.1.37-next.1 + - @backstage/plugin-scaffolder@1.35.5-next.1 + - @backstage/plugin-api-docs@0.13.5-next.1 + - @backstage/plugin-techdocs@1.17.1-next.1 + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/plugin-scaffolder-react@1.19.8-next.1 + - @backstage/plugin-mui-to-bui@0.2.5-next.1 + - @backstage/plugin-catalog-graph@0.5.8-next.1 + - @backstage/plugin-catalog-import@0.13.11-next.1 + - @backstage/plugin-home@0.9.3-next.1 + - @backstage/plugin-org@0.6.50-next.1 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + - @backstage/plugin-home-react@0.1.36-next.0 + - @backstage/plugin-kubernetes@0.12.17-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.35-next.1 + - @backstage/plugin-notifications@0.5.15-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-search@1.6.2-next.1 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-signals@0.0.29-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + - @backstage/plugin-user-settings@0.9.1-next.1 + +## 0.2.119-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-search@1.6.2-next.0 + - @backstage/plugin-api-docs@0.13.5-next.0 + - @backstage/cli@0.35.5-next.0 + - @backstage/plugin-scaffolder@1.35.5-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/plugin-mui-to-bui@0.2.5-next.0 + - @backstage/plugin-techdocs@1.17.1-next.0 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-catalog-graph@0.5.8-next.0 + - @backstage/plugin-catalog-import@0.13.11-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + - @backstage/plugin-devtools@0.1.37-next.0 + - @backstage/plugin-home@0.9.3-next.0 + - @backstage/plugin-home-react@0.1.36-next.0 + - @backstage/plugin-kubernetes@0.12.17-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.35-next.0 + - @backstage/plugin-notifications@0.5.15-next.0 + - @backstage/plugin-org@0.6.50-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-scaffolder-react@1.19.8-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-signals@0.0.29-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + - @backstage/plugin-user-settings@0.9.1-next.0 + ## 0.2.118 ### Patch Changes diff --git a/packages/app-legacy/package.json b/packages/app-legacy/package.json index 6be63b7a14..0de94c265a 100644 --- a/packages/app-legacy/package.json +++ b/packages/app-legacy/package.json @@ -1,6 +1,6 @@ { "name": "example-app-legacy", - "version": "0.2.118", + "version": "0.2.119-next.1", "backstage": { "role": "frontend" }, diff --git a/packages/app-legacy/src/components/catalog/EntityPage.tsx b/packages/app-legacy/src/components/catalog/EntityPage.tsx index 08d4d921b5..32249cb84a 100644 --- a/packages/app-legacy/src/components/catalog/EntityPage.tsx +++ b/packages/app-legacy/src/components/catalog/EntityPage.tsx @@ -166,7 +166,7 @@ const overviewContent = ( {entityWarningContent} - + @@ -330,7 +330,7 @@ const userPage = ( {entityWarningContent} - + {entityWarningContent} - + {entityWarningContent} - + @@ -418,7 +418,7 @@ const domainPage = ( {entityWarningContent} - + @@ -437,7 +437,7 @@ const resourcePage = ( {entityWarningContent} - + diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index daa9908400..59f5989650 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,105 @@ # example-app +## 0.0.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/cli@0.36.0-next.1 + - @backstage/plugin-devtools@0.1.37-next.1 + - @backstage/plugin-scaffolder@1.35.5-next.1 + - @backstage/plugin-api-docs@0.13.5-next.1 + - @backstage/plugin-techdocs@1.17.1-next.1 + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/plugin-scaffolder-react@1.19.8-next.1 + - @backstage/plugin-app@0.4.1-next.1 + - @backstage/plugin-app-visualizer@0.2.1-next.1 + - @backstage/plugin-catalog-graph@0.5.8-next.1 + - @backstage/plugin-catalog-import@0.13.11-next.1 + - @backstage/plugin-home@0.9.3-next.1 + - @backstage/plugin-org@0.6.50-next.1 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-compat-api@0.5.9-next.1 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-auth@0.1.6-next.0 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + - @backstage/plugin-home-react@0.1.36-next.0 + - @backstage/plugin-kubernetes@0.12.17-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.35-next.1 + - @backstage/plugin-notifications@0.5.15-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-search@1.6.2-next.1 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-signals@0.0.29-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.1 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + - @backstage/plugin-user-settings@0.9.1-next.1 + +## 0.0.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/plugin-search-react@1.10.5-next.0 + - @backstage/plugin-search@1.6.2-next.0 + - @backstage/plugin-api-docs@0.13.5-next.0 + - @backstage/cli@0.35.5-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-scaffolder@1.35.5-next.0 + - @backstage/plugin-app@0.4.1-next.0 + - @backstage/plugin-app-visualizer@0.2.1-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/plugin-techdocs@1.17.1-next.0 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-compat-api@0.5.9-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-auth@0.1.6-next.0 + - @backstage/plugin-auth-react@0.1.25-next.0 + - @backstage/plugin-catalog-common@1.1.8 + - @backstage/plugin-catalog-graph@0.5.8-next.0 + - @backstage/plugin-catalog-import@0.13.11-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.27-next.0 + - @backstage/plugin-devtools@0.1.37-next.0 + - @backstage/plugin-home@0.9.3-next.0 + - @backstage/plugin-home-react@0.1.36-next.0 + - @backstage/plugin-kubernetes@0.12.17-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.35-next.0 + - @backstage/plugin-notifications@0.5.15-next.0 + - @backstage/plugin-org@0.6.50-next.0 + - @backstage/plugin-permission-react@0.4.41-next.0 + - @backstage/plugin-scaffolder-react@1.19.8-next.0 + - @backstage/plugin-search-common@1.2.22 + - @backstage/plugin-signals@0.0.29-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.34-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + - @backstage/plugin-user-settings@0.9.1-next.0 + ## 0.0.32 ### Patch Changes diff --git a/packages/app/app-config.yaml b/packages/app/app-config.yaml index 2000e785b3..37893722f7 100644 --- a/packages/app/app-config.yaml +++ b/packages/app/app-config.yaml @@ -48,6 +48,8 @@ app: - page:catalog/entity: config: showNavItemIcons: true + # default content order for all groups, can be 'title' or 'natural' + # defaultContentOrder: title groups: # placing a tab at the beginning - overview: @@ -58,6 +60,9 @@ app: - documentation: title: Docs icon: docs + # example aliasing a group + # aliases: + # - docs - deployment: title: Deployments # example adding a new group @@ -97,22 +102,19 @@ app: # - entity-card:azure-devops/readme # Entity page contents - - entity-content:catalog/overview: - config: - group: overview + - entity-content:catalog/overview - entity-content:api-docs/definition - entity-content:api-docs/apis: config: - # example associating with a default group + # example overriding the default group group: documentation icon: kind:api - entity-content:techdocs: config: - group: documentation icon: techdocs - entity-content:kubernetes/kubernetes: config: - # example disassociating with a default group + # example disassociating from the default group group: false # - entity-content:azure-devops/pipelines # - entity-content:azure-devops/pull-requests diff --git a/packages/app/package.json b/packages/app/package.json index b0d46befa8..e79ba2775c 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.0.32", + "version": "0.0.33-next.1", "backstage": { "role": "frontend" }, diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md index f1d26a7f4a..b07cb0869d 100644 --- a/packages/backend-app-api/CHANGELOG.md +++ b/packages/backend-app-api/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/backend-app-api +## 1.5.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + ## 1.5.0 ### Minor Changes diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 97e5964e39..08b12d7d6d 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-app-api", - "version": "1.5.0", + "version": "1.5.1-next.0", "description": "Core API used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index 0491faa96f..f782295e71 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -899,7 +899,7 @@ describe('BackendInitializer', () => { }); it('should reject duplicate plugins', async () => { - const init = new BackendInitializer([]); + const init = new BackendInitializer(baseFactories); init.add( createBackendPlugin({ pluginId: 'test', @@ -922,13 +922,24 @@ describe('BackendInitializer', () => { }, }), ); - await expect(init.start()).rejects.toThrow( + + const err = await init.start().then( + () => { + throw new Error('Expected BackendStartupError to be thrown'); + }, + (e: BackendStartupError) => e, + ); + + expect(err).toBeInstanceOf(BackendStartupError); + const plugin = err?.result.plugins.find(p => p.pluginId === 'test'); + expect(plugin?.failure?.error.message).toBe( "Plugin 'test' is already registered", ); + expect(plugin?.failure?.allowed).toBe(false); }); it('should reject duplicate modules', async () => { - const init = new BackendInitializer([]); + const init = new BackendInitializer(baseFactories); init.add(testPlugin); init.add( createBackendModule({ @@ -954,8 +965,202 @@ describe('BackendInitializer', () => { }, }), ); - await expect(init.start()).rejects.toThrow( - "Module 'mod' for plugin 'test' is already registered", + + const err = await init.start().then( + () => { + throw new Error('Expected BackendStartupError to be thrown'); + }, + (e: BackendStartupError) => e, + ); + + expect(err).toBeInstanceOf(BackendStartupError); + const plugin = err?.result.plugins.find(p => p.pluginId === 'test'); + const modResult = plugin?.modules.find( + m => + m.failure?.error.message === + "Module 'mod' for plugin 'test' is already registered", + ); + expect(modResult).toBeDefined(); + expect(modResult?.failure?.allowed).toBe(false); + }); + + it('should allow other plugins to continue when one has a registration error', async () => { + const pluginAInit = jest.fn(async () => {}); + const init = new BackendInitializer(baseFactories); + init.add( + createBackendPlugin({ + pluginId: 'plugin-a', + register(reg) { + reg.registerInit({ + deps: {}, + init: pluginAInit, + }); + }, + }), + ); + init.add( + createBackendPlugin({ + pluginId: 'plugin-b', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + init.add( + createBackendPlugin({ + pluginId: 'plugin-b', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + + const err = await init.start().then( + () => { + throw new Error('Expected BackendStartupError to be thrown'); + }, + (e: BackendStartupError) => e, + ); + + expect(err).toBeInstanceOf(BackendStartupError); + // plugin-a should have started successfully + expect(pluginAInit).toHaveBeenCalled(); + const pluginA = err?.result.plugins.find(p => p.pluginId === 'plugin-a'); + expect(pluginA?.failure).toBeUndefined(); + // plugin-b should have a registration failure + const pluginB = err?.result.plugins.find(p => p.pluginId === 'plugin-b'); + expect(pluginB?.failure?.error.message).toBe( + "Plugin 'plugin-b' is already registered", + ); + }); + + it('should permit registration errors for plugins with onPluginBootFailure: continue', async () => { + const init = new BackendInitializer([ + ...baseFactories, + mockServices.rootConfig.factory({ + data: { + backend: { + startup: { + plugins: { test: { onPluginBootFailure: 'continue' } }, + }, + }, + }, + }), + ]); + init.add( + createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + init.add( + createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + + const { result } = await init.start(); + const plugin = result.plugins.find(p => p.pluginId === 'test'); + expect(plugin?.failure?.error.message).toBe( + "Plugin 'test' is already registered", + ); + expect(plugin?.failure?.allowed).toBe(true); + }); + + it('should attribute duplicate extension point errors to the correct plugin', async () => { + const extensionPoint = createExtensionPoint({ id: 'shared-ext' }); + const init = new BackendInitializer(baseFactories); + init.add( + createBackendPlugin({ + pluginId: 'plugin-a', + register(reg) { + reg.registerExtensionPoint(extensionPoint, 'a'); + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + init.add( + createBackendPlugin({ + pluginId: 'plugin-b', + register(reg) { + reg.registerExtensionPoint(extensionPoint, 'b'); + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), + ); + + const err = await init.start().then( + () => { + throw new Error('Expected BackendStartupError to be thrown'); + }, + (e: BackendStartupError) => e, + ); + + expect(err).toBeInstanceOf(BackendStartupError); + // plugin-a should succeed (registered first) + const pluginA = err?.result.plugins.find(p => p.pluginId === 'plugin-a'); + expect(pluginA?.failure).toBeUndefined(); + // plugin-b should fail due to duplicate extension point + const pluginB = err?.result.plugins.find(p => p.pluginId === 'plugin-b'); + expect(pluginB?.failure?.error.message).toBe( + "ExtensionPoint with ID 'shared-ext' is already registered", + ); + }); + + it('should attribute invalid registration type errors to plugin when pluginId is available', async () => { + const init = new BackendInitializer(baseFactories); + // Create a fake registration with an invalid type but valid pluginId + const fakeFeature = { + $$type: '@backstage/BackendFeature' as const, + version: 'v1' as const, + featureType: 'registrations' as const, + getRegistrations: () => [ + { + type: 'invalid-type', + pluginId: 'broken-plugin', + init: { deps: {}, func: async () => {} }, + extensionPoints: [], + }, + ], + }; + init.add(fakeFeature as any); + + const err = await init.start().then( + () => { + throw new Error('Expected BackendStartupError to be thrown'); + }, + (e: BackendStartupError) => e, + ); + + expect(err).toBeInstanceOf(BackendStartupError); + const plugin = err?.result.plugins.find( + p => p.pluginId === 'broken-plugin', + ); + expect(plugin?.failure?.error.message).toBe( + "Invalid registration type 'invalid-type'", ); }); diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index d51befb644..8ebab723da 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -310,77 +310,6 @@ export class BackendInitializer { // Initialize all root scoped services await this.#serviceRegistry.initializeEagerServicesWithScope('root'); - const pluginInits = new Map(); - const moduleInits = new Map>(); - - // Enumerate all registrations - for (const feature of this.#registrations) { - for (const r of feature.getRegistrations()) { - const provides = new Set>(); - - if (r.type === 'plugin' || r.type === 'module') { - // Handle v1 format: Array, unknown]> - for (const [extRef, extImpl] of r.extensionPoints) { - if (this.#extensionPoints.has(extRef.id)) { - throw new Error( - `ExtensionPoint with ID '${extRef.id}' is already registered`, - ); - } - this.#extensionPoints.set(extRef.id, { - pluginId: r.pluginId, - factory: () => extImpl, - }); - provides.add(extRef); - } - } else if (r.type === 'plugin-v1.1' || r.type === 'module-v1.1') { - // Handle v1.1 format: Array - for (const extReg of r.extensionPoints) { - if (this.#extensionPoints.has(extReg.extensionPoint.id)) { - throw new Error( - `ExtensionPoint with ID '${extReg.extensionPoint.id}' is already registered`, - ); - } - this.#extensionPoints.set(extReg.extensionPoint.id, { - pluginId: r.pluginId, - factory: extReg.factory, - }); - provides.add(extReg.extensionPoint); - } - } - - if (r.type === 'plugin' || r.type === 'plugin-v1.1') { - if (pluginInits.has(r.pluginId)) { - throw new Error(`Plugin '${r.pluginId}' is already registered`); - } - pluginInits.set(r.pluginId, { - provides, - consumes: new Set(Object.values(r.init.deps)), - init: r.init, - }); - } else if (r.type === 'module' || r.type === 'module-v1.1') { - let modules = moduleInits.get(r.pluginId); - if (!modules) { - modules = new Map(); - moduleInits.set(r.pluginId, modules); - } - if (modules.has(r.moduleId)) { - throw new Error( - `Module '${r.moduleId}' for plugin '${r.pluginId}' is already registered`, - ); - } - modules.set(r.moduleId, { - provides, - consumes: new Set(Object.values(r.init.deps)), - init: r.init, - }); - } else { - throw new Error(`Invalid registration type '${(r as any).type}'`); - } - } - } - - const pluginIds = [...pluginInits.keys()]; - const rootConfig = await this.#serviceRegistry.get( coreServices.rootConfig, 'root', @@ -390,15 +319,32 @@ export class BackendInitializer { 'root', ); + const allRegistrations = this.#registrations.flatMap(f => + f.getRegistrations(), + ); + + const allPluginIds = [ + ...new Set( + allRegistrations.flatMap(r => + 'pluginId' in r && typeof r.pluginId === 'string' ? [r.pluginId] : [], + ), + ), + ]; + const resultCollector = createInitializationResultCollector({ - pluginIds, + pluginIds: allPluginIds, logger: rootLogger, allowBootFailurePredicate: createAllowBootFailurePredicate(rootConfig), }); + const { pluginInits, moduleInits } = this.#enumerateRegistrations( + allRegistrations, + resultCollector, + ); + // All plugins are initialized in parallel await Promise.all( - pluginIds.map(async pluginId => { + [...pluginInits.keys()].map(async pluginId => { try { // Initialize all eager services await this.#serviceRegistry.initializeEagerServicesWithScope( @@ -491,6 +437,104 @@ export class BackendInitializer { return { result }; } + #enumerateRegistrations( + allRegistrations: ReturnType< + InternalBackendRegistrations['getRegistrations'] + >, + resultCollector: ReturnType, + ): { + pluginInits: Map; + moduleInits: Map>; + } { + const pluginInits = new Map(); + const moduleInits = new Map>(); + + for (const r of allRegistrations) { + const addedExtensionPointIds: string[] = []; + try { + const provides = new Set>(); + + if (r.type === 'plugin' || r.type === 'module') { + // Handle v1 format: Array, unknown]> + for (const [extRef, extImpl] of r.extensionPoints) { + if (this.#extensionPoints.has(extRef.id)) { + throw new Error( + `ExtensionPoint with ID '${extRef.id}' is already registered`, + ); + } + this.#extensionPoints.set(extRef.id, { + pluginId: r.pluginId, + factory: () => extImpl, + }); + addedExtensionPointIds.push(extRef.id); + provides.add(extRef); + } + } else if (r.type === 'plugin-v1.1' || r.type === 'module-v1.1') { + // Handle v1.1 format: Array + for (const extReg of r.extensionPoints) { + if (this.#extensionPoints.has(extReg.extensionPoint.id)) { + throw new Error( + `ExtensionPoint with ID '${extReg.extensionPoint.id}' is already registered`, + ); + } + this.#extensionPoints.set(extReg.extensionPoint.id, { + pluginId: r.pluginId, + factory: extReg.factory, + }); + addedExtensionPointIds.push(extReg.extensionPoint.id); + provides.add(extReg.extensionPoint); + } + } + + if (r.type === 'plugin' || r.type === 'plugin-v1.1') { + if (pluginInits.has(r.pluginId)) { + throw new Error(`Plugin '${r.pluginId}' is already registered`); + } + pluginInits.set(r.pluginId, { + provides, + consumes: new Set(Object.values(r.init.deps)), + init: r.init, + }); + } else if (r.type === 'module' || r.type === 'module-v1.1') { + let modules = moduleInits.get(r.pluginId); + if (!modules) { + modules = new Map(); + moduleInits.set(r.pluginId, modules); + } + if (modules.has(r.moduleId)) { + throw new Error( + `Module '${r.moduleId}' for plugin '${r.pluginId}' is already registered`, + ); + } + modules.set(r.moduleId, { + provides, + consumes: new Set(Object.values(r.init.deps)), + init: r.init, + }); + } else { + throw new Error(`Invalid registration type '${(r as any).type}'`); + } + } catch (error: unknown) { + assertError(error); + // Clean up partially registered extension points + for (const id of addedExtensionPointIds) { + this.#extensionPoints.delete(id); + } + if ('pluginId' in r && 'moduleId' in r) { + resultCollector.onPluginModuleResult(r.pluginId, r.moduleId, error); + } else if ('pluginId' in r) { + pluginInits.delete(r.pluginId); + moduleInits.delete(r.pluginId); + resultCollector.onPluginResult(r.pluginId, error); + } else { + throw error; + } + } + } + + return { pluginInits, moduleInits }; + } + // It's fine to call .stop() multiple times, which for example can happen with manual stop + process exit async stop(): Promise { instanceRegistry.unregister(this); diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index da970f1f45..05f54be89f 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,55 @@ # @backstage/backend-defaults +## 0.16.0-next.1 + +### Minor Changes + +- 0e7d8f9: The scheduler service now uses the metrics service to create metrics, providing plugin-scoped attribution. +- 527cf88: **BREAKING** Removed deprecated `BitbucketUrlReader`. Use the `BitbucketCloudUrlReader` or the `BitbucketServerUrlReader` instead. + +### Patch Changes + +- 62f0a53: Fixed error forwarding in the actions registry so that known errors like `InputError` and `NotFoundError` thrown by actions preserve their original status codes and messages instead of being wrapped in `ForwardedError` and coerced to 500. +- Updated dependencies + - @backstage/cli-node@0.2.19-next.1 + - @backstage/integration@2.0.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/backend-app-api@1.5.1-next.0 + - @backstage/backend-dev-utils@0.1.7 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-node@0.10.11-next.0 + +## 0.15.3-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- d933f62: Add configurable throttling and retry mechanism for GitLab integration. +- b99158a: Fixed `yarn backstage-cli config:check --strict --config app-config.yaml` config validation error by adding + an optional `default` type discriminator to PostgreSQL connection configuration, + allowing `config:check` to properly validate `default` connection configurations. +- 1ee5b28: Adds an alpha `MetricsService` to provide a unified interface for metrics instrumentation across Backstage plugins. +- Updated dependencies + - @backstage/cli-node@0.2.19-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-app-api@1.5.1-next.0 + - @backstage/backend-dev-utils@0.1.7 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/integration-aws-node@0.1.20 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-node@0.10.11-next.0 + ## 0.15.2 ### Patch Changes diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts index 64aabe5cc8..3d09146b0d 100644 --- a/packages/backend-defaults/config.d.ts +++ b/packages/backend-defaults/config.d.ts @@ -1127,6 +1127,36 @@ export interface Config { headers?: { [name: string]: string }; }; + /** + * Options for the metrics service. + */ + metrics?: { + /** + * Plugin-specific metrics configuration. Each plugin can override meter metadata. + */ + plugin?: { + [pluginId: string]: { + /** + * Meter configuration for this plugin. + */ + meter?: { + /** + * Custom meter name. If not set, defaults to backstage-plugin-{pluginId}. + */ + name?: string; + /** + * Version for the meter. + */ + version?: string; + /** + * Schema URL for the meter. + */ + schemaUrl?: string; + }; + }; + }; + }; + /** * Options to configure the default RootLoggerService. */ diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 6f705de7ba..4aaf0408b4 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-defaults", - "version": "0.15.2", + "version": "0.16.0-next.1", "description": "Backend defaults used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/backend-defaults/report-alpha.api.md b/packages/backend-defaults/report-alpha.api.md index c40517863a..352ec1d688 100644 --- a/packages/backend-defaults/report-alpha.api.md +++ b/packages/backend-defaults/report-alpha.api.md @@ -5,6 +5,7 @@ ```ts import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; import { ActionsService } from '@backstage/backend-plugin-api/alpha'; +import { MetricsService } from '@backstage/backend-plugin-api/alpha'; import { RootSystemMetadataService } from '@backstage/backend-plugin-api/alpha'; import { ServiceFactory } from '@backstage/backend-plugin-api'; @@ -22,6 +23,13 @@ export const actionsServiceFactory: ServiceFactory< 'singleton' >; +// @alpha +export const metricsServiceFactory: ServiceFactory< + MetricsService, + 'plugin', + 'singleton' +>; + // @alpha export const rootSystemMetadataServiceFactory: ServiceFactory< RootSystemMetadataService, diff --git a/packages/backend-defaults/report-scheduler.api.md b/packages/backend-defaults/report-scheduler.api.md index e0a85710a5..44a503d18c 100644 --- a/packages/backend-defaults/report-scheduler.api.md +++ b/packages/backend-defaults/report-scheduler.api.md @@ -6,6 +6,7 @@ import { DatabaseService } from '@backstage/backend-plugin-api'; import { HttpRouterService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; +import { MetricsService } from '@backstage/backend-plugin-api/alpha'; import { PluginMetadataService } from '@backstage/backend-plugin-api'; import { RootLifecycleService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; @@ -17,6 +18,7 @@ export class DefaultSchedulerService { static create(options: { database: DatabaseService; logger: LoggerService; + metrics: MetricsService; rootLifecycle: RootLifecycleService; httpRouter: HttpRouterService; pluginMetadata: PluginMetadataService; diff --git a/packages/backend-defaults/report-urlReader.api.md b/packages/backend-defaults/report-urlReader.api.md index 78a35911b8..c32a63231b 100644 --- a/packages/backend-defaults/report-urlReader.api.md +++ b/packages/backend-defaults/report-urlReader.api.md @@ -10,7 +10,6 @@ import { AzureCredentialsManager } from '@backstage/integration'; import { AzureDevOpsCredentialsProvider } from '@backstage/integration'; import { AzureIntegration } from '@backstage/integration'; import { BitbucketCloudIntegration } from '@backstage/integration'; -import { BitbucketIntegration } from '@backstage/integration'; import { BitbucketServerIntegration } from '@backstage/integration'; import { Config } from '@backstage/config'; import { GerritIntegration } from '@backstage/integration'; @@ -190,38 +189,6 @@ export class BitbucketServerUrlReader implements UrlReaderService { toString(): string; } -// @public @deprecated -export class BitbucketUrlReader implements UrlReaderService { - constructor( - integration: BitbucketIntegration, - logger: LoggerService, - deps: { - treeResponseFactory: ReadTreeResponseFactory; - }, - ); - // (undocumented) - static factory: ReaderFactory; - // (undocumented) - read(url: string): Promise; - // (undocumented) - readTree( - url: string, - options?: UrlReaderServiceReadTreeOptions, - ): Promise; - // (undocumented) - readUrl( - url: string, - options?: UrlReaderServiceReadUrlOptions, - ): Promise; - // (undocumented) - search( - url: string, - options?: UrlReaderServiceSearchOptions, - ): Promise; - // (undocumented) - toString(): string; -} - // @public export class FetchUrlReader implements UrlReaderService { static factory: ReaderFactory; diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index 9f8116a996..2b4ee5befa 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -38,6 +38,7 @@ import { eventsServiceFactory } from '@backstage/plugin-events-node'; import { actionsRegistryServiceFactory, actionsServiceFactory, + metricsServiceFactory, } from '@backstage/backend-defaults/alpha'; import { instanceMetadataServiceFactory } from './alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory'; @@ -66,6 +67,7 @@ export const defaultServiceFactories = [ // alpha services actionsRegistryServiceFactory, actionsServiceFactory, + metricsServiceFactory, // Unexported alpha services kept around for compatibility reasons instanceMetadataServiceFactory, diff --git a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts index 7348e0a185..ffe74f81d9 100644 --- a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts +++ b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts @@ -28,12 +28,7 @@ import { ActionsRegistryActionOptions, ActionsRegistryService, } from '@backstage/backend-plugin-api/alpha'; -import { - ForwardedError, - InputError, - NotAllowedError, - NotFoundError, -} from '@backstage/errors'; +import { InputError, NotAllowedError, NotFoundError } from '@backstage/errors'; export class DefaultActionsRegistryService implements ActionsRegistryService { private actions: Map> = @@ -131,31 +126,24 @@ export class DefaultActionsRegistryService implements ActionsRegistryService { ); } - try { - const result = await action.action({ - input: input.data, - credentials, - logger: this.logger, - }); + const result = await action.action({ + input: input.data, + credentials, + logger: this.logger, + }); - const output = action.schema?.output - ? action.schema.output(z).safeParse(result?.output) - : ({ success: true, data: result?.output } as const); + const output = action.schema?.output + ? action.schema.output(z).safeParse(result?.output) + : ({ success: true, data: result?.output } as const); - if (!output.success) { - throw new InputError( - `Invalid output from action "${req.params.actionId}"`, - output.error, - ); - } - - res.json({ output: output.data }); - } catch (error) { - throw new ForwardedError( - `Failed execution of action "${req.params.actionId}"`, - error, + if (!output.success) { + throw new InputError( + `Invalid output from action "${req.params.actionId}"`, + output.error, ); } + + res.json({ output: output.data }); }, ); return router; diff --git a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.test.ts b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.test.ts index 79c103c284..1e7c27960e 100644 --- a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.test.ts +++ b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/actionsRegistryServiceFactory.test.ts @@ -22,7 +22,7 @@ import { import { httpRouterServiceFactory } from '../../../entrypoints/httpRouter'; import request from 'supertest'; import { actionsRegistryServiceFactory } from './actionsRegistryServiceFactory'; -import { InputError } from '@backstage/errors'; +import { InputError, NotFoundError } from '@backstage/errors'; import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha'; describe('actionsRegistryServiceFactory', () => { @@ -510,7 +510,7 @@ describe('actionsRegistryServiceFactory', () => { expect(body).toMatchObject({ output: { ok: true } }); }); - it('should return the error from the action if it throws', async () => { + it('should forward the original error when the action throws a known error', async () => { const { server } = await startTestBackend({ features: [pluginSubject, ...defaultServices], }); @@ -528,9 +528,32 @@ describe('actionsRegistryServiceFactory', () => { expect(status).toBe(400); expect(body).toMatchObject({ error: { - message: expect.stringContaining( - 'Failed execution of action "my-plugin:test"', - ), + name: 'InputError', + message: 'test', + }, + }); + }); + + it('should forward a NotFoundError from the action with 404 status', async () => { + const { server } = await startTestBackend({ + features: [pluginSubject, ...defaultServices], + }); + + mockAction.mockRejectedValue(new NotFoundError('entity not found')); + + const { body, status } = await request(server) + .post( + '/api/my-plugin/.backstage/actions/v1/actions/my-plugin:test/invoke', + ) + .send({ + name: 'test', + }); + + expect(status).toBe(404); + expect(body).toMatchObject({ + error: { + name: 'NotFoundError', + message: 'entity not found', }, }); }); diff --git a/packages/backend-defaults/src/alpha/entrypoints/metrics/DefaultMetricsService.test.ts b/packages/backend-defaults/src/alpha/entrypoints/metrics/DefaultMetricsService.test.ts new file mode 100644 index 0000000000..75e5104754 --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/metrics/DefaultMetricsService.test.ts @@ -0,0 +1,117 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { metrics } from '@opentelemetry/api'; +import { DefaultMetricsService } from './DefaultMetricsService'; + +const mockGetMeter = jest.spyOn(metrics, 'getMeter'); + +describe('DefaultMetricsService', () => { + beforeEach(() => { + mockGetMeter.mockClear(); + }); + + describe('create', () => { + it('should create a MetricsService with name only', () => { + const service = DefaultMetricsService.create({ name: 'test-meter' }); + + expect(mockGetMeter).toHaveBeenCalledTimes(1); + expect(mockGetMeter).toHaveBeenCalledWith('test-meter', undefined, { + schemaUrl: undefined, + }); + + expect(service).toBeDefined(); + }); + + it('should create a MetricsService with name, version, and schemaUrl', () => { + const service = DefaultMetricsService.create({ + name: 'test-meter', + version: '1.2.3', + schemaUrl: 'https://example.com/schema', + }); + + expect(mockGetMeter).toHaveBeenCalledTimes(1); + expect(mockGetMeter).toHaveBeenCalledWith('test-meter', '1.2.3', { + schemaUrl: 'https://example.com/schema', + }); + + expect(service).toBeDefined(); + }); + }); + + describe('metric instruments', () => { + it('should create a counter', () => { + const service = DefaultMetricsService.create({ name: 'test' }); + const counter = service.createCounter('my_counter', { + description: 'A test counter', + unit: 'bytes', + }); + + expect(counter).toBeDefined(); + expect(counter.add).toBeDefined(); + }); + + it('should create an up-down counter', () => { + const service = DefaultMetricsService.create({ name: 'test' }); + const upDownCounter = service.createUpDownCounter('my_updown'); + + expect(upDownCounter).toBeDefined(); + expect(upDownCounter.add).toBeDefined(); + }); + + it('should create a histogram', () => { + const service = DefaultMetricsService.create({ name: 'test' }); + const histogram = service.createHistogram('my_histogram'); + + expect(histogram).toBeDefined(); + expect(histogram.record).toBeDefined(); + }); + + it('should create a gauge', () => { + const service = DefaultMetricsService.create({ name: 'test' }); + const gauge = service.createGauge('my_gauge'); + + expect(gauge).toBeDefined(); + expect(gauge.record).toBeDefined(); + }); + + it('should create an observable counter', () => { + const service = DefaultMetricsService.create({ name: 'test' }); + const counter = service.createObservableCounter('my_observable_counter'); + + expect(counter).toBeDefined(); + expect(counter.addCallback).toBeDefined(); + expect(counter.removeCallback).toBeDefined(); + }); + + it('should create an observable up-down counter', () => { + const service = DefaultMetricsService.create({ name: 'test' }); + const counter = service.createObservableUpDownCounter( + 'my_observable_updown', + ); + + expect(counter).toBeDefined(); + expect(counter.addCallback).toBeDefined(); + }); + + it('should create an observable gauge', () => { + const service = DefaultMetricsService.create({ name: 'test' }); + const gauge = service.createObservableGauge('my_observable_gauge'); + + expect(gauge).toBeDefined(); + expect(gauge.addCallback).toBeDefined(); + }); + }); +}); diff --git a/packages/backend-defaults/src/alpha/entrypoints/metrics/DefaultMetricsService.ts b/packages/backend-defaults/src/alpha/entrypoints/metrics/DefaultMetricsService.ts new file mode 100644 index 0000000000..764c6d6d1c --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/metrics/DefaultMetricsService.ts @@ -0,0 +1,123 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Meter, metrics } from '@opentelemetry/api'; +import { + MetricsService, + MetricAttributes, + MetricOptions, + MetricsServiceCounter, + MetricsServiceUpDownCounter, + MetricsServiceHistogram, + MetricsServiceGauge, + MetricsServiceObservableCounter, + MetricsServiceObservableGauge, + MetricsServiceObservableUpDownCounter, +} from '@backstage/backend-plugin-api/alpha'; + +/** + * Options for creating a {@link DefaultMetricsService}. + * + * @alpha + */ +export interface DefaultMetricsServiceOptions { + name: string; + version?: string; + schemaUrl?: string; +} + +/** + * Default implementation of the {@link MetricsService} interface. + * + * This implementation provides a thin wrapper around the OpenTelemetry Meter API. + * + * @alpha + */ +export class DefaultMetricsService implements MetricsService { + private readonly meter: Meter; + + private constructor(opts: DefaultMetricsServiceOptions) { + // The meter name sets the OpenTelemetry Instrumentation Scope which identifies the source of metrics in telemetry backends. + this.meter = metrics.getMeter(opts.name, opts.version, { + schemaUrl: opts.schemaUrl, + }); + } + + /** + * Creates a new {@link MetricsService} instance. + * + * @param opts - Options for configuring the meter scope + * @returns A new MetricsService instance + */ + static create(opts: DefaultMetricsServiceOptions): MetricsService { + return new DefaultMetricsService(opts); + } + + createCounter( + name: string, + opts?: MetricOptions, + ): MetricsServiceCounter { + return this.meter.createCounter(name, opts); + } + + createUpDownCounter( + name: string, + opts?: MetricOptions, + ): MetricsServiceUpDownCounter { + return this.meter.createUpDownCounter(name, opts); + } + + createHistogram( + name: string, + opts?: MetricOptions, + ): MetricsServiceHistogram { + return this.meter.createHistogram(name, opts); + } + + createGauge( + name: string, + opts?: MetricOptions, + ): MetricsServiceGauge { + return this.meter.createGauge(name, opts); + } + + createObservableCounter< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableCounter { + return this.meter.createObservableCounter(name, opts); + } + + createObservableUpDownCounter< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableUpDownCounter { + return this.meter.createObservableUpDownCounter(name, opts); + } + + createObservableGauge< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableGauge { + return this.meter.createObservableGauge(name, opts); + } +} diff --git a/packages/backend-defaults/src/alpha/entrypoints/metrics/index.ts b/packages/backend-defaults/src/alpha/entrypoints/metrics/index.ts new file mode 100644 index 0000000000..fc3e04a855 --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/metrics/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 { metricsServiceFactory } from './metricsServiceFactory'; diff --git a/packages/backend-defaults/src/alpha/entrypoints/metrics/metricsServiceFactory.test.ts b/packages/backend-defaults/src/alpha/entrypoints/metrics/metricsServiceFactory.test.ts new file mode 100644 index 0000000000..854fcfd389 --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/metrics/metricsServiceFactory.test.ts @@ -0,0 +1,130 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + mockServices, + ServiceFactoryTester, +} from '@backstage/backend-test-utils'; +import { metricsServiceFactory } from './metricsServiceFactory'; +import { DefaultMetricsService } from './DefaultMetricsService'; + +describe('metricsServiceFactory', () => { + let createSpy: jest.SpyInstance; + + beforeEach(() => { + createSpy = jest.spyOn(DefaultMetricsService, 'create'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + const defaultServices = [ + mockServices.rootConfig.factory(), + metricsServiceFactory, + ]; + + it('should use backstage-plugin-{pluginId} as meter name when no config is set', async () => { + await ServiceFactoryTester.from(metricsServiceFactory, { + dependencies: defaultServices, + }).getSubject('my-plugin'); + + expect(createSpy).toHaveBeenCalledWith({ + name: 'backstage-plugin-my-plugin', + version: undefined, + schemaUrl: undefined, + }); + }); + + it('should use custom name from config', async () => { + await ServiceFactoryTester.from(metricsServiceFactory, { + dependencies: [ + mockServices.rootConfig.factory({ + data: { + backend: { + metrics: { + plugin: { + 'my-plugin': { + meter: { + name: 'custom-metrics-name', + }, + }, + }, + }, + }, + }, + }), + metricsServiceFactory, + ], + }).getSubject('my-plugin'); + + expect(createSpy).toHaveBeenCalledWith({ + name: 'custom-metrics-name', + version: undefined, + schemaUrl: undefined, + }); + }); + + it('should accept version and schemaUrl from config', async () => { + await ServiceFactoryTester.from(metricsServiceFactory, { + dependencies: [ + mockServices.rootConfig.factory({ + data: { + backend: { + metrics: { + plugin: { + 'my-plugin': { + meter: { + name: 'my-plugin-metrics', + version: '1.2.3', + schemaUrl: 'https://example.com/schema', + }, + }, + }, + }, + }, + }, + }), + metricsServiceFactory, + ], + }).getSubject('my-plugin'); + + expect(createSpy).toHaveBeenCalledWith({ + name: 'my-plugin-metrics', + version: '1.2.3', + schemaUrl: 'https://example.com/schema', + }); + }); + + it('should implement the full MetricsService interface', async () => { + const subject = await ServiceFactoryTester.from(metricsServiceFactory, { + dependencies: defaultServices, + }).getSubject('test-plugin'); + + expect(createSpy).toHaveBeenCalledWith({ + name: 'backstage-plugin-test-plugin', + version: undefined, + schemaUrl: undefined, + }); + + expect(subject.createCounter).toBeDefined(); + expect(subject.createUpDownCounter).toBeDefined(); + expect(subject.createHistogram).toBeDefined(); + expect(subject.createGauge).toBeDefined(); + expect(subject.createObservableCounter).toBeDefined(); + expect(subject.createObservableUpDownCounter).toBeDefined(); + expect(subject.createObservableGauge).toBeDefined(); + }); +}); diff --git a/packages/backend-defaults/src/alpha/entrypoints/metrics/metricsServiceFactory.ts b/packages/backend-defaults/src/alpha/entrypoints/metrics/metricsServiceFactory.ts new file mode 100644 index 0000000000..a62ad0411a --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/metrics/metricsServiceFactory.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { metricsServiceRef } from '@backstage/backend-plugin-api/alpha'; +import { + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { DefaultMetricsService } from './DefaultMetricsService'; + +/** + * Service factory for collecting plugin-scoped metrics. + * + * @alpha + */ +export const metricsServiceFactory = createServiceFactory({ + service: metricsServiceRef, + deps: { + config: coreServices.rootConfig, + pluginMetadata: coreServices.pluginMetadata, + }, + factory: ({ config, pluginMetadata }) => { + const pluginId = pluginMetadata.getId(); + + const meterConfig = config.getOptionalConfig( + `backend.metrics.plugin.${pluginId}.meter`, + ); + const scopeName = `backstage-plugin-${pluginId}`; + const name = meterConfig?.getOptionalString('name') ?? scopeName; + const version = meterConfig?.getOptionalString('version'); + const schemaUrl = meterConfig?.getOptionalString('schemaUrl'); + + return DefaultMetricsService.create({ name, version, schemaUrl }); + }, +}); diff --git a/packages/backend-defaults/src/alpha/index.ts b/packages/backend-defaults/src/alpha/index.ts index c17e0e71cb..1d0ec158af 100644 --- a/packages/backend-defaults/src/alpha/index.ts +++ b/packages/backend-defaults/src/alpha/index.ts @@ -16,4 +16,5 @@ export { actionsRegistryServiceFactory } from './entrypoints/actionsRegistry'; export { actionsServiceFactory } from './entrypoints/actions'; +export { metricsServiceFactory } from './entrypoints/metrics'; export { rootSystemMetadataServiceFactory } from './entrypoints/rootSystemMetadata'; diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.test.ts index 61f9bd0639..3de69ba566 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.test.ts @@ -20,6 +20,7 @@ import waitForExpect from 'wait-for-expect'; import { DefaultSchedulerService } from './DefaultSchedulerService'; import { createTestScopedSignal } from './__testUtils__/createTestScopedSignal'; import { PluginMetadataService } from '@backstage/backend-plugin-api'; +import { metricsServiceMock } from '@backstage/backend-test-utils/alpha'; jest.setTimeout(60_000); @@ -32,6 +33,7 @@ describe('TaskScheduler', () => { getId: () => 'test', } satisfies PluginMetadataService; const testScopedSignal = createTestScopedSignal(); + const metrics = metricsServiceMock.mock(); it.each(databases.eachSupportedId())( 'can return a working v1 plugin impl, %p', @@ -42,6 +44,7 @@ describe('TaskScheduler', () => { const manager = DefaultSchedulerService.create({ database, logger, + metrics, rootLifecycle, httpRouter, pluginMetadata, @@ -71,6 +74,7 @@ describe('TaskScheduler', () => { const manager = DefaultSchedulerService.create({ database, logger, + metrics, rootLifecycle, httpRouter, pluginMetadata, diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.ts index 0a7b9fbb87..07531cffc8 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.ts @@ -27,6 +27,7 @@ import { Duration } from 'luxon'; import { migrateBackendTasks } from '../database/migrateBackendTasks'; import { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl'; import { PluginTaskSchedulerJanitor } from './PluginTaskSchedulerJanitor'; +import { MetricsService } from '@backstage/backend-plugin-api/alpha'; /** * Default implementation of the task scheduler service. @@ -37,6 +38,7 @@ export class DefaultSchedulerService { static create(options: { database: DatabaseService; logger: LoggerService; + metrics: MetricsService; rootLifecycle: RootLifecycleService; httpRouter: HttpRouterService; pluginMetadata: PluginMetadataService; @@ -67,6 +69,7 @@ export class DefaultSchedulerService { options.pluginMetadata.getId(), databaseFactory, options.logger, + options.metrics, options.rootLifecycle, ); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts index 5872ae793e..2148148c9e 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts @@ -27,6 +27,7 @@ import { parseDuration, } from './PluginTaskSchedulerImpl'; import { createDeferred } from '@backstage/types'; +import { metricsServiceMock } from '@backstage/backend-test-utils/alpha'; jest.setTimeout(60_000); @@ -56,6 +57,7 @@ describe('PluginTaskManagerImpl', () => { 'myplugin', async () => knex, mockServices.logger.mock(), + metricsServiceMock.mock(), { addShutdownHook, addBeforeShutdownHook: jest.fn(), diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts index 90f5d08e4a..e711744123 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts @@ -24,7 +24,13 @@ import { SchedulerServiceTaskRunner, SchedulerServiceTaskScheduleDefinition, } from '@backstage/backend-plugin-api'; -import { Counter, Histogram, Gauge, metrics, trace } from '@opentelemetry/api'; +import { trace } from '@opentelemetry/api'; +import { + MetricsService, + MetricsServiceCounter, + MetricsServiceGauge, + MetricsServiceHistogram, +} from '@backstage/backend-plugin-api/alpha'; import { Knex } from 'knex'; import { Duration } from 'luxon'; import express from 'express'; @@ -45,10 +51,10 @@ export class PluginTaskSchedulerImpl implements SchedulerService { private readonly allScheduledTasks: SchedulerServiceTaskDescriptor[] = []; private readonly shutdownInitiated: Promise; - private readonly counter: Counter; - private readonly duration: Histogram; - private readonly lastStarted: Gauge; - private readonly lastCompleted: Gauge; + private readonly counter: MetricsServiceCounter; + private readonly duration: MetricsServiceHistogram; + private readonly lastStarted: MetricsServiceGauge; + private readonly lastCompleted: MetricsServiceGauge; private readonly pluginId: string; private readonly databaseFactory: () => Promise; @@ -58,24 +64,27 @@ export class PluginTaskSchedulerImpl implements SchedulerService { pluginId: string, databaseFactory: () => Promise, logger: LoggerService, + metrics: MetricsService, rootLifecycle: RootLifecycleService, ) { this.pluginId = pluginId; this.databaseFactory = databaseFactory; this.logger = logger; - const meter = metrics.getMeter('default'); - this.counter = meter.createCounter('backend_tasks.task.runs.count', { + this.counter = metrics.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', - }); - this.lastStarted = meter.createGauge('backend_tasks.task.runs.started', { + this.duration = metrics.createHistogram( + 'backend_tasks.task.runs.duration', + { + description: 'Histogram of task run durations', + unit: 'seconds', + }, + ); + this.lastStarted = metrics.createGauge('backend_tasks.task.runs.started', { description: 'Epoch timestamp seconds when the task was last started', unit: 'seconds', }); - this.lastCompleted = meter.createGauge( + this.lastCompleted = metrics.createGauge( 'backend_tasks.task.runs.completed', { description: 'Epoch timestamp seconds when the task was last completed', diff --git a/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.ts b/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.ts index 186e5f6940..8aacdc0005 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.ts @@ -18,6 +18,7 @@ import { coreServices, createServiceFactory, } from '@backstage/backend-plugin-api'; +import { metricsServiceRef } from '@backstage/backend-plugin-api/alpha'; import { DefaultSchedulerService } from './lib/DefaultSchedulerService'; /** @@ -37,6 +38,7 @@ export const schedulerServiceFactory = createServiceFactory({ rootLifecycle: coreServices.rootLifecycle, httpRouter: coreServices.httpRouter, pluginMetadata: coreServices.pluginMetadata, + metrics: metricsServiceRef, }, async factory({ database, @@ -44,6 +46,7 @@ export const schedulerServiceFactory = createServiceFactory({ rootLifecycle, httpRouter, pluginMetadata, + metrics, }) { return DefaultSchedulerService.create({ database, @@ -51,6 +54,7 @@ export const schedulerServiceFactory = createServiceFactory({ rootLifecycle, httpRouter, pluginMetadata, + metrics, }); }, }); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts deleted file mode 100644 index 1a72d2af73..0000000000 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts +++ /dev/null @@ -1,656 +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 { ConfigReader } from '@backstage/config'; -import { - BitbucketIntegration, - readBitbucketIntegrationConfig, -} from '@backstage/integration'; -import { - createMockDirectory, - mockServices, - registerMswTestHooks, -} from '@backstage/backend-test-utils'; -import fs from 'fs-extra'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import path from 'node:path'; -import { NotModifiedError } from '@backstage/errors'; -import { BitbucketUrlReader } from './BitbucketUrlReader'; -import { DefaultReadTreeResponseFactory } from './tree'; -import getRawBody from 'raw-body'; -import { UrlReaderServiceReadUrlResponse } from '@backstage/backend-plugin-api'; - -const logger = mockServices.logger.mock(); - -describe('BitbucketUrlReader.factory', () => { - it('only apply integration configs not inherited from bitbucketCloud or bitbucketServer', () => { - const config = new ConfigReader({ - integrations: { - bitbucket: [], - bitbucketCloud: [ - { - username: 'username', - appPassword: 'password', - }, - ], - bitbucketServer: [ - { - host: 'bitbucket-server.local', - token: 'test-token', - }, - ], - }, - }); - const treeResponseFactory = DefaultReadTreeResponseFactory.create({ - config: config, - }); - - const tuples = BitbucketUrlReader.factory({ - config, - logger, - treeResponseFactory, - }); - - expect(tuples).toHaveLength(0); - }); -}); - -describe('BitbucketUrlReader', () => { - const mockDir = createMockDirectory({ mockOsTmpDir: true }); - - beforeEach(mockDir.clear); - - const treeResponseFactory = DefaultReadTreeResponseFactory.create({ - config: new ConfigReader({}), - }); - - const bitbucketProcessor = new BitbucketUrlReader( - new BitbucketIntegration( - readBitbucketIntegrationConfig( - new ConfigReader({ - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', - }), - ), - ), - logger, - { treeResponseFactory }, - ); - - const hostedBitbucketProcessor = new BitbucketUrlReader( - new BitbucketIntegration( - readBitbucketIntegrationConfig( - new ConfigReader({ - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', - }), - ), - ), - logger, - { treeResponseFactory }, - ); - - const worker = setupServer(); - registerMswTestHooks(worker); - - describe('readUrl', () => { - it('should be able to readUrl via buffer without ETag', async () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (req, res, ctx) => { - expect(req.headers.get('If-None-Match')).toBeNull(); - return res( - ctx.status(200), - ctx.body('foo'), - ctx.set('ETag', 'etag-value'), - ); - }, - ), - ); - - const result = await bitbucketProcessor.readUrl( - 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', - ); - const buffer = await result.buffer(); - expect(buffer.toString()).toBe('foo'); - }); - - it('should be able to readUrl using provided token', async () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (req, res, ctx) => { - expect(req.headers.get('authorization')).toBe( - 'Bearer manual-token', - ); - return res(ctx.status(200), ctx.body('foo')); - }, - ), - ); - - const result = await bitbucketProcessor.readUrl( - 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', - { token: 'manual-token' }, - ); - const buffer = await result.buffer(); - expect(buffer.toString()).toBe('foo'); - }); - - it('should be able to readUrl via stream without ETag', async () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (req, res, ctx) => { - expect(req.headers.get('If-None-Match')).toBeNull(); - return res( - ctx.status(200), - ctx.body('foo'), - ctx.set('ETag', 'etag-value'), - ); - }, - ), - ); - - const result = await bitbucketProcessor.readUrl( - 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', - ); - const fromStream = await getRawBody(result.stream!()); - expect(fromStream.toString()).toBe('foo'); - }); - - it('should be able to readUrl with matching ETag', async () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (req, res, ctx) => { - expect(req.headers.get('If-None-Match')).toBe( - 'matching-etag-value', - ); - return res(ctx.status(304)); - }, - ), - ); - - await expect( - bitbucketProcessor.readUrl( - 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', - { etag: 'matching-etag-value' }, - ), - ).rejects.toThrow(NotModifiedError); - }); - - it('should be able to readUrl without matching ETag', async () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (req, res, ctx) => { - expect(req.headers.get('If-None-Match')).toBe( - 'previous-etag-value', - ); - return res( - ctx.status(200), - ctx.body('foo'), - ctx.set('ETag', 'new-etag-value'), - ); - }, - ), - ); - - const result = await bitbucketProcessor.readUrl( - 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', - { etag: 'previous-etag-value' }, - ); - const buffer = await result.buffer(); - expect(buffer.toString()).toBe('foo'); - expect(result.etag).toBe('new-etag-value'); - }); - - it('should be able to readUrl via buffer without If-Modified-Since', async () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (req, res, ctx) => { - expect(req.headers.get('If-None-Match')).toBeNull(); - return res( - ctx.status(200), - ctx.body('foo'), - ctx.set('ETag', 'etag-value'), - ctx.set( - 'Last-Modified', - new Date('2020-01-01T00:00:00Z').toUTCString(), - ), - ); - }, - ), - ); - - const result = await bitbucketProcessor.readUrl( - 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', - ); - const buffer = await result.buffer(); - expect(result.lastModifiedAt).toEqual(new Date('2020-01-01T00:00:00Z')); - expect(buffer.toString()).toBe('foo'); - }); - - it('should be throw not modified when If-Modified-Since returns a 304', async () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (req, res, ctx) => { - expect(req.headers.get('If-Modified-Since')).toBe( - new Date('1999 12 31 23:59:59 GMT').toUTCString(), - ); - return res(ctx.status(304)); - }, - ), - ); - - await expect( - bitbucketProcessor.readUrl( - 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', - { lastModifiedAfter: new Date('1999 12 31 23:59:59 GMT') }, - ), - ).rejects.toThrow(NotModifiedError); - }); - - it('should be able to readUrl when If-Modified-Since is before Last-Modified', async () => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage-verification/test-template/src/master/template.yaml', - (req, res, ctx) => { - expect(req.headers.get('If-Modified-Since')).toBe( - new Date('1999 12 31 23:59:59 GMT').toUTCString(), - ); - return res( - ctx.status(200), - ctx.set( - 'Last-Modified', - new Date('2020-01-01T00:00:00Z').toUTCString(), - ), - ctx.body('foo'), - ); - }, - ), - ); - - const result = await bitbucketProcessor.readUrl( - 'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml', - { lastModifiedAfter: new Date('1999 12 31 23:59:59 GMT') }, - ); - const buffer = await result.buffer(); - expect(buffer.toString()).toBe('foo'); - expect(result.lastModifiedAt).toEqual(new Date('2020-01-01T00:00:00Z')); - }); - }); - - describe('read', () => { - it('rejects unknown targets', async () => { - await expect( - bitbucketProcessor.read('https://not.bitbucket.com/apa'), - ).rejects.toThrow( - 'Incorrect URL: https://not.bitbucket.com/apa, Error: Invalid Bitbucket URL or file path', - ); - }); - }); - - describe('readTree', () => { - const repoBuffer = fs.readFileSync( - path.resolve( - __dirname, - '__fixtures__/bitbucket-repo-with-commit-hash.tar.gz', - ), - ); - - const privateBitbucketRepoBuffer = fs.readFileSync( - path.resolve(__dirname, '__fixtures__/bitbucket-server-repo.tar.gz'), - ); - - beforeEach(() => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage/mock', - (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - mainbranch: { - type: 'branch', - name: 'master', - }, - }), - ), - ), - rest.get( - 'https://bitbucket.org/backstage/mock/get/master.tar.gz', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/zip'), - ctx.set( - 'content-disposition', - 'attachment; filename=backstage-mock-12ab34cd56ef.tar.gz', - ), - ctx.body(new Uint8Array(repoBuffer)), - ), - ), - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage/mock/commits/master', - (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], - }), - ), - ), - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/zip'), - ctx.set( - 'content-disposition', - 'attachment; filename=backstage-mock.tgz', - ), - ctx.body(new Uint8Array(privateBitbucketRepoBuffer)), - ), - ), - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits', - (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], - }), - ), - ), - ); - }); - - it('returns the wanted files from an archive', async () => { - const response = await bitbucketProcessor.readTree( - 'https://bitbucket.org/backstage/mock/src/master', - ); - - expect(response.etag).toBe('12ab34cd56ef'); - - const files = await response.files(); - - expect(files.length).toBe(2); - const mkDocsFile = await files[0].content(); - const indexMarkdownFile = await files[1].content(); - - expect(indexMarkdownFile.toString()).toBe('# Test\n'); - expect(mkDocsFile.toString()).toBe('site_name: Test\n'); - }); - - it('creates a directory with the wanted files', async () => { - const response = await bitbucketProcessor.readTree( - 'https://bitbucket.org/backstage/mock', - ); - - const dir = await response.dir({ targetDir: mockDir.path }); - - await expect( - fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'), - ).resolves.toBe('site_name: Test\n'); - await expect( - fs.readFile(path.join(dir, 'docs', 'index.md'), 'utf8'), - ).resolves.toBe('# Test\n'); - }); - - it('uses private bitbucket host', async () => { - const response = await hostedBitbucketProcessor.readTree( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch', - ); - - expect(response.etag).toBe('12ab34cd56ef'); - - const files = await response.files(); - - expect(files.length).toBe(1); - const indexMarkdownFile = await files[0].content(); - - expect(indexMarkdownFile.toString()).toBe('# Test\n'); - }); - - it('returns the wanted files from an archive with a subpath', async () => { - const response = await bitbucketProcessor.readTree( - 'https://bitbucket.org/backstage/mock/src/master/docs', - ); - - expect(response.etag).toBe('12ab34cd56ef'); - - const files = await response.files(); - - expect(files.length).toBe(1); - const indexMarkdownFile = await files[0].content(); - - expect(indexMarkdownFile.toString()).toBe('# Test\n'); - }); - - it('creates a directory with the wanted files with a subpath', async () => { - const response = await bitbucketProcessor.readTree( - 'https://bitbucket.org/backstage/mock/src/master/docs', - ); - - const dir = await response.dir({ targetDir: mockDir.path }); - - await expect( - fs.readFile(path.join(dir, 'index.md'), 'utf8'), - ).resolves.toBe('# Test\n'); - }); - - it('throws a NotModifiedError when given a etag in options', async () => { - const fnBitbucket = async () => { - await bitbucketProcessor.readTree( - 'https://bitbucket.org/backstage/mock', - { etag: '12ab34cd56ef' }, - ); - }; - - await expect(fnBitbucket).rejects.toThrow(NotModifiedError); - }); - - it('should not throw a NotModifiedError when given an outdated etag in options', async () => { - const response = await bitbucketProcessor.readTree( - 'https://bitbucket.org/backstage/mock', - { etag: 'outdatedetag123abc' }, - ); - - expect(response.etag).toBe('12ab34cd56ef'); - }); - }); - - describe('search hosted', () => { - const repoBuffer = fs.readFileSync( - path.resolve( - __dirname, - '__fixtures__/bitbucket-repo-with-commit-hash.tar.gz', - ), - ); - - beforeEach(() => { - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage/mock', - (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - mainbranch: { - type: 'branch', - name: 'master', - }, - }), - ), - ), - rest.get( - 'https://bitbucket.org/backstage/mock/get/master.tar.gz', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/zip'), - ctx.set( - 'content-disposition', - 'attachment; filename=backstage-mock-12ab34cd56ef.tar.gz', - ), - ctx.body(new Uint8Array(repoBuffer)), - ), - ), - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage/mock/commits/master', - (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], - }), - ), - ), - ); - }); - - it('works for the naive case', async () => { - const result = await bitbucketProcessor.search( - 'https://bitbucket.org/backstage/mock/src/master/**/index.*', - ); - expect(result.etag).toBe('12ab34cd56ef'); - expect(result.files.length).toBe(1); - expect(result.files[0].url).toBe( - 'https://bitbucket.org/backstage/mock/src/master/docs/index.md', - ); - await expect(result.files[0].content()).resolves.toEqual( - Buffer.from('# Test\n'), - ); - }); - - it('works in nested folders', async () => { - const result = await bitbucketProcessor.search( - 'https://bitbucket.org/backstage/mock/src/master/docs/index.*', - ); - expect(result.etag).toBe('12ab34cd56ef'); - expect(result.files.length).toBe(1); - expect(result.files[0].url).toBe( - 'https://bitbucket.org/backstage/mock/src/master/docs/index.md', - ); - await expect(result.files[0].content()).resolves.toEqual( - Buffer.from('# Test\n'), - ); - }); - - it('throws NotModifiedError when same etag', async () => { - await expect( - bitbucketProcessor.search( - 'https://bitbucket.org/backstage/mock/src/master/**/index.*', - { etag: '12ab34cd56ef' }, - ), - ).rejects.toThrow(NotModifiedError); - }); - }); - - describe('search private', () => { - const privateBitbucketRepoBuffer = fs.readFileSync( - path.resolve(__dirname, '__fixtures__/bitbucket-server-repo.tar.gz'), - ); - - beforeEach(() => { - worker.use( - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/zip'), - ctx.set( - 'content-disposition', - 'attachment; filename=backstage-mock.tgz', - ), - ctx.body(new Uint8Array(privateBitbucketRepoBuffer)), - ), - ), - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits', - (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], - }), - ), - ), - ); - }); - - it('works for the naive case', async () => { - const result = await hostedBitbucketProcessor.search( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master', - ); - expect(result.etag).toBe('12ab34cd56ef'); - expect(result.files.length).toBe(1); - expect(result.files[0].url).toBe( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master', - ); - await expect(result.files[0].content()).resolves.toEqual( - Buffer.from('# Test\n'), - ); - }); - - it('works in nested folders', async () => { - const result = await hostedBitbucketProcessor.search( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.*?at=master', - ); - expect(result.etag).toBe('12ab34cd56ef'); - expect(result.files.length).toBe(1); - expect(result.files[0].url).toBe( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master', - ); - await expect(result.files[0].content()).resolves.toEqual( - Buffer.from('# Test\n'), - ); - }); - - it('throws NotModifiedError when same etag', async () => { - await expect( - hostedBitbucketProcessor.search( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master', - { etag: '12ab34cd56ef' }, - ), - ).rejects.toThrow(NotModifiedError); - }); - - it('should work for exact URLs', async () => { - hostedBitbucketProcessor.readUrl = jest.fn().mockResolvedValue({ - buffer: async () => Buffer.from('content'), - etag: 'etag', - } as UrlReaderServiceReadUrlResponse); - - const result = await hostedBitbucketProcessor.search( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master', - ); - expect(result.etag).toBe('etag'); - expect(result.files.length).toBe(1); - expect(result.files[0].url).toBe( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master', - ); - expect((await result.files[0].content()).toString()).toEqual('content'); - }); - }); -}); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.ts deleted file mode 100644 index 8c8843d7e9..0000000000 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.ts +++ /dev/null @@ -1,313 +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 { - UrlReaderService, - UrlReaderServiceReadTreeOptions, - UrlReaderServiceReadTreeResponse, - UrlReaderServiceReadUrlOptions, - UrlReaderServiceReadUrlResponse, - UrlReaderServiceSearchOptions, - UrlReaderServiceSearchResponse, -} from '@backstage/backend-plugin-api'; -import { - assertError, - NotFoundError, - NotModifiedError, -} from '@backstage/errors'; -import { - BitbucketIntegration, - getBitbucketDefaultBranch, - getBitbucketDownloadUrl, - getBitbucketFileFetchUrl, - getBitbucketRequestOptions, - ScmIntegrations, -} from '@backstage/integration'; -import parseGitUrl from 'git-url-parse'; -import { trimEnd } from 'lodash'; -import { Minimatch } from 'minimatch'; -import { LoggerService } from '@backstage/backend-plugin-api'; -import { ReaderFactory, ReadTreeResponseFactory } from './types'; -import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; - -/** - * Implements a {@link @backstage/backend-plugin-api#UrlReaderService} for files from Bitbucket v1 and v2 APIs, such - * as the one exposed by Bitbucket Cloud itself. - * - * @public - * @deprecated in favor of BitbucketCloudUrlReader and BitbucketServerUrlReader - */ -export class BitbucketUrlReader implements UrlReaderService { - static factory: ReaderFactory = ({ config, logger, treeResponseFactory }) => { - const integrations = ScmIntegrations.fromConfig(config); - return integrations.bitbucket - .list() - .filter( - item => - !integrations.bitbucketCloud.byHost(item.config.host) && - !integrations.bitbucketServer.byHost(item.config.host), - ) - .map(integration => { - const reader = new BitbucketUrlReader(integration, logger, { - treeResponseFactory, - }); - const predicate = (url: URL) => url.host === integration.config.host; - return { reader, predicate }; - }); - }; - - private readonly integration: BitbucketIntegration; - private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }; - - constructor( - integration: BitbucketIntegration, - logger: LoggerService, - deps: { treeResponseFactory: ReadTreeResponseFactory }, - ) { - this.integration = integration; - this.deps = deps; - const { host, token, username, appPassword } = integration.config; - const replacement = - host === 'bitbucket.org' ? 'bitbucketCloud' : 'bitbucketServer'; - logger.warn( - `[Deprecated] Please migrate from "integrations.bitbucket" to "integrations.${replacement}".`, - ); - - if (!token && username && !appPassword) { - throw new Error( - `Bitbucket integration for '${host}' has configured a username but is missing a required appPassword.`, - ); - } - } - - async read(url: string): Promise { - const response = await this.readUrl(url); - return response.buffer(); - } - - private getCredentials = async (options?: { - token?: string; - }): Promise<{ headers: Record }> => { - if (options?.token) { - return { - headers: { - Authorization: `Bearer ${options.token}`, - }, - }; - } - - return await getBitbucketRequestOptions(this.integration.config); - }; - - async readUrl( - url: string, - options?: UrlReaderServiceReadUrlOptions, - ): Promise { - const { etag, lastModifiedAfter, signal } = options ?? {}; - const bitbucketUrl = getBitbucketFileFetchUrl(url, this.integration.config); - const requestOptions = await this.getCredentials(options); - - let response: Response; - try { - response = await fetch(bitbucketUrl.toString(), { - headers: { - ...requestOptions.headers, - ...(etag && { 'If-None-Match': etag }), - ...(lastModifiedAfter && { - 'If-Modified-Since': lastModifiedAfter.toUTCString(), - }), - }, - // TODO(freben): The signal cast is there because pre-3.x versions of - // node-fetch have a very slightly deviating AbortSignal type signature. - // The difference does not affect us in practice however. The cast can be - // removed after we support ESM for CLI dependencies and migrate to - // version 3 of node-fetch. - // https://github.com/backstage/backstage/issues/8242 - ...(signal && { signal: signal as any }), - }); - } catch (e) { - throw new Error(`Unable to read ${url}, ${e}`); - } - - if (response.status === 304) { - throw new NotModifiedError(); - } - - if (response.ok) { - return ReadUrlResponseFactory.fromResponse(response); - } - - const message = `${url} could not be read as ${bitbucketUrl}, ${response.status} ${response.statusText}`; - if (response.status === 404) { - throw new NotFoundError(message); - } - throw new Error(message); - } - - async readTree( - url: string, - options?: UrlReaderServiceReadTreeOptions, - ): Promise { - const { filepath } = parseGitUrl(url); - - const lastCommitShortHash = await this.getLastCommitShortHash(url); - if (options?.etag && options.etag === lastCommitShortHash) { - throw new NotModifiedError(); - } - - const downloadUrl = await getBitbucketDownloadUrl( - url, - this.integration.config, - ); - const archiveBitbucketResponse = await fetch( - downloadUrl, - getBitbucketRequestOptions(this.integration.config), - ); - if (!archiveBitbucketResponse.ok) { - const message = `Failed to read tree from ${url}, ${archiveBitbucketResponse.status} ${archiveBitbucketResponse.statusText}`; - if (archiveBitbucketResponse.status === 404) { - throw new NotFoundError(message); - } - throw new Error(message); - } - - return await this.deps.treeResponseFactory.fromTarArchive({ - response: archiveBitbucketResponse, - subpath: filepath, - etag: lastCommitShortHash, - filter: options?.filter, - }); - } - - async search( - url: string, - options?: UrlReaderServiceSearchOptions, - ): Promise { - const { filepath } = parseGitUrl(url); - - // If it's a direct URL we use readUrl instead - if (!filepath?.match(/[*?]/)) { - try { - const data = await this.readUrl(url, options); - - return { - files: [ - { - url: url, - content: data.buffer, - lastModifiedAt: data.lastModifiedAt, - }, - ], - etag: data.etag ?? '', - }; - } catch (error) { - assertError(error); - if (error.name === 'NotFoundError') { - return { - files: [], - etag: '', - }; - } - throw error; - } - } - - const matcher = new Minimatch(filepath); - - // TODO(freben): For now, read the entire repo and filter through that. In - // a future improvement, we could be smart and try to deduce that non-glob - // prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used - // to get just that part of the repo. - const treeUrl = trimEnd(url.replace(filepath, ''), '/'); - - const tree = await this.readTree(treeUrl, { - etag: options?.etag, - filter: path => matcher.match(path), - }); - const files = await tree.files(); - - return { - etag: tree.etag, - files: files.map(file => ({ - url: this.integration.resolveUrl({ - url: `/${file.path}`, - base: url, - }), - content: file.content, - lastModifiedAt: file.lastModifiedAt, - })), - }; - } - - toString() { - const { host, token, username, appPassword } = this.integration.config; - let authed = Boolean(token); - if (!authed) { - authed = Boolean(username && appPassword); - } - return `bitbucket{host=${host},authed=${authed}}`; - } - - private async getLastCommitShortHash(url: string): Promise { - const { resource, name: repoName, owner: project, ref } = parseGitUrl(url); - - let branch = ref; - if (!branch) { - branch = await getBitbucketDefaultBranch(url, this.integration.config); - } - - const isHosted = resource === 'bitbucket.org'; - // Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp222 - const commitsApiUrl = isHosted - ? `${this.integration.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}` - : `${this.integration.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`; - - const commitsResponse = await fetch( - commitsApiUrl, - getBitbucketRequestOptions(this.integration.config), - ); - if (!commitsResponse.ok) { - const message = `Failed to retrieve commits from ${commitsApiUrl}, ${commitsResponse.status} ${commitsResponse.statusText}`; - if (commitsResponse.status === 404) { - throw new NotFoundError(message); - } - throw new Error(message); - } - - const commits = await commitsResponse.json(); - if (isHosted) { - if ( - commits && - commits.values && - commits.values.length > 0 && - commits.values[0].hash - ) { - return commits.values[0].hash.substring(0, 12); - } - } else { - if ( - commits && - commits.values && - commits.values.length > 0 && - commits.values[0].id - ) { - return commits.values[0].id.substring(0, 12); - } - } - - throw new Error(`Failed to read response from ${commitsApiUrl}`); - } -} diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/UrlReaders.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/UrlReaders.ts index 229bdd4903..f04a668b0a 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/UrlReaders.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/UrlReaders.ts @@ -24,7 +24,6 @@ import { UrlReaderPredicateMux } from './UrlReaderPredicateMux'; import { AzureUrlReader } from './AzureUrlReader'; import { BitbucketCloudUrlReader } from './BitbucketCloudUrlReader'; import { BitbucketServerUrlReader } from './BitbucketServerUrlReader'; -import { BitbucketUrlReader } from './BitbucketUrlReader'; import { GerritUrlReader } from './GerritUrlReader'; import { GithubUrlReader } from './GithubUrlReader'; import { GitlabUrlReader } from './GitlabUrlReader'; @@ -92,7 +91,6 @@ export class UrlReaders { AzureUrlReader.factory, BitbucketCloudUrlReader.factory, BitbucketServerUrlReader.factory, - BitbucketUrlReader.factory, GerritUrlReader.factory, GithubUrlReader.factory, GiteaUrlReader.factory, diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/index.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/index.ts index fc33efbc3a..3f5d7a7ea5 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/index.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/index.ts @@ -16,7 +16,6 @@ export { AzureUrlReader } from './AzureUrlReader'; export { BitbucketCloudUrlReader } from './BitbucketCloudUrlReader'; -export { BitbucketUrlReader } from './BitbucketUrlReader'; export { BitbucketServerUrlReader } from './BitbucketServerUrlReader'; export { GerritUrlReader } from './GerritUrlReader'; export { GithubUrlReader } from './GithubUrlReader'; diff --git a/packages/backend-dynamic-feature-service/CHANGELOG.md b/packages/backend-dynamic-feature-service/CHANGELOG.md index a9be5cd0ba..85387c2b74 100644 --- a/packages/backend-dynamic-feature-service/CHANGELOG.md +++ b/packages/backend-dynamic-feature-service/CHANGELOG.md @@ -1,5 +1,60 @@ # @backstage/backend-dynamic-feature-service +## 0.8.0-next.1 + +### Minor Changes + +- 0fbcf23: Migrated OpenAPI schemas to 3.1. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.5.0-next.1 + - @backstage/cli-common@0.2.0-next.1 + - @backstage/cli-node@0.2.19-next.1 + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/plugin-events-backend@0.6.0-next.1 + - @backstage/plugin-scaffolder-node@0.13.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-app-node@0.1.43-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + +## 0.7.10-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/cli-node@0.2.19-next.0 + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/plugin-catalog-backend@3.5.0-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-openapi-utils@0.6.7-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-app-node@0.1.43-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-events-backend@0.5.12-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-scaffolder-node@0.12.6-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-search-common@1.2.22 + ## 0.7.9 ### Patch Changes diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index c197ad041c..0e95427ca3 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-dynamic-feature-service", - "version": "0.7.9", + "version": "0.8.0-next.1", "description": "Backstage dynamic feature service", "backstage": { "role": "node-library" diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi.yaml b/packages/backend-dynamic-feature-service/src/schema/openapi.yaml index 9b1871c366..987b59f4b5 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi.yaml +++ b/packages/backend-dynamic-feature-service/src/schema/openapi.yaml @@ -1,4 +1,4 @@ -openapi: 3.0.3 +openapi: 3.1.0 info: title: .backstage/dynamic-features version: '1' diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/Api.server.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/Api.server.ts index 285345d44c..7d241760dc 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/Api.server.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/Api.server.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/index.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/index.ts index 8d81cbaf39..9a0ffed740 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/index.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/index.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/index.ts index dec4b8804e..58fc52487a 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/index.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorError.model.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorError.model.ts index e0265e95d7..853f88cc14 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorRequest.model.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorRequest.model.ts index 3eb5e15740..1591911bc9 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorResponse.model.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorResponse.model.ts index edbcc32df7..1eff0557ed 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ModelError.model.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ModelError.model.ts index 958fde7d0b..9d5c36325e 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ModelError.model.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,6 @@ import { ErrorResponse } from '../models/ErrorResponse.model'; */ export interface ModelError { [key: string]: any; - error: ErrorError; request?: ErrorRequest; response: ErrorResponse; diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/Remote.model.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/Remote.model.ts index 7440110ad8..ff5cae705f 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/Remote.model.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/Remote.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/RemoteInfo.model.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/RemoteInfo.model.ts index 72edba90d4..6e8bdeff0a 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/RemoteInfo.model.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/RemoteInfo.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/index.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/index.ts index 0c54a6585b..4cf296a0b9 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/index.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/router.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/router.ts index cf48bcfc95..b228198c3c 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/generated/router.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/generated/router.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import { createValidatedOpenApiRouterFromGeneratedEndpointMap } from '@backstage import { EndpointMap } from './apis'; export const spec = { - openapi: '3.0.3', + openapi: '3.1.0', info: { title: '.backstage/dynamic-features', version: '1', diff --git a/packages/backend-dynamic-feature-service/src/schema/openapi/index.ts b/packages/backend-dynamic-feature-service/src/schema/openapi/index.ts index 196aad553a..fb49a2b9c0 100644 --- a/packages/backend-dynamic-feature-service/src/schema/openapi/index.ts +++ b/packages/backend-dynamic-feature-service/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md index 58436b3235..ce142b728e 100644 --- a/packages/backend-openapi-utils/CHANGELOG.md +++ b/packages/backend-openapi-utils/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/backend-openapi-utils +## 0.6.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + ## 0.6.6 ### Patch Changes diff --git a/packages/backend-openapi-utils/README.md b/packages/backend-openapi-utils/README.md index 33f2200f81..bb420f49fa 100644 --- a/packages/backend-openapi-utils/README.md +++ b/packages/backend-openapi-utils/README.md @@ -4,6 +4,8 @@ This package is meant to provide a typed Express router for an OpenAPI spec. Based on the [`oatx`](https://github.com/varanauskas/oatx) library and adapted to override Express values. +Only supports OpenAPI 3.1 specifications. + ## Getting Started ### Configuration diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json index 5c6a9c5773..1f5bc417a8 100644 --- a/packages/backend-openapi-utils/package.json +++ b/packages/backend-openapi-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-openapi-utils", - "version": "0.6.6", + "version": "0.6.7-next.0", "description": "OpenAPI typescript support.", "backstage": { "role": "node-library" diff --git a/packages/backend-openapi-utils/src/stub.ts b/packages/backend-openapi-utils/src/stub.ts index 717e2ae19b..b595f357bc 100644 --- a/packages/backend-openapi-utils/src/stub.ts +++ b/packages/backend-openapi-utils/src/stub.ts @@ -54,6 +54,7 @@ export function getOpenApiSpecRoute(baseUrl: string) { /** * Create a router with validation middleware. This is used by typing methods to create an * "OpenAPI router" with all of the expected validation + metadata. + * Only supports OpenAPI 3.1 specifications. * @param spec - Your OpenAPI spec imported as a JSON object. * @param validatorOptions - `openapi-express-validator` options to override the defaults. * @returns A new express router with validation middleware. @@ -115,6 +116,7 @@ function createRouterWithValidation( /** * Create a new OpenAPI router with some default middleware. + * Only supports OpenAPI 3.1 specifications. * @param spec - Your OpenAPI spec imported as a JSON object. * @param validatorOptions - `openapi-express-validator` options to override the defaults. * @returns A new express router with validation middleware. @@ -132,6 +134,7 @@ export function createValidatedOpenApiRouter( /** * Create a new OpenAPI router with some default middleware. + * Only supports OpenAPI 3.1 specifications. * @param spec - Your OpenAPI spec imported as a JSON object. * @param validatorOptions - `openapi-express-validator` options to override the defaults. * @returns A new express router with validation middleware. diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md index 94768add54..b100b47e26 100644 --- a/packages/backend-plugin-api/CHANGELOG.md +++ b/packages/backend-plugin-api/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/backend-plugin-api +## 1.7.1-next.0 + +### Patch Changes + +- 1ee5b28: Adds an alpha `MetricsService` to provide a unified interface for metrics instrumentation across Backstage plugins. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + ## 1.7.0 ### Minor Changes diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index 131f24bafb..6acc8a384b 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-plugin-api", - "version": "1.7.0", + "version": "1.7.1-next.0", "description": "Core API used by Backstage backend plugins", "backstage": { "role": "node-library" diff --git a/packages/backend-plugin-api/report-alpha.api.md b/packages/backend-plugin-api/report-alpha.api.md index e06e0c45d2..7d6786b97e 100644 --- a/packages/backend-plugin-api/report-alpha.api.md +++ b/packages/backend-plugin-api/report-alpha.api.md @@ -103,6 +103,150 @@ export const actionsServiceRef: ServiceRef< 'singleton' >; +// @alpha +export interface MetricAdvice { + explicitBucketBoundaries?: number[]; +} + +// @alpha +export interface MetricAttributes { + // (undocumented) + [attributeKey: string]: MetricAttributeValue | undefined; +} + +// @alpha +export type MetricAttributeValue = + | string + | number + | boolean + | Array + | Array + | Array; + +// @alpha +export interface MetricOptions { + advice?: MetricAdvice; + description?: string; + unit?: string; +} + +// @alpha +export interface MetricsService { + createCounter( + name: string, + opts?: MetricOptions, + ): MetricsServiceCounter; + createGauge( + name: string, + opts?: MetricOptions, + ): MetricsServiceGauge; + createHistogram( + name: string, + opts?: MetricOptions, + ): MetricsServiceHistogram; + createObservableCounter< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableCounter; + createObservableGauge< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableGauge; + createObservableUpDownCounter< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableUpDownCounter; + createUpDownCounter( + name: string, + opts?: MetricOptions, + ): MetricsServiceUpDownCounter; +} + +// @alpha +export interface MetricsServiceCounter< + TAttributes extends MetricAttributes = MetricAttributes, +> { + // (undocumented) + add(value: number, attributes?: TAttributes): void; +} + +// @alpha +export interface MetricsServiceGauge< + TAttributes extends MetricAttributes = MetricAttributes, +> { + // (undocumented) + record(value: number, attributes?: TAttributes): void; +} + +// @alpha +export interface MetricsServiceHistogram< + TAttributes extends MetricAttributes = MetricAttributes, +> { + // (undocumented) + record(value: number, attributes?: TAttributes): void; +} + +// @alpha +export interface MetricsServiceObservable< + TAttributes extends MetricAttributes = MetricAttributes, +> { + // (undocumented) + addCallback(callback: MetricsServiceObservableCallback): void; + // (undocumented) + removeCallback(callback: MetricsServiceObservableCallback): void; +} + +// @alpha +export type MetricsServiceObservableCallback< + TAttributes extends MetricAttributes = MetricAttributes, +> = ( + observableResult: MetricsServiceObservableResult, +) => void | Promise; + +// @alpha +export type MetricsServiceObservableCounter< + TAttributes extends MetricAttributes = MetricAttributes, +> = MetricsServiceObservable; + +// @alpha +export type MetricsServiceObservableGauge< + TAttributes extends MetricAttributes = MetricAttributes, +> = MetricsServiceObservable; + +// @alpha +export interface MetricsServiceObservableResult< + TAttributes extends MetricAttributes = MetricAttributes, +> { + // (undocumented) + observe(value: number, attributes?: TAttributes): void; +} + +// @alpha +export type MetricsServiceObservableUpDownCounter< + TAttributes extends MetricAttributes = MetricAttributes, +> = MetricsServiceObservable; + +// @alpha +export const metricsServiceRef: ServiceRef< + MetricsService, + 'plugin', + 'singleton' +>; + +// @alpha +export interface MetricsServiceUpDownCounter< + TAttributes extends MetricAttributes = MetricAttributes, +> { + // (undocumented) + add(value: number, attributes?: TAttributes): void; +} + // @public (undocumented) export interface RootSystemMetadataService { // (undocumented) diff --git a/packages/backend-plugin-api/src/alpha/MetricsService.ts b/packages/backend-plugin-api/src/alpha/MetricsService.ts new file mode 100644 index 0000000000..1c92ea9e09 --- /dev/null +++ b/packages/backend-plugin-api/src/alpha/MetricsService.ts @@ -0,0 +1,273 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Attribute values that can be attached to metric measurements. + * + * @alpha + */ +export type MetricAttributeValue = + | string + | number + | boolean + | Array + | Array + | Array; + +/** + * A set of key-value pairs that can be attached to metric measurements. + * + * @alpha + */ +export interface MetricAttributes { + [attributeKey: string]: MetricAttributeValue | undefined; +} + +/** + * Advisory options that influence aggregation configuration. + * + * @alpha + */ +export interface MetricAdvice { + /** + * Hint the explicit bucket boundaries for histogram aggregation. + */ + explicitBucketBoundaries?: number[]; +} + +/** + * Options for creating a metric instrument. + * + * @alpha + */ +export interface MetricOptions { + /** + * The description of the Metric. + */ + description?: string; + /** + * The unit of the Metric values. + */ + unit?: string; + /** + * Advisory options that influence aggregation configuration. + */ + advice?: MetricAdvice; +} + +/** + * A counter metric that only supports non-negative increments. + * + * @alpha + */ +export interface MetricsServiceCounter< + TAttributes extends MetricAttributes = MetricAttributes, +> { + add(value: number, attributes?: TAttributes): void; +} + +/** + * A counter metric that supports both positive and negative increments. + * + * @alpha + */ +export interface MetricsServiceUpDownCounter< + TAttributes extends MetricAttributes = MetricAttributes, +> { + add(value: number, attributes?: TAttributes): void; +} + +/** + * A histogram metric for recording distributions of values. + * + * @alpha + */ +export interface MetricsServiceHistogram< + TAttributes extends MetricAttributes = MetricAttributes, +> { + record(value: number, attributes?: TAttributes): void; +} + +/** + * A gauge metric for recording instantaneous values. + * + * @alpha + */ +export interface MetricsServiceGauge< + TAttributes extends MetricAttributes = MetricAttributes, +> { + record(value: number, attributes?: TAttributes): void; +} + +/** + * The result object passed to observable metric callbacks. + * + * @alpha + */ +export interface MetricsServiceObservableResult< + TAttributes extends MetricAttributes = MetricAttributes, +> { + observe(value: number, attributes?: TAttributes): void; +} + +/** + * A callback function for observable metrics. Called whenever a metric + * collection is initiated. + * + * @alpha + */ +export type MetricsServiceObservableCallback< + TAttributes extends MetricAttributes = MetricAttributes, +> = ( + observableResult: MetricsServiceObservableResult, +) => void | Promise; + +/** + * An observable metric instrument that reports values via callbacks. + * + * @alpha + */ +export interface MetricsServiceObservable< + TAttributes extends MetricAttributes = MetricAttributes, +> { + addCallback(callback: MetricsServiceObservableCallback): void; + removeCallback(callback: MetricsServiceObservableCallback): void; +} + +/** + * An observable counter metric that reports non-negative sums via callbacks. + * + * @alpha + */ +export type MetricsServiceObservableCounter< + TAttributes extends MetricAttributes = MetricAttributes, +> = MetricsServiceObservable; + +/** + * An observable counter metric that reports sums that can go up or down + * via callbacks. + * + * @alpha + */ +export type MetricsServiceObservableUpDownCounter< + TAttributes extends MetricAttributes = MetricAttributes, +> = MetricsServiceObservable; + +/** + * An observable gauge metric that reports instantaneous values via callbacks. + * + * @alpha + */ +export type MetricsServiceObservableGauge< + TAttributes extends MetricAttributes = MetricAttributes, +> = MetricsServiceObservable; + +/** + * A service that provides a facility for emitting metrics. + * + * @alpha + */ +export interface MetricsService { + /** + * Creates a new counter metric. + * + * @param name - The name of the metric. + * @param opts - The options for the metric. + * @returns The counter metric. + */ + createCounter( + name: string, + opts?: MetricOptions, + ): MetricsServiceCounter; + + /** + * Creates a new up-down counter metric. + * + * @param name - The name of the metric. + * @param opts - The options for the metric. + * @returns The up-down counter metric. + */ + createUpDownCounter( + name: string, + opts?: MetricOptions, + ): MetricsServiceUpDownCounter; + + /** + * Creates a new histogram metric. + * + * @param name - The name of the metric. + * @param opts - The options for the metric. + * @returns The histogram metric. + */ + createHistogram( + name: string, + opts?: MetricOptions, + ): MetricsServiceHistogram; + + /** + * Creates a new gauge metric. + * + * @param name - The name of the metric. + * @param opts - The options for the metric. + * @returns The gauge metric. + */ + createGauge( + name: string, + opts?: MetricOptions, + ): MetricsServiceGauge; + + /** + * Creates a new observable counter metric. + * + * @param name - The name of the metric. + * @param opts - The options for the metric. + * @returns The observable counter metric. + */ + createObservableCounter< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableCounter; + + /** + * Creates a new observable up-down counter metric. + * + * @param name - The name of the metric. + * @param opts - The options for the metric. + * @returns The observable up-down counter metric. + */ + createObservableUpDownCounter< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableUpDownCounter; + + /** + * Creates a new observable gauge metric. + * + * @param name - The name of the metric. + * @param opts - The options for the metric. + * @returns The observable gauge metric. + */ + createObservableGauge< + TAttributes extends MetricAttributes = MetricAttributes, + >( + name: string, + opts?: MetricOptions, + ): MetricsServiceObservableGauge; +} diff --git a/packages/backend-plugin-api/src/alpha/index.ts b/packages/backend-plugin-api/src/alpha/index.ts index 56f02f5275..c487686338 100644 --- a/packages/backend-plugin-api/src/alpha/index.ts +++ b/packages/backend-plugin-api/src/alpha/index.ts @@ -27,8 +27,27 @@ export type { export type { ActionsService, ActionsServiceAction } from './ActionsService'; +export type { + MetricsService, + MetricAdvice, + MetricAttributes, + MetricAttributeValue, + MetricOptions, + MetricsServiceCounter, + MetricsServiceUpDownCounter, + MetricsServiceHistogram, + MetricsServiceGauge, + MetricsServiceObservable, + MetricsServiceObservableCallback, + MetricsServiceObservableCounter, + MetricsServiceObservableGauge, + MetricsServiceObservableResult, + MetricsServiceObservableUpDownCounter, +} from './MetricsService'; + export { actionsRegistryServiceRef, actionsServiceRef, + metricsServiceRef, rootSystemMetadataServiceRef, } from './refs'; diff --git a/packages/backend-plugin-api/src/alpha/refs.ts b/packages/backend-plugin-api/src/alpha/refs.ts index a890271364..bd87ea8467 100644 --- a/packages/backend-plugin-api/src/alpha/refs.ts +++ b/packages/backend-plugin-api/src/alpha/refs.ts @@ -56,3 +56,14 @@ export const rootSystemMetadataServiceRef = createServiceRef< id: 'alpha.core.rootSystemMetadata', scope: 'root', }); + +/** + * Service for managing metrics. + * + * @alpha + */ +export const metricsServiceRef = createServiceRef< + import('./MetricsService').MetricsService +>({ + id: 'alpha.core.metrics', +}); diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 0b144df1da..e372736ab0 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/backend-test-utils +## 1.11.1-next.1 + +### Patch Changes + +- 62f0a53: Fixed error forwarding in the actions registry so that known errors like `InputError` and `NotFoundError` thrown by actions preserve their original status codes and messages instead of being wrapped in `ForwardedError` and coerced to 500. +- Updated dependencies + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/backend-app-api@1.5.1-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + +## 1.11.1-next.0 + +### Patch Changes + +- 1ee5b28: Adds a new metrics service mock to be leveraged in tests +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/backend-app-api@1.5.1-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-events-node@0.4.20-next.0 + - @backstage/plugin-permission-common@0.9.6 + ## 1.11.0 ### Minor Changes diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index ee65ddbc78..c8a6fe812d 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-test-utils", - "version": "1.11.0", + "version": "1.11.1-next.1", "description": "Test helpers library for Backstage backends", "backstage": { "role": "node-library" diff --git a/packages/backend-test-utils/report-alpha.api.md b/packages/backend-test-utils/report-alpha.api.md index ca6f47191d..b6bf9e883d 100644 --- a/packages/backend-test-utils/report-alpha.api.md +++ b/packages/backend-test-utils/report-alpha.api.md @@ -12,6 +12,7 @@ import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { LoggerService } from '@backstage/backend-plugin-api'; +import { MetricsService } from '@backstage/backend-plugin-api/alpha'; import { ServiceFactory } from '@backstage/backend-plugin-api'; // @alpha (undocumented) @@ -43,6 +44,16 @@ export namespace actionsServiceMock { ) => ServiceMock; } +// @alpha (undocumented) +export namespace metricsServiceMock { + const // (undocumented) + factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; +} + // @alpha export class MockActionsRegistry implements ActionsRegistryService, ActionsService diff --git a/packages/backend-test-utils/src/alpha/services/MetricsServiceMock.ts b/packages/backend-test-utils/src/alpha/services/MetricsServiceMock.ts new file mode 100644 index 0000000000..301d47fb2e --- /dev/null +++ b/packages/backend-test-utils/src/alpha/services/MetricsServiceMock.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createServiceMock } from './alphaCreateServiceMock'; +import { + MetricsService, + metricsServiceRef, +} from '@backstage/backend-plugin-api/alpha'; +import { metricsServiceFactory } from '@backstage/backend-defaults/alpha'; + +/** + * @alpha + */ +export namespace metricsServiceMock { + export const factory = () => metricsServiceFactory; + + export const mock = createServiceMock( + metricsServiceRef, + () => ({ + createCounter: jest.fn().mockImplementation(() => ({ + add: jest.fn(), + })), + createUpDownCounter: jest.fn().mockImplementation(() => ({ + add: jest.fn(), + })), + createHistogram: jest.fn().mockImplementation(() => ({ + record: jest.fn(), + })), + createGauge: jest.fn().mockImplementation(() => ({ + record: jest.fn(), + })), + createObservableCounter: jest.fn().mockImplementation(() => ({ + addCallback: jest.fn(), + removeCallback: jest.fn(), + })), + createObservableUpDownCounter: jest.fn().mockImplementation(() => ({ + addCallback: jest.fn(), + removeCallback: jest.fn(), + })), + createObservableGauge: jest.fn().mockImplementation(() => ({ + addCallback: jest.fn(), + removeCallback: jest.fn(), + })), + }), + ); +} diff --git a/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts b/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts index 94e5987afd..384a5e8e95 100644 --- a/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts +++ b/packages/backend-test-utils/src/alpha/services/MockActionsRegistry.ts @@ -17,7 +17,7 @@ import { BackstageCredentials, LoggerService, } from '@backstage/backend-plugin-api'; -import { ForwardedError, InputError, NotFoundError } from '@backstage/errors'; +import { InputError, NotFoundError } from '@backstage/errors'; import { JsonObject, JsonValue } from '@backstage/types'; import { z, AnyZodObject } from 'zod'; import zodToJsonSchema from 'zod-to-json-schema'; @@ -126,31 +126,24 @@ export class MockActionsRegistry throw new InputError(`Invalid input to action "${opts.id}"`, input.error); } - try { - const result = await action.action({ - input: input.data, - credentials: opts.credentials ?? mockCredentials.none(), - logger: this.logger, - }); + const result = await action.action({ + input: input.data, + credentials: opts.credentials ?? mockCredentials.none(), + logger: this.logger, + }); - const output = action.schema?.output - ? action.schema.output(z).safeParse(result?.output) - : ({ success: true, data: result?.output } as const); + const output = action.schema?.output + ? action.schema.output(z).safeParse(result?.output) + : ({ success: true, data: result?.output } as const); - if (!output.success) { - throw new InputError( - `Invalid output from action "${opts.id}"`, - output.error, - ); - } - - return { output: output.data }; - } catch (error) { - throw new ForwardedError( - `Failed execution of action "${opts.id}"`, - error, + if (!output.success) { + throw new InputError( + `Invalid output from action "${opts.id}"`, + output.error, ); } + + return { output: output.data }; } register< diff --git a/packages/backend-test-utils/src/alpha/services/index.ts b/packages/backend-test-utils/src/alpha/services/index.ts index ac92033ff8..527bb1822f 100644 --- a/packages/backend-test-utils/src/alpha/services/index.ts +++ b/packages/backend-test-utils/src/alpha/services/index.ts @@ -17,4 +17,5 @@ export { actionsRegistryServiceMock } from './ActionsRegistryServiceMock'; export { MockActionsRegistry } from './MockActionsRegistry'; export { actionsServiceMock } from './ActionsServiceMock'; +export { metricsServiceMock } from './MetricsServiceMock'; export { type ServiceMock } from './alphaCreateServiceMock'; diff --git a/packages/backend-test-utils/src/wiring/TestBackend.ts b/packages/backend-test-utils/src/wiring/TestBackend.ts index 43a63a49e5..11decb883d 100644 --- a/packages/backend-test-utils/src/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/wiring/TestBackend.ts @@ -43,6 +43,7 @@ import { HostDiscovery } from '@backstage/backend-defaults/discovery'; import { actionsRegistryServiceMock, actionsServiceMock, + metricsServiceMock, } from '../alpha/services'; /** @public */ @@ -92,6 +93,7 @@ export const defaultServiceFactories = [ // Alpha services actionsRegistryServiceMock.factory(), actionsServiceMock.factory(), + metricsServiceMock.factory(), ]; /** diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 27ef95a0f3..8e744ff611 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,89 @@ # example-backend +## 0.0.48-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.5.0-next.1 + - @backstage/plugin-auth-backend@0.27.1-next.1 + - @backstage/plugin-techdocs-backend@2.1.6-next.1 + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/plugin-scaffolder-backend@3.2.0-next.1 + - @backstage/plugin-mcp-actions-backend@0.1.10-next.1 + - @backstage/plugin-events-backend@0.6.0-next.1 + - @backstage/plugin-search-backend@2.1.0-next.1 + - @backstage/plugin-auth-node@0.6.14-next.1 + - @backstage/plugin-kubernetes-backend@0.21.2-next.1 + - @backstage/plugin-search-backend-module-catalog@0.3.13-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.4.12-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-app-backend@0.5.12-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.5.1-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.17-next.0 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.5-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.12-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.2.20-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.18-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.9-next.0 + - @backstage/plugin-devtools-backend@0.5.15-next.0 + - @backstage/plugin-events-backend-module-google-pubsub@0.2.1-next.0 + - @backstage/plugin-notifications-backend@0.6.3-next.0 + - @backstage/plugin-permission-backend@0.7.10-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.17-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-proxy-backend@0.6.11-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.7-next.1 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.20-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.8.1-next.0 + - @backstage/plugin-search-backend-module-explore@0.3.12-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-signals-backend@0.3.13-next.0 + +## 0.0.48-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/plugin-auth-backend@0.27.1-next.0 + - @backstage/plugin-catalog-backend@3.5.0-next.0 + - @backstage/plugin-scaffolder-backend@3.1.4-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/plugin-mcp-actions-backend@0.1.10-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/plugin-app-backend@0.5.12-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.5.1-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.17-next.0 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.5-next.0 + - @backstage/plugin-auth-node@0.6.14-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.12-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.2.20-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.18-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.9-next.0 + - @backstage/plugin-devtools-backend@0.5.15-next.0 + - @backstage/plugin-events-backend@0.5.12-next.0 + - @backstage/plugin-events-backend-module-google-pubsub@0.2.1-next.0 + - @backstage/plugin-kubernetes-backend@0.21.2-next.0 + - @backstage/plugin-notifications-backend@0.6.3-next.0 + - @backstage/plugin-permission-backend@0.7.10-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.17-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-node@0.10.11-next.0 + - @backstage/plugin-proxy-backend@0.6.11-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.9.7-next.0 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.20-next.0 + - @backstage/plugin-search-backend@2.0.13-next.0 + - @backstage/plugin-search-backend-module-catalog@0.3.13-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.8.1-next.0 + - @backstage/plugin-search-backend-module-explore@0.3.12-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.4.12-next.0 + - @backstage/plugin-search-backend-node@1.4.2-next.0 + - @backstage/plugin-signals-backend@0.3.13-next.0 + - @backstage/plugin-techdocs-backend@2.1.6-next.0 + ## 0.0.47 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index b6b97f214e..01de1d1626 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.0.47", + "version": "0.0.48-next.1", "backstage": { "role": "backend" }, diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 563a0ef828..62b214a403 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/catalog-client +## 1.14.0-next.1 + +### Minor Changes + +- 972f686: Added support for the `query` field in `getEntitiesByRefs` requests, enabling predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. +- 56c908e: Added support for the `query` field in `getEntityFacets` requests, enabling predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators. +- 0fbcf23: Migrated OpenAPI schemas to 3.1. +- 51e23eb: Added predicate-based entity filtering via POST /entities/by-query endpoint. + + Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$hasPrefix`, and (partially) `$contains` operators for expressive entity queries. Integrated into the existing `queryEntities` flow with full cursor-based pagination, permission enforcement, and `totalItems` support. + + The catalog client's `queryEntities()` method automatically routes to the POST endpoint when a `query` predicate is provided. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + +## 1.13.1-next.0 + +### Patch Changes + +- d2494d6: Minor update to catalog client docs +- Updated dependencies + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + - @backstage/filter-predicates@0.1.0 + ## 1.13.0 ### Minor Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 1a299182e8..20a68905a0 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-client", - "version": "1.13.0", + "version": "1.14.0-next.1", "description": "An isomorphic client for the catalog backend", "backstage": { "role": "common-library" diff --git a/packages/catalog-client/report.api.md b/packages/catalog-client/report.api.md index 58fc4bc8a1..c155084ccd 100644 --- a/packages/catalog-client/report.api.md +++ b/packages/catalog-client/report.api.md @@ -7,8 +7,8 @@ import type { AnalyzeLocationRequest } from '@backstage/plugin-catalog-common'; import type { AnalyzeLocationResponse } from '@backstage/plugin-catalog-common'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; -import { FilterPredicate } from '@backstage/filter-predicates'; -import { SerializedError } from '@backstage/errors'; +import type { FilterPredicate } from '@backstage/filter-predicates'; +import type { SerializedError } from '@backstage/errors'; // @public export type AddLocationRequest = { @@ -236,6 +236,7 @@ export interface GetEntitiesByRefsRequest { entityRefs: string[]; fields?: EntityFieldsQuery | undefined; filter?: EntityFilterQuery; + query?: FilterPredicate; } // @public @@ -280,6 +281,7 @@ export interface GetEntityAncestorsResponse { export interface GetEntityFacetsRequest { facets: string[]; filter?: EntityFilterQuery; + query?: FilterPredicate; } // @public @@ -320,6 +322,7 @@ export type QueryEntitiesInitialRequest = { limit?: number; offset?: number; filter?: EntityFilterQuery; + query?: FilterPredicate; orderFields?: EntityOrderQuery; fullTextFilter?: { term: string; diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index d62323852e..70265bd6cb 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -302,6 +302,103 @@ describe('CatalogClient', () => { expect(response).toEqual({ items: [entity, undefined] }); }); + + it('sends only query predicate in the body when query is provided without filter', async () => { + expect.assertions(3); + const entity = { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'Test2', + namespace: 'test1', + }, + }; + server.use( + rest.post(`${mockBaseUrl}/entities/by-refs`, async (req, res, ctx) => { + expect(req.url.search).toBe(''); + await expect(req.json()).resolves.toEqual({ + entityRefs: ['k:n/a'], + query: { kind: 'Component' }, + }); + return res(ctx.json({ items: [entity] })); + }), + ); + + const response = await client.getEntitiesByRefs( + { + entityRefs: ['k:n/a'], + query: { kind: 'Component' }, + }, + { token }, + ); + + expect(response).toEqual({ items: [entity] }); + }); + + it('merges filter and query into $all predicate when both are provided', async () => { + expect.assertions(4); + const entity = { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'Test2', + namespace: 'test1', + }, + }; + server.use( + rest.post(`${mockBaseUrl}/entities/by-refs`, async (req, res, ctx) => { + expect(req.url.search).toBe(''); + const body = await req.json(); + expect(body.entityRefs).toEqual(['k:n/a']); + expect(body.query).toEqual({ + $all: [{ kind: 'Component' }, { kind: 'API' }], + }); + return res(ctx.json({ items: [entity] })); + }), + ); + + const response = await client.getEntitiesByRefs( + { + entityRefs: ['k:n/a'], + query: { kind: 'Component' }, + filter: { kind: ['API'] }, + }, + { token }, + ); + + expect(response).toEqual({ items: [entity] }); + }); + + it('sends filter as query parameter when only filter is provided (backward compat)', async () => { + expect.assertions(4); + const entity = { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'Test2', + namespace: 'test1', + }, + }; + server.use( + rest.post(`${mockBaseUrl}/entities/by-refs`, async (req, res, ctx) => { + expect(req.url.search).toBe('?filter=kind%3DAPI'); + const body = await req.json(); + expect(body).toEqual({ entityRefs: ['k:n/a'] }); + expect(body.query).toBeUndefined(); + return res(ctx.json({ items: [entity] })); + }), + ); + + const response = await client.getEntitiesByRefs( + { + entityRefs: ['k:n/a'], + filter: { kind: ['API'] }, + }, + { token }, + ); + + expect(response).toEqual({ items: [entity] }); + }); }); describe('queryEntities', () => { @@ -540,6 +637,350 @@ describe('CatalogClient', () => { }); }); + describe('queryEntities with predicate-based queries (POST endpoint)', () => { + const defaultResponse = { + items: [ + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'service-1', + namespace: 'default', + }, + spec: { + type: 'service', + owner: 'team-a', + }, + }, + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'service-2', + namespace: 'default', + }, + spec: { + type: 'service', + owner: 'team-b', + }, + }, + ], + totalItems: 2, + pageInfo: {}, + }; + + it('should use POST endpoint when query is provided', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.method).toBe('POST'); + expect(req.body).toMatchObject({ + query: { kind: 'component' }, + limit: 20, + }); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + const response = await client.queryEntities({ + query: { kind: 'component' }, + limit: 20, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + expect(response.items).toEqual(defaultResponse.items); + expect(response.totalItems).toBe(2); + }); + + it('should support $all operator', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body).toMatchObject({ + query: { + $all: [{ kind: 'component' }, { 'spec.type': 'service' }], + }, + }); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { + $all: [{ kind: 'component' }, { 'spec.type': 'service' }], + }, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should support $any operator', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body).toMatchObject({ + query: { + $any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }], + }, + }); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { + $any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }], + }, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should support $not operator', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body).toMatchObject({ + query: { + $not: { 'spec.lifecycle': 'experimental' }, + }, + }); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { + $not: { 'spec.lifecycle': 'experimental' }, + }, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should support $exists operator', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body).toMatchObject({ + query: { + 'spec.owner': { $exists: true }, + }, + }); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { + 'spec.owner': { $exists: true }, + }, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should support $in operator', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body).toMatchObject({ + query: { + 'spec.owner': { $in: ['team-a', 'team-b', 'team-c'] }, + }, + }); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { + 'spec.owner': { $in: ['team-a', 'team-b', 'team-c'] }, + }, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should support complex nested predicates', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body).toMatchObject({ + query: { + $all: [ + { kind: 'component' }, + { + $any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }], + }, + { + $not: { + 'spec.lifecycle': 'experimental', + }, + }, + ], + }, + }); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { + $all: [ + { kind: 'component' }, + { + $any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }], + }, + { + $not: { + 'spec.lifecycle': 'experimental', + }, + }, + ], + }, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should send orderFields with correct format', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body.orderBy).toEqual([ + { field: 'metadata.name', order: 'asc' }, + ]); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { kind: 'component' }, + orderFields: { field: 'metadata.name', order: 'asc' }, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should send multiple orderFields with correct format', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body.orderBy).toEqual([ + { field: 'metadata.name', order: 'asc' }, + { field: 'spec.type', order: 'desc' }, + ]); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { kind: 'component' }, + orderFields: [ + { field: 'metadata.name', order: 'asc' }, + { field: 'spec.type', order: 'desc' }, + ], + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should send limit and offset parameters in the body', async () => { + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.body.limit).toBe(50); + return res(ctx.json(defaultResponse)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await client.queryEntities({ + query: { kind: 'component' }, + limit: 50, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + }); + + it('should paginate using POST when cursor contains a query', async () => { + // Simulate a cursor that contains a query predicate (as the server would encode it) + const cursorPayload = Buffer.from( + JSON.stringify({ + orderFields: [], + orderFieldValues: [], + isPrevious: false, + query: { kind: 'component' }, + totalItems: 100, + }), + ).toString('base64'); + + const page2Response = { + items: [ + { + apiVersion: '1', + kind: 'Component', + metadata: { name: 'service-3', namespace: 'default' }, + }, + ], + totalItems: 100, + pageInfo: {}, + }; + + const mockedEndpoint = jest.fn().mockImplementation((req, res, ctx) => { + expect(req.method).toBe('POST'); + expect(req.body).toMatchObject({ cursor: cursorPayload }); + return res(ctx.json(page2Response)); + }); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + const response = await client.queryEntities({ + cursor: cursorPayload, + }); + + expect(mockedEndpoint).toHaveBeenCalledTimes(1); + expect(response.items).toEqual(page2Response.items); + expect(response.totalItems).toBe(100); + }); + + it('should use GET endpoint for cursor without query', async () => { + // A cursor that does NOT contain a query field should go to GET + const cursorPayload = Buffer.from( + JSON.stringify({ + orderFields: [], + orderFieldValues: [], + isPrevious: false, + totalItems: 50, + }), + ).toString('base64'); + + const mockedGetEndpoint = jest.fn().mockImplementation((_req, res, ctx) => + res( + ctx.json({ + items: [], + totalItems: 50, + pageInfo: {}, + }), + ), + ); + + const mockedPostEndpoint = jest.fn(); + + server.use( + rest.get(`${mockBaseUrl}/entities/by-query`, mockedGetEndpoint), + rest.post(`${mockBaseUrl}/entities/by-query`, mockedPostEndpoint), + ); + + await client.queryEntities({ cursor: cursorPayload }); + + expect(mockedGetEndpoint).toHaveBeenCalledTimes(1); + expect(mockedPostEndpoint).not.toHaveBeenCalled(); + }); + + it('should handle errors from POST endpoint', async () => { + const mockedEndpoint = jest + .fn() + .mockImplementation((_req, res, ctx) => res(ctx.status(400))); + + server.use(rest.post(`${mockBaseUrl}/entities/by-query`, mockedEndpoint)); + + await expect(() => + client.queryEntities({ query: { kind: 'component' } }), + ).rejects.toThrow(/Request failed with 400/); + }); + }); + describe('streamEntities', () => { const defaultResponse: QueryEntitiesResponse = { items: [ diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index b45bef8bbd..a671a155d3 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -20,7 +20,8 @@ import { parseEntityRef, stringifyLocationRef, } from '@backstage/catalog-model'; -import { ResponseError } from '@backstage/errors'; +import { InputError, ResponseError } from '@backstage/errors'; +import { FilterPredicate } from '@backstage/filter-predicates'; import { AddLocationRequest, AddLocationResponse, @@ -46,10 +47,17 @@ import { StreamEntitiesRequest, ValidateEntityResponse, } from './types/api'; -import { isQueryEntitiesInitialRequest, splitRefsIntoChunks } from './utils'; +import { + convertFilterToPredicate, + isQueryEntitiesInitialRequest, + splitRefsIntoChunks, + cursorContainsQuery, +} from './utils'; import { DefaultApiClient, + GetEntitiesByQuery, GetLocationsByQueryRequest, + QueryEntitiesByPredicateRequest, TypedResponse, } from './schema/openapi'; import type { @@ -229,11 +237,36 @@ export class CatalogClient implements CatalogApi { request: GetEntitiesByRefsRequest, options?: CatalogRequestOptions, ): Promise { + const { filter, query } = request; + + // Only convert and merge if both filter and query are provided, or if + // query alone is provided. When only filter is given, preserve the old + // query-parameter behavior for backward compatibility. + let filterPredicate: FilterPredicate | undefined; + if (query !== undefined) { + if (typeof query !== 'object' || query === null || Array.isArray(query)) { + throw new InputError('Query must be an object'); + } + filterPredicate = query; + if (filter !== undefined) { + const converted = convertFilterToPredicate(filter); + filterPredicate = { $all: [filterPredicate, converted] }; + } + } + const getOneChunk = async (refs: string[]) => { const response = await this.apiClient.getEntitiesByRefs( { - body: { entityRefs: refs, fields: request.fields }, - query: { filter: this.getFilterValue(request.filter) }, + body: { + entityRefs: refs, + fields: request.fields, + ...(filterPredicate && { + query: filterPredicate as unknown as { [key: string]: any }, + }), + }, + query: filterPredicate + ? {} + : { filter: this.getFilterValue(request.filter) }, }, options, ); @@ -266,11 +299,26 @@ export class CatalogClient implements CatalogApi { request: QueryEntitiesRequest = {}, options?: CatalogRequestOptions, ): Promise { - const params: Partial< - Parameters[0]['query'] - > = {}; + const isInitialRequest = isQueryEntitiesInitialRequest(request); - if (isQueryEntitiesInitialRequest(request)) { + // Route to POST endpoint if query predicate is provided (initial request) + if (isInitialRequest && request.query) { + return this.queryEntitiesByPredicate(request, options); + } + + // Route to POST endpoint if cursor contains a query predicate (pagination) + // TODO(freben): It's costly and non-opaque to have to introspect the cursor + // like this. It should be refactored in the future to not need this. + // Suggestion: make the GET and POST endpoints understand the same cursor + // format, and pick which one to call ONLY based on whether the cursor size + // risks hitting url length limits + if (!isInitialRequest && cursorContainsQuery(request.cursor)) { + return this.queryEntitiesByPredicate(request, options); + } + + const params: Partial = {}; + + if (isInitialRequest) { const { fields = [], filter, @@ -320,6 +368,84 @@ export class CatalogClient implements CatalogApi { ); } + /** + * Query entities using predicate-based filters (POST endpoint). + * @internal + */ + private async queryEntitiesByPredicate( + request: QueryEntitiesRequest, + options?: CatalogRequestOptions, + ): Promise { + const body: QueryEntitiesByPredicateRequest = {}; + + if (isQueryEntitiesInitialRequest(request)) { + const { + filter, + query, + limit, + offset, + orderFields, + fullTextFilter, + fields, + } = request; + + let filterPredicate: FilterPredicate | undefined; + if (query !== undefined) { + if ( + typeof query !== 'object' || + query === null || + Array.isArray(query) + ) { + throw new InputError('Query must be an object'); + } + filterPredicate = query; + } + if (filter !== undefined) { + const converted = convertFilterToPredicate(filter); + filterPredicate = filterPredicate + ? { $all: [filterPredicate, converted] } + : converted; + } + if (filterPredicate !== undefined) { + body.query = filterPredicate as unknown as { [key: string]: any }; + } + + if (limit !== undefined) { + body.limit = limit; + } + if (offset !== undefined) { + body.offset = offset; + } + if (orderFields !== undefined) { + body.orderBy = [orderFields].flat(); + } + if (fullTextFilter) { + body.fullTextFilter = fullTextFilter; + } + if (fields?.length) { + body.fields = fields; + } + } else { + body.cursor = request.cursor; + if (request.limit !== undefined) { + body.limit = request.limit; + } + if (request.fields?.length) { + body.fields = request.fields; + } + } + + const res = await this.requestRequired( + await this.apiClient.queryEntitiesByPredicate({ body }, options), + ); + + return { + items: res.items, + totalItems: res.totalItems, + pageInfo: res.pageInfo, + }; + } + /** * {@inheritdoc CatalogApi.getEntityByRef} */ @@ -378,7 +504,13 @@ export class CatalogClient implements CatalogApi { request: GetEntityFacetsRequest, options?: CatalogRequestOptions, ): Promise { - const { filter = [], facets } = request; + const { filter = [], query, facets } = request; + + // Route to POST endpoint if query predicate is provided + if (query) { + return this.getEntityFacetsByPredicate(request, options); + } + return await this.requestOptional( await this.apiClient.getEntityFacets( { @@ -389,6 +521,45 @@ export class CatalogClient implements CatalogApi { ); } + /** + * Get entity facets using predicate-based filters (POST endpoint). + * @internal + */ + private async getEntityFacetsByPredicate( + request: GetEntityFacetsRequest, + options?: CatalogRequestOptions, + ): Promise { + const { filter, query, facets } = request; + + let filterPredicate: FilterPredicate | undefined; + if (query !== undefined) { + if (typeof query !== 'object' || query === null || Array.isArray(query)) { + throw new InputError('Query must be an object'); + } + filterPredicate = query; + } + if (filter !== undefined) { + const converted = convertFilterToPredicate(filter); + filterPredicate = filterPredicate + ? { $all: [filterPredicate, converted] } + : converted; + } + + return await this.requestOptional( + await this.apiClient.queryEntityFacetsByPredicate( + { + body: { + facets, + ...(filterPredicate && { + query: filterPredicate as unknown as { [key: string]: any }, + }), + }, + }, + options, + ), + ); + } + /** * {@inheritdoc CatalogApi.addLocation} */ diff --git a/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts b/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts index 4530cd795c..63d55aea55 100644 --- a/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts +++ b/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts @@ -29,6 +29,8 @@ import { Entity } from '../models/Entity.model'; import { EntityAncestryResponse } from '../models/EntityAncestryResponse.model'; import { EntityFacetsResponse } from '../models/EntityFacetsResponse.model'; import { GetEntitiesByRefsRequest } from '../models/GetEntitiesByRefsRequest.model'; +import { QueryEntitiesByPredicateRequest } from '../models/QueryEntitiesByPredicateRequest.model'; +import { QueryEntityFacetsByPredicateRequest } from '../models/QueryEntityFacetsByPredicateRequest.model'; import { RefreshEntityRequest } from '../models/RefreshEntityRequest.model'; import { ValidateEntityRequest } from '../models/ValidateEntityRequest.model'; import { AnalyzeLocationRequest } from '../models/AnalyzeLocationRequest.model'; @@ -139,6 +141,18 @@ export type GetEntityFacets = { filter?: Array; }; }; +/** + * @public + */ +export type QueryEntitiesByPredicate = { + body: QueryEntitiesByPredicateRequest; +}; +/** + * @public + */ +export type QueryEntityFacetsByPredicate = { + body: QueryEntityFacetsByPredicateRequest; +}; /** * @public */ @@ -246,9 +260,9 @@ export class DefaultApiClient { /** * Get all entities matching a given filter. - * @param fields - By default the full entities are returned, but you can pass in a `fields` query parameter which selects what parts of the entity data to retain. This makes the response smaller and faster to transfer, and may allow the catalog to perform more efficient queries. The query parameter value is a comma separated list of simplified JSON paths like above. Each path corresponds to the key of either a value, or of a subtree root that you want to keep in the output. The rest is pruned away. For example, specifying `?fields=metadata.name,metadata.annotations,spec` retains only the `name` and `annotations` fields of the `metadata` of each entity (it'll be an object with at most two keys), keeps the entire `spec` unchanged, and cuts out all other roots such as `relations`. Some more real world usable examples: - Return only enough data to form the full ref of each entity: `/entities/by-query?fields=kind,metadata.namespace,metadata.name` + * @param fields - By default the full entities are returned, but you can pass in a `fields` query parameter which selects what parts of the entity data to retain. This makes the response smaller and faster to transfer, and may allow the catalog to perform more efficient queries. The query parameter value is a comma separated list of simplified JSON paths like above. Each path corresponds to the key of either a value, or of a subtree root that you want to keep in the output. The rest is pruned away. For example, specifying `?fields=metadata.name,metadata.annotations,spec` retains only the `name` and `annotations` fields of the `metadata` of each entity (it\'ll be an object with at most two keys), keeps the entire `spec` unchanged, and cuts out all other roots such as `relations`. Some more real world usable examples: - Return only enough data to form the full ref of each entity: `/entities/by-query?fields=kind,metadata.namespace,metadata.name` * @param limit - Number of records to return in the response. - * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let's look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` + * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let\'s look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` * @param offset - Number of records to skip in the query page. * @param after - Pointer to the previous page of results. * @param order - @@ -277,12 +291,12 @@ export class DefaultApiClient { /** * Search for entities by a given query. - * @param fields - By default the full entities are returned, but you can pass in a `fields` query parameter which selects what parts of the entity data to retain. This makes the response smaller and faster to transfer, and may allow the catalog to perform more efficient queries. The query parameter value is a comma separated list of simplified JSON paths like above. Each path corresponds to the key of either a value, or of a subtree root that you want to keep in the output. The rest is pruned away. For example, specifying `?fields=metadata.name,metadata.annotations,spec` retains only the `name` and `annotations` fields of the `metadata` of each entity (it'll be an object with at most two keys), keeps the entire `spec` unchanged, and cuts out all other roots such as `relations`. Some more real world usable examples: - Return only enough data to form the full ref of each entity: `/entities/by-query?fields=kind,metadata.namespace,metadata.name` + * @param fields - By default the full entities are returned, but you can pass in a `fields` query parameter which selects what parts of the entity data to retain. This makes the response smaller and faster to transfer, and may allow the catalog to perform more efficient queries. The query parameter value is a comma separated list of simplified JSON paths like above. Each path corresponds to the key of either a value, or of a subtree root that you want to keep in the output. The rest is pruned away. For example, specifying `?fields=metadata.name,metadata.annotations,spec` retains only the `name` and `annotations` fields of the `metadata` of each entity (it\'ll be an object with at most two keys), keeps the entire `spec` unchanged, and cuts out all other roots such as `relations`. Some more real world usable examples: - Return only enough data to form the full ref of each entity: `/entities/by-query?fields=kind,metadata.namespace,metadata.name` * @param limit - Number of records to return in the response. * @param offset - Number of records to skip in the query page. * @param orderField - By default the entities are returned ordered by their internal uid. You can customize the `orderField` query parameters to affect that ordering. For example, to return entities by their name: `/entities/by-query?orderField=metadata.name,asc` Each parameter can be followed by `asc` for ascending lexicographical order or `desc` for descending (reverse) lexicographical order. - * @param cursor - You may pass the `cursor` query parameters to perform cursor based pagination through the set of entities. The value of `cursor` will be returned in the response, under the `pageInfo` property: ```json \"pageInfo\": { \"nextCursor\": \"a-cursor\", \"prevCursor\": \"another-cursor\" } ``` If `nextCursor` exists, it can be used to retrieve the next batch of entities. Following the same approach, if `prevCursor` exists, it can be used to retrieve the previous batch of entities. - [`filter`](#filtering), for selecting only a subset of all entities - [`fields`](#field-selection), for selecting only parts of the full data structure of each entity - `limit` for limiting the number of entities returned (20 is the default) - [`orderField`](#ordering), for deciding the order of the entities - `fullTextFilter` **NOTE**: [`filter`, `orderField`, `fullTextFilter`] and `cursor` are mutually exclusive. This means that, it isn't possible to change any of [`filter`, `orderField`, `fullTextFilter`] when passing `cursor` as query parameters, as changing any of these properties will affect pagination. If any of `filter`, `orderField`, `fullTextFilter` is specified together with `cursor`, only the latter is taken into consideration. - * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let's look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` + * @param cursor - You may pass the `cursor` query parameters to perform cursor based pagination through the set of entities. The value of `cursor` will be returned in the response, under the `pageInfo` property: ```json \"pageInfo\": { \"nextCursor\": \"a-cursor\", \"prevCursor\": \"another-cursor\" } ``` If `nextCursor` exists, it can be used to retrieve the next batch of entities. Following the same approach, if `prevCursor` exists, it can be used to retrieve the previous batch of entities. - [`filter`](#filtering), for selecting only a subset of all entities - [`fields`](#field-selection), for selecting only parts of the full data structure of each entity - `limit` for limiting the number of entities returned (20 is the default) - [`orderField`](#ordering), for deciding the order of the entities - `fullTextFilter` **NOTE**: [`filter`, `orderField`, `fullTextFilter`] and `cursor` are mutually exclusive. This means that, it isn\'t possible to change any of [`filter`, `orderField`, `fullTextFilter`] when passing `cursor` as query parameters, as changing any of these properties will affect pagination. If any of `filter`, `orderField`, `fullTextFilter` is specified together with `cursor`, only the latter is taken into consideration. + * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let\'s look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` * @param fullTextFilterTerm - Text search term. * @param fullTextFilterFields - A comma separated list of fields to sort returned results by. */ @@ -310,7 +324,7 @@ export class DefaultApiClient { /** * Get a batch set of entities given an array of entityRefs. - * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let's look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` + * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let\'s look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` * @param getEntitiesByRefsRequest - */ public async getEntitiesByRefs( @@ -337,7 +351,7 @@ export class DefaultApiClient { } /** - * Get an entity's ancestry by entity ref. + * Get an entity\'s ancestry by entity ref. * @param kind - * @param namespace - * @param name - @@ -425,7 +439,7 @@ export class DefaultApiClient { /** * Get all entity facets that match the given filters. * @param facet - - * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let's look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` + * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let\'s look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` */ public async getEntityFacets( // @ts-ignore @@ -449,6 +463,56 @@ export class DefaultApiClient { }); } + /** + * Query entities using predicate-based filters. + * @param queryEntitiesByPredicateRequest - + */ + public async queryEntitiesByPredicate( + // @ts-ignore + request: QueryEntitiesByPredicate, + options?: RequestOptions, + ): Promise> { + const baseUrl = await this.discoveryApi.getBaseUrl(pluginId); + + const uriTemplate = `/entities/by-query`; + + const uri = parser.parse(uriTemplate).expand({}); + + return await this.fetchApi.fetch(`${baseUrl}${uri}`, { + headers: { + 'Content-Type': 'application/json', + ...(options?.token && { Authorization: `Bearer ${options?.token}` }), + }, + method: 'POST', + body: JSON.stringify(request.body), + }); + } + + /** + * Get entity facets using predicate-based filters. + * @param queryEntityFacetsByPredicateRequest - + */ + public async queryEntityFacetsByPredicate( + // @ts-ignore + request: QueryEntityFacetsByPredicate, + options?: RequestOptions, + ): Promise> { + const baseUrl = await this.discoveryApi.getBaseUrl(pluginId); + + const uriTemplate = `/entity-facets`; + + const uri = parser.parse(uriTemplate).expand({}); + + return await this.fetchApi.fetch(`${baseUrl}${uri}`, { + headers: { + 'Content-Type': 'application/json', + ...(options?.token && { Authorization: `Bearer ${options?.token}` }), + }, + method: 'POST', + body: JSON.stringify(request.body), + }); + } + /** * Refresh the entity related to entityRef. * @param refreshEntityRequest - diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts index ea3d097ec5..06998f5422 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts @@ -17,12 +17,11 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { Entity } from '../models/Entity.model'; import { LocationSpec } from '../models/LocationSpec.model'; /** - * If the folder pointed to already contained catalog info yaml files, they are read and emitted like this so that the frontend can inform the user that it located them and can make sure to register them as well if they weren't already + * If the folder pointed to already contained catalog info yaml files, they are read and emitted like this so that the frontend can inform the user that it located them and can make sure to register them as well if they weren\'t already * @public */ export interface AnalyzeLocationExistingEntity { diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts index 2999da3ddf..7b2ba72114 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts @@ -17,12 +17,11 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { AnalyzeLocationEntityField } from '../models/AnalyzeLocationEntityField.model'; import { RecursivePartialEntity } from '../models/RecursivePartialEntity.model'; /** - * This is some form of representation of what the analyzer could deduce. We should probably have a chat about how this can best be conveyed to the frontend. It'll probably contain a (possibly incomplete) entity, plus enough info for the frontend to know what form data to show to the user for overriding/completing the info. + * This is some form of representation of what the analyzer could deduce. We should probably have a chat about how this can best be conveyed to the frontend. It\'ll probably contain a (possibly incomplete) entity, plus enough info for the frontend to know what form data to show to the user for overriding/completing the info. * @public */ export interface AnalyzeLocationGenerateEntity { diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts index 5099a5c8aa..a0a5615f25 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { LocationInput } from '../models/LocationInput.model'; /** diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts index a5eac33f7c..d4d8a0fdd9 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { AnalyzeLocationExistingEntity } from '../models/AnalyzeLocationExistingEntity.model'; import { AnalyzeLocationGenerateEntity } from '../models/AnalyzeLocationGenerateEntity.model'; diff --git a/packages/catalog-client/src/schema/openapi/generated/models/CreateLocation201Response.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/CreateLocation201Response.model.ts index 610e565b3e..e224386de4 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/CreateLocation201Response.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/CreateLocation201Response.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { Entity } from '../models/Entity.model'; import { Location } from '../models/Location.model'; diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts index e8bf79a501..0434965888 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { NullableEntity } from '../models/NullableEntity.model'; /** diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts index d201466db6..5165faec58 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntitiesQueryResponsePageInfo } from '../models/EntitiesQueryResponsePageInfo.model'; import { Entity } from '../models/Entity.model'; diff --git a/packages/catalog-client/src/schema/openapi/generated/models/Entity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/Entity.model.ts index 108a7b15b8..00cc91e89b 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/Entity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/Entity.model.ts @@ -17,12 +17,11 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityMeta } from '../models/EntityMeta.model'; import { EntityRelation } from '../models/EntityRelation.model'; /** - * The parts of the format that's common to all versions/kinds of entity. + * The parts of the format that\'s common to all versions/kinds of entity. * @public */ export interface Entity { diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts index f2c1116f35..9d1e4d9052 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityAncestryResponseItemsInner } from '../models/EntityAncestryResponseItemsInner.model'; /** diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts index 9c816e5517..0936545ec7 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { Entity } from '../models/Entity.model'; /** diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts index 5cc67294e5..540650bcaf 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityFacet } from '../models/EntityFacet.model'; /** diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityMeta.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityMeta.model.ts index c1e1471045..a35ee87b61 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityMeta.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityMeta.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityLink } from '../models/EntityLink.model'; /** @@ -26,7 +25,6 @@ import { EntityLink } from '../models/EntityLink.model'; */ export interface EntityMeta { [key: string]: any; - /** * A list of external hyperlinks related to the entity. */ diff --git a/packages/catalog-client/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts index 0e7255f9c6..607c992ace 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts @@ -24,4 +24,8 @@ export interface GetEntitiesByRefsRequest { entityRefs: Array; fields?: Array; + /** + * A type representing all allowed JSON object values. + */ + query?: { [key: string]: any }; } diff --git a/packages/catalog-client/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts index b77b78971f..846b58ba97 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { Location } from '../models/Location.model'; /** diff --git a/packages/catalog-client/src/schema/openapi/generated/models/LocationsQueryResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/LocationsQueryResponse.model.ts index c231fccc57..05259caf32 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/LocationsQueryResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/LocationsQueryResponse.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { Location } from '../models/Location.model'; import { LocationsQueryResponsePageInfo } from '../models/LocationsQueryResponsePageInfo.model'; diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ModelError.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ModelError.model.ts index 207f16f828..9d5c36325e 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ModelError.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ModelError.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { ErrorError } from '../models/ErrorError.model'; import { ErrorRequest } from '../models/ErrorRequest.model'; import { ErrorResponse } from '../models/ErrorResponse.model'; @@ -27,7 +26,6 @@ import { ErrorResponse } from '../models/ErrorResponse.model'; */ export interface ModelError { [key: string]: any; - error: ErrorError; request?: ErrorRequest; response: ErrorResponse; diff --git a/packages/catalog-client/src/schema/openapi/generated/models/NullableEntity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/NullableEntity.model.ts index 5e59af3327..288b6febb6 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/NullableEntity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/NullableEntity.model.ts @@ -17,12 +17,11 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityMeta } from '../models/EntityMeta.model'; import { EntityRelation } from '../models/EntityRelation.model'; /** - * The parts of the format that's common to all versions/kinds of entity. + * The parts of the format that\'s common to all versions/kinds of entity. * @public */ export type NullableEntity = { diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200ResponseAllOf.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model.ts similarity index 54% rename from plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200ResponseAllOf.model.ts rename to packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model.ts index 5b6c2ff9c7..7196156f55 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRun200ResponseAllOf.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,21 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** -import { DryRun200ResponseAllOfDirectoryContentsInner } from '../models/DryRun200ResponseAllOfDirectoryContentsInner.model'; -import { DryRun200ResponseAllOfStepsInner } from '../models/DryRun200ResponseAllOfStepsInner.model'; +import { QueryEntitiesByPredicateRequestFullTextFilter } from '../models/QueryEntitiesByPredicateRequestFullTextFilter.model'; +import { QueryEntitiesByPredicateRequestOrderByInner } from '../models/QueryEntitiesByPredicateRequestOrderByInner.model'; /** * @public */ -export interface DryRun200ResponseAllOf { - steps: Array; - directoryContents?: Array; +export interface QueryEntitiesByPredicateRequest { + cursor?: string; + limit?: number; + offset?: number; + orderBy?: Array; + fullTextFilter?: QueryEntitiesByPredicateRequestFullTextFilter; + fields?: Array; + /** + * A type representing all allowed JSON object values. + */ + query?: { [key: string]: any }; } diff --git a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskSecretsAllOf.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestFullTextFilter.model.ts similarity index 84% rename from plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskSecretsAllOf.model.ts rename to packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestFullTextFilter.model.ts index 0b33852cc9..e7e971e1ea 100644 --- a/plugins/scaffolder-backend/src/schema/openapi/generated/models/TaskSecretsAllOf.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestFullTextFilter.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ /** * @public */ -export interface TaskSecretsAllOf { - backstageToken?: string; +export interface QueryEntitiesByPredicateRequestFullTextFilter { + term?: string; + fields?: Array; } diff --git a/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestOrderByInner.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestOrderByInner.model.ts new file mode 100644 index 0000000000..373ca9715f --- /dev/null +++ b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntitiesByPredicateRequestOrderByInner.model.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** + +/** + * @public + */ +export interface QueryEntitiesByPredicateRequestOrderByInner { + field: string; + order: QueryEntitiesByPredicateRequestOrderByInnerOrderEnum; +} + +/** + * @public + */ +export type QueryEntitiesByPredicateRequestOrderByInnerOrderEnum = + | 'asc' + | 'desc'; diff --git a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResultLogInnerBodyAllOf.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntityFacetsByPredicateRequest.model.ts similarity index 78% rename from plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResultLogInnerBodyAllOf.model.ts rename to packages/catalog-client/src/schema/openapi/generated/models/QueryEntityFacetsByPredicateRequest.model.ts index e67240ee54..14fd3a8bc0 100644 --- a/plugins/scaffolder-common/src/schema/openapi/generated/models/DryRunResultLogInnerBodyAllOf.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/QueryEntityFacetsByPredicateRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,14 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** -import { TaskStatus } from '../models/TaskStatus.model'; /** * @public */ -export interface DryRunResultLogInnerBodyAllOf { - message: string; - status?: TaskStatus; - stepId?: string; +export interface QueryEntityFacetsByPredicateRequest { + facets: Array; + /** + * A type representing all allowed JSON object values. + */ + query?: { [key: string]: any }; } diff --git a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts index 3f09c69672..3a607dd878 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { RecursivePartialEntityMeta } from '../models/RecursivePartialEntityMeta.model'; import { RecursivePartialEntityRelation } from '../models/RecursivePartialEntityRelation.model'; diff --git a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts index 2db84dd085..7c2d3bed48 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { EntityLink } from '../models/EntityLink.model'; /** diff --git a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts deleted file mode 100644 index caca69a622..0000000000 --- a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2026 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// ****************************************************************** -// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * -// ****************************************************************** - -import { EntityLink } from '../models/EntityLink.model'; - -/** - * Metadata fields common to all versions/kinds of entity. - * @public - */ -export interface RecursivePartialEntityMetaAllOf { - /** - * A list of external hyperlinks related to the entity. - */ - links?: Array; - /** - * A list of single-valued strings, to for example classify catalog entities in various ways. - */ - tags?: Array; - /** - * Construct a type with a set of properties K of type T - */ - annotations?: { [key: string]: string }; - /** - * Construct a type with a set of properties K of type T - */ - labels?: { [key: string]: string }; - /** - * A short (typically relatively few words, on one line) description of the entity. - */ - description?: string; - /** - * A display name of the entity, to be presented in user interfaces instead of the `name` property above, when available. This field is sometimes useful when the `name` is cumbersome or ends up being perceived as overly technical. The title generally does not have as stringent format requirements on it, so it may contain special characters and be more explanatory. Do keep it very short though, and avoid situations where a title can be confused with the name of another entity, or where two entities share a title. Note that this is only for display purposes, and may be ignored by some parts of the code. Entity references still always make use of the `name` property, not the title. - */ - title?: string; - /** - * The namespace that the entity belongs to. - */ - namespace?: string; - /** - * The name of the entity. Must be unique within the catalog at any given point in time, for any given namespace + kind pair. This value is part of the technical identifier of the entity, and as such it will appear in URLs, database tables, entity references, and similar. It is subject to restrictions regarding what characters are allowed. If you want to use a different, more human readable string with fewer restrictions on it in user interfaces, see the `title` field below. - */ - name?: string; - /** - * An opaque string that changes for each update operation to any part of the entity, including metadata. This field can not be set by the user at creation time, and the server will reject an attempt to do so. The field will be populated in read operations. The field can (optionally) be specified when performing update or delete operations, and the server will then reject the operation if it does not match the current stored value. - */ - etag?: string; - /** - * A globally unique ID for the entity. This field can not be set by the user at creation time, and the server will reject an attempt to do so. The field will be populated in read operations. The field can (optionally) be specified when performing update or delete operations, but the server is free to reject requests that do so in such a way that it breaks semantics. - */ - uid?: string; -} diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts index aabf825a57..2a41f2df5c 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts @@ -17,7 +17,6 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** - import { ValidateEntity400ResponseErrorsInner } from '../models/ValidateEntity400ResponseErrorsInner.model'; /** diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts index 4582bf2046..f8a7ae4386 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts @@ -23,7 +23,6 @@ */ export interface ValidateEntity400ResponseErrorsInner { [key: string]: any; - name: string; message: string; } diff --git a/packages/catalog-client/src/schema/openapi/generated/models/index.ts b/packages/catalog-client/src/schema/openapi/generated/models/index.ts index abdb58378c..aaafc8e60e 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/index.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/index.ts @@ -45,9 +45,12 @@ export * from '../models/LocationsQueryResponse.model'; export * from '../models/LocationsQueryResponsePageInfo.model'; export * from '../models/ModelError.model'; export * from '../models/NullableEntity.model'; +export * from '../models/QueryEntitiesByPredicateRequest.model'; +export * from '../models/QueryEntitiesByPredicateRequestFullTextFilter.model'; +export * from '../models/QueryEntitiesByPredicateRequestOrderByInner.model'; +export * from '../models/QueryEntityFacetsByPredicateRequest.model'; export * from '../models/RecursivePartialEntity.model'; export * from '../models/RecursivePartialEntityMeta.model'; -export * from '../models/RecursivePartialEntityMetaAllOf.model'; export * from '../models/RecursivePartialEntityRelation.model'; export * from '../models/RefreshEntityRequest.model'; export * from '../models/ValidateEntity400Response.model'; diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts index d867ab1d28..98b5d5c034 100644 --- a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts @@ -370,6 +370,25 @@ describe('InMemoryCatalogClient', () => { { kind: 'CustomKind', metadata: { name: 'e1' } }, ]); }); + + it('supports query predicate filter', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.getEntitiesByRefs({ + entityRefs: ['secondcustomkind:default/e2', 'customkind:default/e1'], + query: { kind: 'CustomKind' }, + }); + expect(result.items).toEqual([undefined, entity1]); + }); + + it('supports both filter and query predicate together', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.getEntitiesByRefs({ + entityRefs: ['customkind:default/e1', 'customkind:other/e3'], + filter: { kind: 'CustomKind' }, + query: { 'metadata.namespace': 'other' }, + }); + expect(result.items).toEqual([undefined, entity3]); + }); }); describe('queryEntities', () => { @@ -683,6 +702,83 @@ describe('InMemoryCatalogClient', () => { ]); }); + it('filters by predicate query', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.queryEntities({ + query: { kind: 'CustomKind' }, + }); + expect(result.items).toEqual([entity1, entity3]); + expect(result.totalItems).toBe(2); + }); + + it('filters by predicate query with $all', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.queryEntities({ + query: { + $all: [{ kind: 'CustomKind' }, { 'spec.type': 'service' }], + }, + }); + expect(result.items).toEqual([entity1, entity3]); + }); + + it('filters by predicate query with $any', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.queryEntities({ + query: { + $any: [{ 'spec.type': 'service' }, { 'spec.type': 'website' }], + }, + }); + expect(result.items).toEqual([entity1, entity3, entity4]); + }); + + it('filters by predicate query with $not', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.queryEntities({ + query: { + $all: [ + { kind: 'CustomKind' }, + { $not: { 'spec.lifecycle': 'production' } }, + ], + }, + }); + expect(result.items).toEqual([]); + }); + + it('filters by predicate query with $in', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.queryEntities({ + query: { 'spec.type': { $in: ['service', 'library'] } }, + }); + expect(result.items).toEqual([entity1, entity2, entity3]); + }); + + it('filters by predicate query with $exists', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.queryEntities({ + query: { 'spec.lifecycle': { $exists: false } }, + }); + expect(result.items).toEqual([entity4]); + }); + + it('preserves query predicate through cursor pagination', async () => { + const client = new InMemoryCatalogClient({ entities }); + const page1 = await client.queryEntities({ + query: { kind: 'CustomKind' }, + orderFields: { field: 'metadata.name', order: 'asc' }, + limit: 1, + }); + expect(page1.items.map(e => e.metadata.name)).toEqual(['e1']); + expect(page1.totalItems).toBe(2); + expect(page1.pageInfo.nextCursor).toBeDefined(); + + const page2 = await client.queryEntities({ + cursor: page1.pageInfo.nextCursor!, + limit: 1, + }); + expect(page2.items.map(e => e.metadata.name)).toEqual(['e3']); + expect(page2.pageInfo.nextCursor).toBeUndefined(); + }); + it('throws InputError for invalid cursor', async () => { const client = new InMemoryCatalogClient({ entities }); await expect( @@ -898,6 +994,29 @@ describe('InMemoryCatalogClient', () => { }); expect(result.facets['spec.nonexistent']).toEqual([]); }); + + it('supports query predicate filter', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.getEntityFacets({ + facets: ['spec.type'], + query: { kind: 'CustomKind' }, + }); + expect(result.facets['spec.type']).toEqual([ + { value: 'service', count: 2 }, + ]); + }); + + it('supports both filter and query predicate together', async () => { + const client = new InMemoryCatalogClient({ entities }); + const result = await client.getEntityFacets({ + facets: ['spec.type'], + filter: { kind: 'CustomKind' }, + query: { 'metadata.namespace': 'default' }, + }); + expect(result.facets['spec.type']).toEqual([ + { value: 'service', count: 1 }, + ]); + }); }); describe('not implemented methods', () => { diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts index e64706ff8d..dd7f269188 100644 --- a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts @@ -51,6 +51,10 @@ import { NotFoundError, NotImplementedError, } from '@backstage/errors'; +import { + FilterPredicate, + filterPredicateToFilterFunction, +} from '@backstage/filter-predicates'; import lodash from 'lodash'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { traverse } from '../../../../plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch'; @@ -357,10 +361,15 @@ export class InMemoryCatalogClient implements CatalogApi { request: GetEntitiesByRefsRequest, ): Promise { const filter = createFilter(request.filter); + const queryFilter = request.query + ? filterPredicateToFilterFunction(request.query) + : undefined; const refMap = this.#createEntityRefMap(); const items = request.entityRefs .map(ref => refMap.get(ref)) - .map(e => (e && filter(e) ? e : undefined)); + .map(e => + e && filter(e) && (!queryFilter || queryFilter(e)) ? e : undefined, + ); return { items: request.fields ? items.map(e => (e ? applyFieldsFilter(e, request.fields) : undefined)) @@ -373,6 +382,7 @@ export class InMemoryCatalogClient implements CatalogApi { ): Promise { // Decode query parameters from cursor or from the request directly let filter: EntityFilterQuery | undefined; + let query: FilterPredicate | undefined; let orderFields: EntityOrderQuery | undefined; let fullTextFilter: { term: string; fields?: string[] } | undefined; let offset: number; @@ -386,12 +396,14 @@ export class InMemoryCatalogClient implements CatalogApi { throw new InputError('Invalid cursor'); } filter = deserializeFilter(c.filter as any[]); + query = c.query as FilterPredicate | undefined; orderFields = c.orderFields as EntityOrderQuery | undefined; fullTextFilter = c.fullTextFilter as typeof fullTextFilter; offset = c.offset as number; limit = request.limit; } else { filter = request?.filter; + query = request?.query; orderFields = request?.orderFields; fullTextFilter = request?.fullTextFilter; offset = request?.offset ?? 0; @@ -401,6 +413,11 @@ export class InMemoryCatalogClient implements CatalogApi { // Apply filter let items = this.#entities.filter(createFilter(filter)); + // Apply predicate-based query filter + if (query) { + items = items.filter(filterPredicateToFilterFunction(query)); + } + // Apply full-text filter, defaulting to the sort field or metadata.uid if (fullTextFilter) { const orderFieldsList = orderFields ? [orderFields].flat() : []; @@ -432,6 +449,7 @@ export class InMemoryCatalogClient implements CatalogApi { const cursorBase = { filter: serializeFilter(filter), + query, orderFields, fullTextFilter, totalItems, @@ -493,7 +511,12 @@ export class InMemoryCatalogClient implements CatalogApi { request: GetEntityFacetsRequest, ): Promise { const filter = createFilter(request.filter); - const filteredEntities = this.#entities.filter(filter); + let filteredEntities = this.#entities.filter(filter); + if (request.query) { + filteredEntities = filteredEntities.filter( + filterPredicateToFilterFunction(request.query), + ); + } const facets = Object.fromEntries( request.facets.map(facet => { const facetValues = new Map(); diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index c766b094b8..fac4ed5928 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; -import { SerializedError } from '@backstage/errors'; +import type { CompoundEntityRef, Entity } from '@backstage/catalog-model'; +import type { SerializedError } from '@backstage/errors'; import type { AnalyzeLocationRequest, AnalyzeLocationResponse, } from '@backstage/plugin-catalog-common'; -import { FilterPredicate } from '@backstage/filter-predicates'; +import type { FilterPredicate } from '@backstage/filter-predicates'; /** * This symbol can be used in place of a value when passed to filters in e.g. @@ -212,6 +212,16 @@ export interface GetEntitiesByRefsRequest { * If given, return only entities that match the given filter. */ filter?: EntityFilterQuery; + /** + * If given, return only entities that match the given predicate query. + * + * @remarks + * + * Supports operators like `$all`, `$any`, `$not`, `$exists`, `$in`, + * `$contains`, and `$hasPrefix`. When both `filter` and `query` are + * provided, they are combined with `$all`. + */ + query?: FilterPredicate; } /** @@ -297,6 +307,16 @@ export interface GetEntityFacetsRequest { * of that key, no matter what its value is. */ filter?: EntityFilterQuery; + /** + * If given, return only entities that match the given predicate query. + * + * @remarks + * + * Supports operators like `$all`, `$any`, `$not`, `$exists`, `$in`, + * `$contains`, and `$hasPrefix`. When both `filter` and `query` are + * provided, they are combined with `$all`. + */ + query?: FilterPredicate; /** * Dot separated paths for the facets to extract from each entity. * @@ -418,16 +438,43 @@ export type QueryEntitiesRequest = * The method takes this type in an initial pagination request, * when requesting the first batch of entities. * - * The properties filter, sortField, query and sortFieldOrder, are going + * The properties filter, query, sortField and sortFieldOrder, are going * to be immutable for the entire lifecycle of the following requests. * + * @remarks + * + * Either `filter` or `query` can be provided, or even both: + * - `filter`: Uses the traditional key-value filter syntax (GET endpoint) + * - `query`: Uses the predicate-based filter syntax with logical operators (POST endpoint) + * * @public */ export type QueryEntitiesInitialRequest = { fields?: string[]; limit?: number; offset?: number; + /** + * Traditional key-value based filter. + */ filter?: EntityFilterQuery; + /** + * Predicate-based filter with operators for logical expressions (`$all`, + * `$any`, and `$not`) and matching (`$exists`, `$in`, `$hasPrefix`, and + * (partially) `$contains`). + * + * @example + * ```typescript + * { + * query: { + * $all: [ + * { kind: 'component' }, + * { 'spec.type': { $in: ['service', 'website'] } } + * ] + * } + * } + * ``` + */ + query?: FilterPredicate; orderFields?: EntityOrderQuery; fullTextFilter?: { term: string; @@ -567,6 +614,7 @@ export interface CatalogApi { * const response = await catalogClient.queryEntities({ * filter: [{ kind: 'group' }], * limit: 20, + * fields: ['metadata', 'kind'], * fullTextFilter: { * term: 'A', * }, @@ -583,11 +631,15 @@ export interface CatalogApi { * * ``` * const secondBatchResponse = await catalogClient - * .queryEntities({ cursor: response.nextCursor }); + * .queryEntities({ + * cursor: response.nextCursor, + * limit: 20, + * fields: ['metadata', 'kind'], + * }); * ``` * - * secondBatchResponse will contain the next batch of (maximum) 20 entities, - * together with a prevCursor property, useful to fetch the previous batch. + * `secondBatchResponse` will contain the next batch of (maximum) 20 entities, + * together with a `prevCursor` property, useful to fetch the previous batch. * * @public * diff --git a/packages/catalog-client/src/utils.test.ts b/packages/catalog-client/src/utils.test.ts index 2f00859985..ce60cc3083 100644 --- a/packages/catalog-client/src/utils.test.ts +++ b/packages/catalog-client/src/utils.test.ts @@ -14,7 +14,87 @@ * limitations under the License. */ -import { splitRefsIntoChunks } from './utils'; +import { CATALOG_FILTER_EXISTS } from './types/api'; +import { convertFilterToPredicate, splitRefsIntoChunks } from './utils'; + +describe('convertFilterToPredicate', () => { + it('converts a single string value', () => { + expect(convertFilterToPredicate({ kind: 'component' })).toEqual({ + kind: 'component', + }); + }); + + it('converts multiple keys into $all', () => { + expect( + convertFilterToPredicate({ + kind: 'component', + 'spec.type': 'service', + }), + ).toEqual({ + $all: [{ kind: 'component' }, { 'spec.type': 'service' }], + }); + }); + + it('converts an array of string values into $in', () => { + expect( + convertFilterToPredicate({ 'spec.type': ['service', 'website'] }), + ).toEqual({ + 'spec.type': { $in: ['service', 'website'] }, + }); + }); + + it('converts CATALOG_FILTER_EXISTS into $exists', () => { + expect( + convertFilterToPredicate({ 'spec.owner': CATALOG_FILTER_EXISTS }), + ).toEqual({ + 'spec.owner': { $exists: true }, + }); + }); + + it('converts an array of records into $any (OR)', () => { + expect( + convertFilterToPredicate([{ kind: 'component' }, { kind: 'api' }]), + ).toEqual({ + $any: [{ kind: 'component' }, { kind: 'api' }], + }); + }); + + it('converts array of records with multiple keys each', () => { + expect( + convertFilterToPredicate([ + { kind: 'component', 'spec.type': 'service' }, + { kind: 'api' }, + ]), + ).toEqual({ + $any: [ + { $all: [{ kind: 'component' }, { 'spec.type': 'service' }] }, + { kind: 'api' }, + ], + }); + }); + + it('treats CATALOG_FILTER_EXISTS mixed with string values as just existence', () => { + expect( + convertFilterToPredicate({ + 'spec.owner': [CATALOG_FILTER_EXISTS, 'team-a'], + }), + ).toEqual({ + 'spec.owner': { $exists: true }, + }); + }); + + it('converts a single-element array filter without wrapping in $any', () => { + expect(convertFilterToPredicate([{ kind: 'component' }])).toEqual({ + kind: 'component', + }); + }); + + it('ignores entries with no valid values', () => { + expect( + convertFilterToPredicate({ kind: 'component', other: [] as string[] }), + ).toEqual({ kind: 'component' }); + }); +}); describe('splitRefsIntoChunks', () => { it('splits by count limit', () => { diff --git a/packages/catalog-client/src/utils.ts b/packages/catalog-client/src/utils.ts index a16cf53db1..231aa2409f 100644 --- a/packages/catalog-client/src/utils.ts +++ b/packages/catalog-client/src/utils.ts @@ -14,7 +14,13 @@ * limitations under the License. */ +import type { + FilterPredicate, + FilterPredicateExpression, +} from '@backstage/filter-predicates'; import { + CATALOG_FILTER_EXISTS, + EntityFilterQuery, QueryEntitiesCursorRequest, QueryEntitiesInitialRequest, QueryEntitiesRequest, @@ -26,6 +32,58 @@ export function isQueryEntitiesInitialRequest( return !(request as QueryEntitiesCursorRequest).cursor; } +/** + * Check if a cursor contains a predicate query by attempting to decode it. + * @internal + */ +export function cursorContainsQuery(cursor: string): boolean { + try { + const decoded = JSON.parse(atob(cursor)); + return 'query' in decoded; + } catch { + return false; + } +} + +/** + * Converts an {@link EntityFilterQuery} into a predicate query object. + * @internal + */ +export function convertFilterToPredicate(filter: EntityFilterQuery): + | FilterPredicateExpression + | { + $all: FilterPredicate[]; + } + | { + $any: FilterPredicate[]; + } { + const records = [filter].flat(); + + const clauses = records.map(record => { + const parts: FilterPredicateExpression[] = []; + + for (const [key, value] of Object.entries(record)) { + const values = [value].flat(); + const strings = values.filter((v): v is string => typeof v === 'string'); + const hasExists = values.some(v => v === CATALOG_FILTER_EXISTS); + + if (hasExists) { + // Ignore whether there ALSO were some strings - that would boil down to + // just existence anyway since there's effectively an OR between them + parts.push({ [key]: { $exists: true } } as FilterPredicateExpression); + } else if (strings.length === 1) { + parts.push({ [key]: strings[0] } as FilterPredicateExpression); + } else if (strings.length > 1) { + parts.push({ [key]: { $in: strings } } as FilterPredicateExpression); + } + } + + return parts.length === 1 ? parts[0] : { $all: parts }; + }); + + return clauses.length === 1 ? clauses[0] : { $any: clauses }; +} + /** * Takes a set of entity refs, and splits them into chunks (groups) such that * the total string length in each chunk does not exceed the default Express.js diff --git a/packages/cli-common/CHANGELOG.md b/packages/cli-common/CHANGELOG.md index c36322d60f..d7a5679026 100644 --- a/packages/cli-common/CHANGELOG.md +++ b/packages/cli-common/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/cli-common +## 0.2.0-next.1 + +### Patch Changes + +- e44b6a9: The `findOwnRootDir` utility now searches for the monorepo root by traversing up the directory tree looking for a `package.json` with `workspaces`, instead of assuming a fixed `../..` relative path. If no workspaces root is found during this traversal, `findOwnRootDir` now throws to enforce stricter validation of the repository layout. +- Updated dependencies + - @backstage/errors@1.2.7 + +## 0.2.0-next.0 + +### Minor Changes + +- 56bd494: Added `targetPaths` and `findOwnPaths` as replacements for `findPaths`, with a cleaner separation between target project paths and package-relative paths. + + To migrate existing `findPaths` usage: + + ```ts + // Before + import { findPaths } from '@backstage/cli-common'; + const paths = findPaths(__dirname); + + // After — for target project paths (cwd-based): + import { targetPaths } from '@backstage/cli-common'; + // paths.targetDir → targetPaths.dir + // paths.targetRoot → targetPaths.rootDir + // paths.resolveTarget('src') → targetPaths.resolve('src') + // paths.resolveTargetRoot('yarn.lock') → targetPaths.resolveRoot('yarn.lock') + + // After — for package-relative paths: + import { findOwnPaths } from '@backstage/cli-common'; + const own = findOwnPaths(__dirname); + // paths.ownDir → own.dir + // paths.ownRoot → own.rootDir + // paths.resolveOwn('config/jest.js') → own.resolve('config/jest.js') + // paths.resolveOwnRoot('tsconfig.json') → own.resolveRoot('tsconfig.json') + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.7 + ## 0.1.18 ### Patch Changes diff --git a/packages/cli-common/package.json b/packages/cli-common/package.json index 4eae1d96a4..7e44334900 100644 --- a/packages/cli-common/package.json +++ b/packages/cli-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-common", - "version": "0.1.18", + "version": "0.2.0-next.1", "description": "Common functionality used by cli, backend, and create-app", "backstage": { "role": "node-library" diff --git a/packages/cli-common/src/paths.test.ts b/packages/cli-common/src/paths.test.ts index 6ade904e12..df0e7c2044 100644 --- a/packages/cli-common/src/paths.test.ts +++ b/packages/cli-common/src/paths.test.ts @@ -16,18 +16,18 @@ /* eslint-disable no-restricted-syntax */ import { resolve as resolvePath } from 'node:path'; -import { findPaths, findRootPath, findOwnDir, findOwnRootDir } from './paths'; +import { findPaths, findRootPath, findOwnRootDir, findOwnPaths } from './paths'; describe('paths', () => { afterEach(() => { jest.restoreAllMocks(); }); - it('findOwnDir and findOwnRootDir should find owns paths', () => { - const dir = findOwnDir(__dirname); - const root = findOwnRootDir(dir); + it('findOwnPaths and findOwnRootDir should find own paths', () => { + const own = findOwnPaths(__dirname); + const root = findOwnRootDir(own.dir); - expect(dir).toBe(resolvePath(__dirname, '..')); + expect(own.dir).toBe(resolvePath(__dirname, '..')); expect(root).toBe(resolvePath(__dirname, '../../..')); }); @@ -98,7 +98,9 @@ describe('paths', () => { }); it('findPaths should find workspace root with object', () => { - jest.spyOn(JSON, 'parse').mockReturnValue({ workspaces: { packages: [] } }); + jest + .spyOn(JSON, 'parse') + .mockReturnValue({ workspaces: { packages: ['packages/*'] } }); jest.spyOn(process, 'cwd').mockReturnValue(__dirname); const paths = findPaths(__dirname); @@ -110,7 +112,7 @@ describe('paths', () => { }); it('findPaths should find workspace root with array', () => { - jest.spyOn(JSON, 'parse').mockReturnValue({ workspaces: [] }); + jest.spyOn(JSON, 'parse').mockReturnValue({ workspaces: ['packages/*'] }); jest.spyOn(process, 'cwd').mockReturnValue(__dirname); const paths = findPaths(__dirname); diff --git a/packages/cli-common/src/paths.ts b/packages/cli-common/src/paths.ts index 1cd84d4f5b..c6bedc77e4 100644 --- a/packages/cli-common/src/paths.ts +++ b/packages/cli-common/src/paths.ts @@ -119,7 +119,23 @@ export function findOwnRootDir(ownDir: string) { ); } - return resolvePath(ownDir, '../..'); + const rootDir = findRootPath(ownDir, pkgJsonPath => { + try { + const content = fs.readFileSync(pkgJsonPath, 'utf8'); + const data = JSON.parse(content); + return Boolean(data.workspaces); + } catch (error) { + throw new Error( + `Failed to read package.json at '${pkgJsonPath}', ${error}`, + ); + } + }); + + if (!rootDir) { + throw new Error(`No monorepo root found when searching from '${ownDir}'`); + } + + return rootDir; } // Hierarchical directory cache shared across all OwnPathsImpl instances. @@ -199,11 +215,6 @@ class OwnPathsImpl implements OwnPaths { }; } -// Finds the root of a given package -export function findOwnDir(searchDir: string) { - return OwnPathsImpl.findDir(searchDir); -} - // Used by the test utility in testUtils.ts to override targetPaths export let targetPathsOverride: TargetPaths | undefined; diff --git a/packages/cli-common/src/run.test.ts b/packages/cli-common/src/run.test.ts index 01c6c915d6..f38c1659ba 100644 --- a/packages/cli-common/src/run.test.ts +++ b/packages/cli-common/src/run.test.ts @@ -338,5 +338,26 @@ describe('run', () => { const result = await runCheck(['nonexistent-command-12345']); expect(result).toBe(false); }); + + it('should not leak stdout or stderr from the child process', async () => { + const stdoutSpy = jest.spyOn(process.stdout, 'write'); + const stderrSpy = jest.spyOn(process.stderr, 'write'); + const stdoutBefore = stdoutSpy.mock.calls.length; + const stderrBefore = stderrSpy.mock.calls.length; + + await runCheck([ + 'node', + '--eval', + 'console.log("leaked stdout"); console.error("leaked stderr")', + ]); + + const stdoutCalls = stdoutSpy.mock.calls.slice(stdoutBefore); + const stderrCalls = stderrSpy.mock.calls.slice(stderrBefore); + const stdout = stdoutCalls.map(c => String(c[0])).join(''); + const stderr = stderrCalls.map(c => String(c[0])).join(''); + + expect(stdout).not.toContain('leaked stdout'); + expect(stderr).not.toContain('leaked stderr'); + }); }); }); diff --git a/packages/cli-common/src/run.ts b/packages/cli-common/src/run.ts index 23e71460e4..0e95bd517b 100644 --- a/packages/cli-common/src/run.ts +++ b/packages/cli-common/src/run.ts @@ -211,7 +211,7 @@ export async function runOutput( */ export async function runCheck(args: string[]): Promise { try { - await run(args).waitForExit(); + await run(args, { stdio: 'ignore' }).waitForExit(); return true; } catch { return false; diff --git a/packages/cli-node/CHANGELOG.md b/packages/cli-node/CHANGELOG.md index b960abfbac..96a404989e 100644 --- a/packages/cli-node/CHANGELOG.md +++ b/packages/cli-node/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/cli-node +## 0.2.19-next.1 + +### Patch Changes + +- 61cb976: Added `toString()` method to `Lockfile` for serializing lockfiles back to string format. +- 3c811bf: Added `hasBackstageYarnPlugin` and `SuccessCache` exports, moved from `@backstage/cli`. +- a9d23c4: Properly support `package.json` `workspaces` field +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + +## 0.2.19-next.0 + +### Patch Changes + +- 06c2015: Added `runConcurrentTasks` and `runWorkerQueueThreads` utilities, moved from the `@backstage/cli` internal code. +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + ## 0.2.18 ### Patch Changes diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index a4629c3de2..4fa5af208c 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-node", - "version": "0.2.18", + "version": "0.2.19-next.1", "description": "Node.js library for Backstage CLIs", "backstage": { "role": "node-library" @@ -35,14 +35,17 @@ "@backstage/errors": "workspace:^", "@backstage/types": "workspace:^", "@manypkg/get-packages": "^1.1.3", + "@yarnpkg/lockfile": "^1.1.0", "@yarnpkg/parsers": "^3.0.0", "fs-extra": "^11.2.0", "semver": "^7.5.3", + "yaml": "^2.0.0", "zod": "^3.25.76" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/test-utils": "workspace:^" + "@backstage/test-utils": "workspace:^", + "@types/yarnpkg__lockfile": "^1.1.4" } } diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index b3d8fe5641..9d4887ca59 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -99,6 +99,9 @@ export class GitUtils { static readFileAtRef(path: string, ref: string): Promise; } +// @public +export function hasBackstageYarnPlugin(workspaceDir?: string): Promise; + // @public export function isMonoRepo(): Promise; @@ -111,6 +114,7 @@ export class Lockfile { keys(): IterableIterator; static load(path: string): Promise; static parse(content: string): Lockfile; + toString(): string; } // @public @@ -220,6 +224,17 @@ export function runWorkerQueueThreads( results: TResult[]; }>; +// @public +export class SuccessCache { + // (undocumented) + static create(options: { name: string; basePath?: string }): SuccessCache; + // (undocumented) + read(): Promise>; + static trimPaths(input: string): string; + // (undocumented) + write(newEntries: Iterable): Promise; +} + // @public export type WorkerQueueThreadsOptions = { items: Iterable; diff --git a/packages/cli/src/lib/cache/SuccessCache.ts b/packages/cli-node/src/cache/SuccessCache.ts similarity index 86% rename from packages/cli/src/lib/cache/SuccessCache.ts rename to packages/cli-node/src/cache/SuccessCache.ts index 8131a469a4..78913ed0b6 100644 --- a/packages/cli/src/lib/cache/SuccessCache.ts +++ b/packages/cli-node/src/cache/SuccessCache.ts @@ -22,6 +22,12 @@ const DEFAULT_CACHE_BASE_PATH = 'node_modules/.cache/backstage-cli'; const CACHE_MAX_AGE_MS = 7 * 24 * 3600_000; +/** + * A file-system-based cache that tracks successful operations by storing + * timestamped marker files. + * + * @public + */ export class SuccessCache { readonly #path: string; @@ -34,8 +40,15 @@ export class SuccessCache { return input.replaceAll(targetPaths.rootDir, ''); } - constructor(name: string, basePath?: string) { - this.#path = resolvePath(basePath ?? DEFAULT_CACHE_BASE_PATH, name); + static create(options: { name: string; basePath?: string }): SuccessCache { + return new SuccessCache(options); + } + + private constructor(options: { name: string; basePath?: string }) { + this.#path = resolvePath( + options.basePath ?? DEFAULT_CACHE_BASE_PATH, + options.name, + ); } async read(): Promise> { @@ -89,7 +102,6 @@ export class SuccessCache { const empty = Buffer.alloc(0); for (const key of newEntries) { - // Remove any existing items with the key we're about to add const trimmedItems = existingItems.filter(item => item.endsWith(`_${key}`), ); diff --git a/packages/cli-node/src/cache/index.ts b/packages/cli-node/src/cache/index.ts new file mode 100644 index 0000000000..53c9f0e4e4 --- /dev/null +++ b/packages/cli-node/src/cache/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 { SuccessCache } from './SuccessCache'; diff --git a/packages/cli-node/src/index.ts b/packages/cli-node/src/index.ts index 5540666a05..35074507c4 100644 --- a/packages/cli-node/src/index.ts +++ b/packages/cli-node/src/index.ts @@ -20,7 +20,9 @@ * @packageDocumentation */ +export * from './cache'; +export * from './concurrency'; export * from './git'; export * from './monorepo'; -export * from './concurrency'; export * from './roles'; +export * from './yarn'; diff --git a/packages/cli-node/src/monorepo/Lockfile.test.ts b/packages/cli-node/src/monorepo/Lockfile.test.ts index 03b50fea90..c2d35f7352 100644 --- a/packages/cli-node/src/monorepo/Lockfile.test.ts +++ b/packages/cli-node/src/monorepo/Lockfile.test.ts @@ -15,6 +15,7 @@ */ import { Lockfile } from './Lockfile'; +import { createMockDirectory } from '@backstage/backend-test-utils'; const LEGACY_HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. # yarn lockfile v1 @@ -29,7 +30,74 @@ __metadata: cacheKey: 8 `; -describe('New Lockfile', () => { +const mockLegacy = `${LEGACY_HEADER} +a@^1: + version "1.0.1" + resolved "https://my-registry/a-1.0.01.tgz#abc123" + integrity sha512-xyz + dependencies: + b "^2" + +b@2.0.x: + version "2.0.1" + +b@^2: + version "2.0.0" +`; + +const mockModern = `${MODERN_HEADER} +a@^1: + version: 1.0.1 + dependencies: + b: ^2 + integrity: sha512-xyz + resolved: "https://my-registry/a-1.0.01.tgz#abc123" + +"b@2.0.x, b@^2.0.1": + version: 2.0.1 + +b@^2: + version: 2.0.0 +`; + +describe('Lockfile', () => { + const mockDir = createMockDirectory(); + + it('should load and serialize a legacy lockfile', async () => { + mockDir.setContent({ + 'yarn.lock': mockLegacy, + }); + + const lockfile = await Lockfile.load(mockDir.resolve('yarn.lock')); + expect(lockfile.get('a')).toEqual([ + { range: '^1', version: '1.0.1', dataKey: 'a@^1' }, + ]); + expect(lockfile.get('b')).toEqual([ + { range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x' }, + { range: '^2', version: '2.0.0', dataKey: 'b@^2' }, + ]); + expect(lockfile.toString()).toBe(mockLegacy); + }); + + it('should load and serialize a modern lockfile', async () => { + mockDir.setContent({ + 'yarn.lock': mockModern, + }); + + const lockfile = await Lockfile.load(mockDir.resolve('yarn.lock')); + expect(lockfile.get('a')).toEqual([ + { range: '^1', version: '1.0.1', dataKey: 'a@^1' }, + ]); + expect(lockfile.get('b')).toEqual([ + { range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' }, + { range: '^2.0.1', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' }, + { range: '^2', version: '2.0.0', dataKey: 'b@^2' }, + ]); + expect(lockfile.toString()).toBe(mockModern); + }); +}); + +describe('Lockfile advanced', () => { describe('diff', () => { const lockfileLegacyA = Lockfile.parse(`${LEGACY_HEADER} a@^1: diff --git a/packages/cli-node/src/monorepo/Lockfile.ts b/packages/cli-node/src/monorepo/Lockfile.ts index ff624191af..65e22dcf13 100644 --- a/packages/cli-node/src/monorepo/Lockfile.ts +++ b/packages/cli-node/src/monorepo/Lockfile.ts @@ -14,12 +14,22 @@ * limitations under the License. */ -import { parseSyml } from '@yarnpkg/parsers'; +import { parseSyml, stringifySyml } from '@yarnpkg/parsers'; +import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile'; import crypto from 'node:crypto'; import fs from 'fs-extra'; const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/; +// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-core/sources/Project.ts#L1741-L1746 +const NEW_HEADER = `${[ + `# This file is generated by running "yarn install" inside your project.\n`, + `# Manual changes might be lost - proceed with caution!\n`, +].join(``)}\n`; + +// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-parsers/sources/syml.ts#L136 +const LEGACY_REGEX = /^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i; + /** @internal */ type LockfileData = { [entry: string]: { @@ -97,6 +107,8 @@ export class Lockfile { * @public */ static parse(content: string): Lockfile { + const legacy = LEGACY_REGEX.test(content); + let data: LockfileData; try { data = parseSyml(content); @@ -130,18 +142,21 @@ export class Lockfile { } } - return new Lockfile(packages, data); + return new Lockfile(packages, data, legacy); } private readonly packages: Map; private readonly data: LockfileData; + private readonly legacy: boolean; private constructor( packages: Map, data: LockfileData, + legacy: boolean = false, ) { this.packages = packages; this.data = data; + this.legacy = legacy; } /** Returns the name of all packages available in the lockfile */ @@ -154,6 +169,15 @@ export class Lockfile { return this.packages.keys(); } + /** + * Serialize the lockfile back to a string. + */ + toString(): string { + return this.legacy + ? legacyStringifyLockfile(this.data) + : NEW_HEADER + stringifySyml(this.data); + } + /** * Creates a simplified dependency graph from the lockfile data, where each * key is a package, and the value is a set of all packages that it depends on diff --git a/packages/cli-node/src/monorepo/isMonoRepo.ts b/packages/cli-node/src/monorepo/isMonoRepo.ts index 93a5e48abb..ca6b209a7a 100644 --- a/packages/cli-node/src/monorepo/isMonoRepo.ts +++ b/packages/cli-node/src/monorepo/isMonoRepo.ts @@ -18,7 +18,10 @@ import { targetPaths } from '@backstage/cli-common'; import fs from 'fs-extra'; /** - * Returns try if the current project is a monorepo. + * Returns true if the current project is a monorepo. + * + * Uses a simple presence check on the `workspaces` field. Empty or invalid + * workspace config is treated as a monorepo; we do not validate patterns. * * @public */ @@ -26,7 +29,7 @@ export async function isMonoRepo(): Promise { const rootPackageJsonPath = targetPaths.resolveRoot('package.json'); try { const pkg = await fs.readJson(rootPackageJsonPath); - return Boolean(pkg?.workspaces?.packages); + return Boolean(pkg?.workspaces); } catch (error) { return false; } diff --git a/packages/cli-node/src/pacman/PackageManager.test.ts b/packages/cli-node/src/pacman/PackageManager.test.ts index ef169b1337..4f7446901f 100644 --- a/packages/cli-node/src/pacman/PackageManager.test.ts +++ b/packages/cli-node/src/pacman/PackageManager.test.ts @@ -52,6 +52,16 @@ describe('PackageManager', () => { await detectPackageManager(); expect(mockYarnCreate).toHaveBeenCalled(); }); + it('should detect via root package.json workspaces (array form)', async () => { + mockDir.setContent({ + 'package.json': JSON.stringify({ + name: 'foo', + workspaces: ['packages/*'], + }), + }); + await detectPackageManager(); + expect(mockYarnCreate).toHaveBeenCalled(); + }); it('should detect via root package.json packageManager', async () => { mockDir.setContent({ diff --git a/packages/cli-node/src/pacman/PackageManager.ts b/packages/cli-node/src/pacman/PackageManager.ts index b908e99c72..852ccc1288 100644 --- a/packages/cli-node/src/pacman/PackageManager.ts +++ b/packages/cli-node/src/pacman/PackageManager.ts @@ -49,12 +49,6 @@ export interface PackageManager { /** The file name of the lockfile used by the package manager. */ lockfileName(): string; - /** - * If this repo is a monorepo, returns the patterns specified by the package - * manager's monorepo configuration. Does not attempt to resolve any globs. - */ - getMonorepoPackages(): Promise; - /** Uses the package manager to run a command in the repo. */ run(args: string[], options?: RunOptions): Promise; diff --git a/packages/cli-node/src/pacman/yarn/Yarn.test.ts b/packages/cli-node/src/pacman/yarn/Yarn.test.ts deleted file mode 100644 index 42b5caa6c5..0000000000 --- a/packages/cli-node/src/pacman/yarn/Yarn.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { createMockDirectory } from '@backstage/backend-test-utils'; -import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; -import { Yarn } from './Yarn'; - -const mockDir = createMockDirectory(); -overrideTargetPaths(mockDir.path); - -const yarnClassic = new Yarn({ version: '1.0.0', codename: 'classic' }); -const yarnBerry = new Yarn({ version: '3.0.0', codename: 'berry' }); -const allYarnVersions = [yarnClassic, yarnBerry]; - -describe('Yarn', () => { - describe.each(allYarnVersions)('%s.getMonorepoPackages', yarn => { - it('should detect a monorepo', async () => { - mockDir.setContent({ - 'package.json': JSON.stringify({ - name: 'foo', - workspaces: { - packages: ['packages/*'], - }, - }), - }); - await expect(yarn.getMonorepoPackages()).resolves.toEqual(['packages/*']); - }); - - it('should detect a non-monorepo', async () => { - mockDir.setContent({ - 'package.json': JSON.stringify({ - name: 'foo', - }), - }); - await expect(yarn.getMonorepoPackages()).resolves.toEqual([]); - }); - - it('should return false if package.json is missing', async () => { - mockDir.setContent({}); - await expect(yarn.getMonorepoPackages()).resolves.toEqual([]); - }); - }); -}); diff --git a/packages/cli-node/src/pacman/yarn/Yarn.ts b/packages/cli-node/src/pacman/yarn/Yarn.ts index b153a6ef40..7aee7bdebf 100644 --- a/packages/cli-node/src/pacman/yarn/Yarn.ts +++ b/packages/cli-node/src/pacman/yarn/Yarn.ts @@ -22,8 +22,6 @@ import { import { PackageInfo, PackageManager } from '../PackageManager'; import { Lockfile } from '../Lockfile'; import { YarnVersion } from './types'; -import fs from 'fs-extra'; -import { targetPaths } from '@backstage/cli-common'; import { run, runOutput, RunOptions } from '@backstage/cli-common'; export class Yarn implements PackageManager { @@ -46,16 +44,6 @@ export class Yarn implements PackageManager { return 'yarn.lock'; } - async getMonorepoPackages() { - const rootPackageJsonPath = targetPaths.resolveRoot('package.json'); - try { - const pkg = await fs.readJson(rootPackageJsonPath); - return pkg?.workspaces?.packages || []; - } catch (error) { - return []; - } - } - async pack(out: string, packageDir: string) { const outArg = this.yarnVersion.codename === 'classic' ? '--filename' : '--out'; diff --git a/packages/cli/src/lib/versioning/index.ts b/packages/cli-node/src/yarn/index.ts similarity index 79% rename from packages/cli/src/lib/versioning/index.ts rename to packages/cli-node/src/yarn/index.ts index e0b9280ee6..871aa14ca8 100644 --- a/packages/cli/src/lib/versioning/index.ts +++ b/packages/cli-node/src/yarn/index.ts @@ -14,6 +14,4 @@ * limitations under the License. */ -export { Lockfile } from './Lockfile'; -export { fetchPackageInfo, mapDependencies } from './packages'; -export type { YarnInfoInspectData } from './packages'; +export { hasBackstageYarnPlugin } from './yarnPlugin'; diff --git a/packages/cli/src/lib/yarnPlugin.test.ts b/packages/cli-node/src/yarn/yarnPlugin.test.ts similarity index 79% rename from packages/cli/src/lib/yarnPlugin.test.ts rename to packages/cli-node/src/yarn/yarnPlugin.test.ts index a07c2a2ac6..ecbff45ba7 100644 --- a/packages/cli/src/lib/yarnPlugin.test.ts +++ b/packages/cli-node/src/yarn/yarnPlugin.test.ts @@ -16,12 +16,12 @@ import { createMockDirectory } from '@backstage/backend-test-utils'; import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; -import { getHasYarnPlugin } from './yarnPlugin'; +import { hasBackstageYarnPlugin } from './yarnPlugin'; const mockDir = createMockDirectory(); overrideTargetPaths(mockDir.path); -describe('getHasYarnPlugin', () => { +describe('hasBackstageYarnPlugin', () => { beforeEach(() => { mockDir.clear(); }); @@ -29,7 +29,7 @@ describe('getHasYarnPlugin', () => { it('should return false when .yarnrc.yml does not exist', async () => { mockDir.setContent({}); - const result = await getHasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(false); }); @@ -38,7 +38,7 @@ describe('getHasYarnPlugin', () => { '.yarnrc.yml': '', }); - const result = await getHasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(false); }); @@ -47,7 +47,7 @@ describe('getHasYarnPlugin', () => { '.yarnrc.yml': 'plugins: []', }); - const result = await getHasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(false); }); @@ -60,7 +60,7 @@ plugins: `, }); - const result = await getHasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(false); }); @@ -74,7 +74,7 @@ plugins: `, }); - const result = await getHasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(true); }); @@ -86,7 +86,7 @@ plugins: `, }); - const result = await getHasYarnPlugin(); + const result = await hasBackstageYarnPlugin(); expect(result).toBe(true); }); @@ -95,7 +95,7 @@ plugins: '.yarnrc.yml': 'invalid: yaml: content: [', }); - await expect(getHasYarnPlugin()).rejects.toThrow(); + await expect(hasBackstageYarnPlugin()).rejects.toThrow(); }); it('should throw error when .yarnrc.yml has unexpected structure', async () => { @@ -105,21 +105,22 @@ plugins: "not an array" `, }); - await expect(getHasYarnPlugin()).rejects.toThrow( + await expect(hasBackstageYarnPlugin()).rejects.toThrow( 'Unexpected content in .yarnrc.yml', ); }); - it('should handle plugins with different structure', async () => { + it('should resolve from a custom workspace directory', async () => { mockDir.setContent({ - '.yarnrc.yml': ` + 'custom-dir': { + '.yarnrc.yml': ` plugins: - path: .yarn/plugins/@yarnpkg/plugin-backstage.cjs - - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs `, + }, }); - const result = await getHasYarnPlugin(); + const result = await hasBackstageYarnPlugin(mockDir.resolve('custom-dir')); expect(result).toBe(true); }); }); diff --git a/packages/cli/src/lib/yarnPlugin.ts b/packages/cli-node/src/yarn/yarnPlugin.ts similarity index 77% rename from packages/cli/src/lib/yarnPlugin.ts rename to packages/cli-node/src/yarn/yarnPlugin.ts index 0390345651..65383edd0c 100644 --- a/packages/cli/src/lib/yarnPlugin.ts +++ b/packages/cli-node/src/yarn/yarnPlugin.ts @@ -15,6 +15,7 @@ */ import fs from 'fs-extra'; +import { resolve as resolvePath } from 'node:path'; import yaml from 'yaml'; import z from 'zod'; import { targetPaths } from '@backstage/cli-common'; @@ -30,15 +31,21 @@ const yarnRcSchema = z.object({ }); /** - * Detects whether the Backstage Yarn plugin is installed in the target repository. + * Detects whether the Backstage Yarn plugin is installed in the given workspace directory. * - * @returns Promise - true if the plugin is installed, false otherwise + * @param workspaceDir - The workspace root directory to check. Defaults to the target root. + * @returns Promise resolving to true if the plugin is installed, false otherwise + * @public */ -export async function getHasYarnPlugin(): Promise { - const yarnRcPath = targetPaths.resolveRoot('.yarnrc.yml'); +export async function hasBackstageYarnPlugin( + workspaceDir?: string, +): Promise { + const yarnRcPath = resolvePath( + workspaceDir ?? targetPaths.rootDir, + '.yarnrc.yml', + ); const yarnRcContent = await fs.readFile(yarnRcPath, 'utf-8').catch(e => { if (e.code === 'ENOENT') { - // gracefully continue in case the file doesn't exist return ''; } throw e; diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 0fe123854c..4d5c2691a7 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/cli +## 0.36.0-next.1 + +### Minor Changes + +- b36a60d: **BREAKING**: The `migrate package-exports` command has been removed. Use `repo fix` instead. + +### Patch Changes + +- 0d2d0f2: Internal refactor of CLI modularization, moving individual commands to be implemented with cleye. +- 2fcba39: Internal refactor to move shared utilities into their consuming modules, reducing cross-module dependencies. +- c85ac86: Internal refactor to split `loadCliConfig` into separate implementations for the build and config CLI modules, removing a cross-module dependency. +- 61cb976: Migrated internal versioning utilities to use `@backstage/cli-node` instead of a local implementation. +- 825c81d: Internal refactor of CLI command modules. +- a9d23c4: Properly support `package.json` `workspaces` field +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + - @backstage/cli-node@0.2.19-next.1 + - @backstage/module-federation-common@0.1.2-next.0 + - @backstage/integration@2.0.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/errors@1.2.7 + - @backstage/eslint-plugin@0.2.2-next.0 + - @backstage/release-manifests@0.0.13 + - @backstage/types@1.2.2 + +## 0.35.5-next.0 + +### Patch Changes + +- 246877a: Updated dependency `bfj` to `^9.0.2`. +- bba2e49: Internal refactor to use new concurrency utilities from `@backstage/cli-node`. +- fd50cb3: Added `translations export` and `translations import` commands for managing translation files. + + The `translations export` command discovers all `TranslationRef` definitions across frontend plugin dependencies and exports their default messages as JSON files. The `translations import` command generates `TranslationResource` wiring code from translated JSON files, ready to be plugged into the app. + + Both commands support a `--pattern` option for controlling the message file layout, for example `--pattern '{lang}/{id}.json'` for language-based directory grouping. + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- 092b41f: Updated dependency `webpack` to `~5.105.0`. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/cli-node@0.2.19-next.0 + - @backstage/eslint-plugin@0.2.2-next.0 + - @backstage/integration@1.21.0-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/module-federation-common@0.1.0 + - @backstage/release-manifests@0.0.13 + - @backstage/types@1.2.2 + ## 0.35.4 ### Patch Changes diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index ee4b03cbca..591b32ead7 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -57,81 +57,75 @@ Commands: ### `backstage-cli config docs` ``` -Usage: backstage-cli config docs [options] +Usage: backstage-cli config docs Options: - --package + --package -h, --help ``` ### `backstage-cli config schema` ``` -Usage: +Usage: backstage-cli config schema Options: - --format - --help + --format --merge - --no-merge - --package - --version + --package + -h, --help ``` ### `backstage-cli config:check` ``` -Usage: +Usage: backstage-cli config:check Options: - --config + --config --deprecated --frontend - --help --lax - --package + --package --strict - --version + -h, --help ``` ### `backstage-cli config:docs` ``` -Usage: program [options] +Usage: backstage-cli config:docs Options: - --package + --package -h, --help ``` ### `backstage-cli config:print` ``` -Usage: +Usage: backstage-cli config:print Options: - --config - --format + --config + --format --frontend - --help --lax - --package - --version + --package --with-secrets + -h, --help ``` ### `backstage-cli config:schema` ``` -Usage: +Usage: backstage-cli config:schema Options: - --format - --help + --format --merge - --no-merge - --package - --version + --package + -h, --help ``` ### `backstage-cli create-github-app` @@ -146,13 +140,12 @@ Options: ### `backstage-cli info` ``` -Usage: +Usage: backstage-cli info Options: - --format - --help - --include - --version + --format + --include + -h, --help ``` ### `backstage-cli migrate` @@ -175,7 +168,7 @@ Commands: ### `backstage-cli migrate package-exports` ``` -Usage: program [options] +Usage: backstage-cli migrate package-exports Options: -h, --help @@ -184,7 +177,7 @@ Options: ### `backstage-cli migrate package-lint-configs` ``` -Usage: program [options] +Usage: backstage-cli migrate package-lint-configs Options: -h, --help @@ -193,7 +186,7 @@ Options: ### `backstage-cli migrate package-roles` ``` -Usage: program [options] +Usage: backstage-cli migrate package-roles Options: -h, --help @@ -202,7 +195,7 @@ Options: ### `backstage-cli migrate package-scripts` ``` -Usage: program [options] +Usage: backstage-cli migrate package-scripts Options: -h, --help @@ -211,7 +204,7 @@ Options: ### `backstage-cli migrate react-router-deps` ``` -Usage: program [options] +Usage: backstage-cli migrate react-router-deps Options: -h, --help @@ -271,7 +264,7 @@ Options: ### `backstage-cli package clean` ``` -Usage: program [options] +Usage: backstage-cli package clean Options: -h, --help @@ -293,7 +286,7 @@ Options: ### `backstage-cli package postpack` ``` -Usage: program [options] +Usage: backstage-cli package postpack Options: -h, --help @@ -302,7 +295,7 @@ Options: ### `backstage-cli package prepack` ``` -Usage: program [options] +Usage: backstage-cli package prepack Options: -h, --help @@ -472,7 +465,7 @@ Options: ### `backstage-cli repo clean` ``` -Usage: program [options] +Usage: backstage-cli repo clean Options: -h, --help @@ -560,25 +553,23 @@ Commands: ### `backstage-cli translations export` ``` -Usage: +Usage: backstage-cli translations export Options: - --help - --output - --pattern - --version + --output + --pattern + -h, --help ``` ### `backstage-cli translations import` ``` -Usage: +Usage: backstage-cli translations import Options: - --help - --input - --output - --version + --input + --output + -h, --help ``` ### `backstage-cli versions:bump` diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index 814f017e1d..c670f0fe8b 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -336,8 +336,8 @@ async function getRootConfig() { rejectFrontendNetworkRequests, }; - const workspacePatterns = - rootPkgJson.workspaces && rootPkgJson.workspaces.packages; + const ws = rootPkgJson.workspaces; + const workspacePatterns = Array.isArray(ws) ? ws : ws?.packages; // Check if we're running within a specific monorepo package. In that case just get the single project config. if (!workspacePatterns || paths.targetRoot !== paths.targetDir) { diff --git a/packages/cli/package.json b/packages/cli/package.json index 9e1d0b77f0..4350836d22 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli", - "version": "0.35.4", + "version": "0.36.0-next.1", "description": "CLI for developing Backstage plugins and apps", "backstage": { "role": "cli" @@ -77,12 +77,11 @@ "@types/webpack-env": "^1.15.2", "@typescript-eslint/eslint-plugin": "^8.17.0", "@typescript-eslint/parser": "^8.16.0", - "@yarnpkg/lockfile": "^1.1.0", - "@yarnpkg/parsers": "^3.0.0", "bfj": "^9.0.2", "buffer": "^6.0.3", "chalk": "^4.0.0", "chokidar": "^3.3.1", + "cleye": "^2.2.1", "commander": "^14.0.3", "cross-fetch": "^4.0.0", "cross-spawn": "^7.0.3", @@ -182,7 +181,6 @@ "@types/tar": "^6.1.1", "@types/terser-webpack-plugin": "^5.0.4", "@types/webpack-sources": "^3.2.3", - "@types/yarnpkg__lockfile": "^1.1.4", "del": "^8.0.0", "esbuild-loader": "^4.0.0", "eslint-webpack-plugin": "^4.2.0", diff --git a/packages/cli/src/lib/versioning/Lockfile.test.ts b/packages/cli/src/lib/versioning/Lockfile.test.ts deleted file mode 100644 index d6996b9143..0000000000 --- a/packages/cli/src/lib/versioning/Lockfile.test.ts +++ /dev/null @@ -1,102 +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 { Lockfile } from './Lockfile'; -import { createMockDirectory } from '@backstage/backend-test-utils'; - -const LEGACY_HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - -`; - -const MODERN_HEADER = `# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - -__metadata: - version: 6 - cacheKey: 8 -`; - -const mockA = `${LEGACY_HEADER} -a@^1: - version "1.0.1" - resolved "https://my-registry/a-1.0.01.tgz#abc123" - integrity sha512-xyz - dependencies: - b "^2" - -b@2.0.x: - version "2.0.1" - -b@^2: - version "2.0.0" -`; - -describe('Lockfile', () => { - const mockDir = createMockDirectory(); - - it('should load and serialize mockA', async () => { - mockDir.setContent({ - 'yarn.lock': mockA, - }); - - const lockfile = await Lockfile.load(mockDir.resolve('yarn.lock')); - expect(lockfile.get('a')).toEqual([ - { range: '^1', version: '1.0.1', dataKey: 'a@^1' }, - ]); - expect(lockfile.get('b')).toEqual([ - { range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x' }, - { range: '^2', version: '2.0.0', dataKey: 'b@^2' }, - ]); - expect(lockfile.toString()).toBe(mockA); - }); -}); - -const mockANew = `${MODERN_HEADER} -a@^1: - version: 1.0.1 - dependencies: - b: ^2 - integrity: sha512-xyz - resolved: "https://my-registry/a-1.0.01.tgz#abc123" - -"b@2.0.x, b@^2.0.1": - version: 2.0.1 - -b@^2: - version: 2.0.0 -`; - -describe('New Lockfile', () => { - const mockDir = createMockDirectory(); - - it('should load and serialize mockANew', async () => { - mockDir.setContent({ - 'yarn.lock': mockANew, - }); - - const lockfile = await Lockfile.load(mockDir.resolve('yarn.lock')); - expect(lockfile.get('a')).toEqual([ - { range: '^1', version: '1.0.1', dataKey: 'a@^1' }, - ]); - expect(lockfile.get('b')).toEqual([ - { range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' }, - { range: '^2.0.1', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' }, - { range: '^2', version: '2.0.0', dataKey: 'b@^2' }, - ]); - expect(lockfile.toString()).toBe(mockANew); - }); -}); diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts deleted file mode 100644 index 7a9a2c801a..0000000000 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ /dev/null @@ -1,138 +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 fs from 'fs-extra'; -import { parseSyml, stringifySyml } from '@yarnpkg/parsers'; -import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile'; - -const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/; - -type LockfileData = { - [entry: string]: { - version: string; - resolved?: string; - integrity?: string /* old */; - checksum?: string /* new */; - dependencies?: { [name: string]: string }; - peerDependencies?: { [name: string]: string }; - }; -}; - -type LockfileQueryEntry = { - range: string; - version: string; - dataKey: string; -}; - -// the new yarn header is handled out of band of the parsing -// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-core/sources/Project.ts#L1741-L1746 -const NEW_HEADER = `${[ - `# This file is generated by running "yarn install" inside your project.\n`, - `# Manual changes might be lost - proceed with caution!\n`, -].join(``)}\n`; - -// taken from yarn parser package -// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-parsers/sources/syml.ts#L136 -const LEGACY_REGEX = /^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i; - -// these are special top level yarn keys. -// https://github.com/yarnpkg/berry/blob/9bd61fbffb83d0b8166a9cc26bec3a58743aa453/packages/yarnpkg-parsers/sources/syml.ts#L9 -const SPECIAL_OBJECT_KEYS = [ - `__metadata`, - `version`, - `resolution`, - `dependencies`, - `peerDependencies`, - `dependenciesMeta`, - `peerDependenciesMeta`, - `binaries`, -]; - -export class Lockfile { - static async load(path: string) { - const lockfileContents = await fs.readFile(path, 'utf8'); - return Lockfile.parse(lockfileContents); - } - - static parse(content: string) { - const legacy = LEGACY_REGEX.test(content); - - let data: LockfileData; - try { - data = parseSyml(content); - } catch (err) { - throw new Error(`Failed yarn.lock parse, ${err}`); - } - - const packages = new Map(); - - for (const [key, value] of Object.entries(data)) { - if (SPECIAL_OBJECT_KEYS.includes(key)) continue; - - const [, name, ranges] = ENTRY_PATTERN.exec(key) ?? []; - if (!name) { - throw new Error(`Failed to parse yarn.lock entry '${key}'`); - } - - let queries = packages.get(name); - if (!queries) { - queries = []; - packages.set(name, queries); - } - for (let range of ranges.split(/\s*,\s*/)) { - if (range.startsWith(`${name}@`)) { - range = range.slice(`${name}@`.length); - } - if (range.startsWith('npm:')) { - range = range.slice('npm:'.length); - } - queries.push({ range, version: value.version, dataKey: key }); - } - } - - return new Lockfile(packages, data, legacy); - } - - private readonly packages: Map; - private readonly data: LockfileData; - private readonly legacy: boolean; - - private constructor( - packages: Map, - data: LockfileData, - legacy: boolean = false, - ) { - this.packages = packages; - this.data = data; - this.legacy = legacy; - } - - /** Get the entries for a single package in the lockfile */ - get(name: string): LockfileQueryEntry[] | undefined { - return this.packages.get(name); - } - - /** Returns the name of all packages available in the lockfile */ - keys(): IterableIterator { - return this.packages.keys(); - } - - toString() { - return this.legacy - ? legacyStringifyLockfile(this.data) - : NEW_HEADER + stringifySyml(this.data); - } -} diff --git a/packages/cli/src/modules/maintenance/commands/package/clean.ts b/packages/cli/src/modules/build/commands/package/clean.ts similarity index 81% rename from packages/cli/src/modules/maintenance/commands/package/clean.ts rename to packages/cli/src/modules/build/commands/package/clean.ts index 70d7ea3771..f222c23666 100644 --- a/packages/cli/src/modules/maintenance/commands/package/clean.ts +++ b/packages/cli/src/modules/build/commands/package/clean.ts @@ -14,11 +14,14 @@ * limitations under the License. */ +import { cli } from 'cleye'; import fs from 'fs-extra'; import { targetPaths } from '@backstage/cli-common'; +import type { CommandContext } from '../../../../wiring/types'; -export default async function clean() { +export default async ({ args, info }: CommandContext) => { + cli({ help: info }, undefined, args); await fs.remove(targetPaths.resolve('dist')); await fs.remove(targetPaths.resolve('dist-types')); await fs.remove(targetPaths.resolve('coverage')); -} +}; diff --git a/packages/integration/src/bitbucket/index.ts b/packages/cli/src/modules/build/commands/package/postpack.ts similarity index 62% rename from packages/integration/src/bitbucket/index.ts rename to packages/cli/src/modules/build/commands/package/postpack.ts index 356e760fd8..8ca1c800ce 100644 --- a/packages/integration/src/bitbucket/index.ts +++ b/packages/cli/src/modules/build/commands/package/postpack.ts @@ -14,15 +14,12 @@ * limitations under the License. */ -export { BitbucketIntegration } from './BitbucketIntegration'; -export { - readBitbucketIntegrationConfig, - readBitbucketIntegrationConfigs, -} from './config'; -export type { BitbucketIntegrationConfig } from './config'; -export { - getBitbucketDefaultBranch, - getBitbucketDownloadUrl, - getBitbucketFileFetchUrl, - getBitbucketRequestOptions, -} from './core'; +import { cli } from 'cleye'; +import { targetPaths } from '@backstage/cli-common'; +import { revertProductionPack } from '../../lib/packager/productionPack'; +import type { CommandContext } from '../../../../wiring/types'; + +export default async ({ args, info }: CommandContext) => { + cli({ help: info }, undefined, args); + await revertProductionPack(targetPaths.dir); +}; diff --git a/packages/cli/src/modules/maintenance/commands/package/pack.ts b/packages/cli/src/modules/build/commands/package/prepack.ts similarity index 75% rename from packages/cli/src/modules/maintenance/commands/package/pack.ts rename to packages/cli/src/modules/build/commands/package/prepack.ts index e5e176d17e..39d92518bc 100644 --- a/packages/cli/src/modules/maintenance/commands/package/pack.ts +++ b/packages/cli/src/modules/build/commands/package/prepack.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import { - productionPack, - revertProductionPack, -} from '../../../../modules/build/lib/packager/productionPack'; -import { targetPaths } from '@backstage/cli-common'; - +import { cli } from 'cleye'; import fs from 'fs-extra'; +import { targetPaths } from '@backstage/cli-common'; +import { productionPack } from '../../lib/packager/productionPack'; import { publishPreflightCheck } from '../../lib/publishing'; -import { createTypeDistProject } from '../../../../lib/typeDistProject'; +import { createTypeDistProject } from '../../lib/typeDistProject'; +import type { CommandContext } from '../../../../wiring/types'; + +export default async ({ args, info }: CommandContext) => { + cli({ help: info }, undefined, args); -export const pre = async () => { publishPreflightCheck({ dir: targetPaths.dir, packageJson: await fs.readJson(targetPaths.resolve('package.json')), @@ -35,7 +35,3 @@ export const pre = async () => { featureDetectionProject: await createTypeDistProject(), }); }; - -export const post = async () => { - await revertProductionPack(targetPaths.dir); -}; diff --git a/packages/cli/src/modules/build/commands/package/start/startFrontend.ts b/packages/cli/src/modules/build/commands/package/start/startFrontend.ts index c81fd2fd66..689cd20f7a 100644 --- a/packages/cli/src/modules/build/commands/package/start/startFrontend.ts +++ b/packages/cli/src/modules/build/commands/package/start/startFrontend.ts @@ -19,11 +19,11 @@ import { resolve as resolvePath } from 'node:path'; import { getModuleFederationRemoteOptions, serveBundle, -} from '../../../../build/lib/bundler'; +} from '../../../lib/bundler'; import { targetPaths } from '@backstage/cli-common'; import { BackstagePackageJson } from '@backstage/cli-node'; -import { hasReactDomClient } from '../../../../build/lib/bundler/hasReactDomClient'; +import { hasReactDomClient } from '../../../lib/bundler/hasReactDomClient'; interface StartAppOptions { verifyVersions?: boolean; diff --git a/packages/cli/src/modules/build/commands/repo/build.ts b/packages/cli/src/modules/build/commands/repo/build.ts index dd65e926b2..59d0a839e4 100644 --- a/packages/cli/src/modules/build/commands/repo/build.ts +++ b/packages/cli/src/modules/build/commands/repo/build.ts @@ -28,7 +28,7 @@ import { } from '@backstage/cli-node'; import { buildFrontend } from '../../lib/buildFrontend'; import { buildBackend } from '../../lib/buildBackend'; -import { createScriptOptionsParser } from '../../../../lib/optionsParser'; +import { createScriptOptionsParser } from '../../lib/optionsParser'; export async function command(opts: OptionValues, cmd: Command): Promise { let packages = await PackageGraph.listTargetPackages(); diff --git a/packages/cli/src/modules/maintenance/commands/repo/clean.ts b/packages/cli/src/modules/build/commands/repo/clean.ts similarity index 89% rename from packages/cli/src/modules/maintenance/commands/repo/clean.ts rename to packages/cli/src/modules/build/commands/repo/clean.ts index 28b123fa9e..efafdae510 100644 --- a/packages/cli/src/modules/maintenance/commands/repo/clean.ts +++ b/packages/cli/src/modules/build/commands/repo/clean.ts @@ -14,13 +14,15 @@ * limitations under the License. */ +import { cli } from 'cleye'; import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { PackageGraph } from '@backstage/cli-node'; - import { run, targetPaths } from '@backstage/cli-common'; +import type { CommandContext } from '../../../../wiring/types'; -export async function command(): Promise { +export default async ({ args, info }: CommandContext) => { + cli({ help: info }, undefined, args); const packages = await PackageGraph.listTargetPackages(); await fs.remove(targetPaths.resolveRoot('dist')); @@ -48,4 +50,4 @@ export async function command(): Promise { } }), ); -} +}; diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli/src/modules/build/index.ts index 9b37efaa8b..1e8d1d2060 100644 --- a/packages/cli/src/modules/build/index.ts +++ b/packages/cli/src/modules/build/index.ts @@ -16,8 +16,14 @@ import { Command, Option } from 'commander'; import { createCliPlugin } from '../../wiring/factory'; -import { lazy } from '../../lib/lazy'; -import { configOption } from '../config'; +import { lazy } from '../../wiring/lazy'; + +const configOption = [ + '--config ', + 'Config files to load instead of app-config.yaml', + (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]), + Array(), +] as const; export function registerPackageCommands(command: Command) { command @@ -197,6 +203,38 @@ export const buildPlugin = createCliPlugin({ }, }); + reg.addCommand({ + path: ['package', 'clean'], + description: 'Delete cache directories', + execute: { + loader: () => import('./commands/package/clean'), + }, + }); + + reg.addCommand({ + path: ['package', 'prepack'], + description: 'Prepares a package for packaging before publishing', + execute: { + loader: () => import('./commands/package/prepack'), + }, + }); + + reg.addCommand({ + path: ['package', 'postpack'], + description: 'Restores the changes made by the prepack command', + execute: { + loader: () => import('./commands/package/postpack'), + }, + }); + + reg.addCommand({ + path: ['repo', 'clean'], + description: 'Delete cache and output directories', + execute: { + loader: () => import('./commands/repo/clean'), + }, + }); + reg.addCommand({ path: ['build-workspace'], description: diff --git a/packages/cli/src/lib/__testUtils__/createFeatureEnvironment.ts b/packages/cli/src/modules/build/lib/__testUtils__/createFeatureEnvironment.ts similarity index 100% rename from packages/cli/src/lib/__testUtils__/createFeatureEnvironment.ts rename to packages/cli/src/modules/build/lib/__testUtils__/createFeatureEnvironment.ts diff --git a/packages/cli/src/modules/build/lib/buildFrontend.ts b/packages/cli/src/modules/build/lib/buildFrontend.ts index 6a5785a5b9..7ca337cfdb 100644 --- a/packages/cli/src/modules/build/lib/buildFrontend.ts +++ b/packages/cli/src/modules/build/lib/buildFrontend.ts @@ -18,7 +18,7 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { buildBundle, getModuleFederationRemoteOptions } from './bundler'; import { BackstagePackageJson } from '@backstage/cli-node'; -import { loadCliConfig } from '../../config/lib/config'; +import { loadCliConfig } from './config'; interface BuildAppOptions { targetDir: string; diff --git a/packages/cli/src/modules/build/lib/bundler/config.ts b/packages/cli/src/modules/build/lib/bundler/config.ts index 1fe81cbec8..c3676ecf70 100644 --- a/packages/cli/src/modules/build/lib/bundler/config.ts +++ b/packages/cli/src/modules/build/lib/bundler/config.ts @@ -32,7 +32,7 @@ import pickBy from 'lodash/pickBy'; import { runOutput, targetPaths } from '@backstage/cli-common'; import { transforms } from './transforms'; -import { version } from '../../../../lib/version'; +import { version } from '../../../../wiring/version'; import yn from 'yn'; import { hasReactDomClient } from './hasReactDomClient'; import { createWorkspaceLinkingPlugins } from './linkWorkspaces'; diff --git a/packages/cli/src/modules/build/lib/bundler/moduleFederation.ts b/packages/cli/src/modules/build/lib/bundler/moduleFederation.ts index 307cb5cebc..69e1415b5d 100644 --- a/packages/cli/src/modules/build/lib/bundler/moduleFederation.ts +++ b/packages/cli/src/modules/build/lib/bundler/moduleFederation.ts @@ -20,7 +20,7 @@ import { readEntryPoints } from '../entryPoints'; import { createTypeDistProject, getEntryPointDefaultFeatureType, -} from '../../../../lib/typeDistProject'; +} from '../typeDistProject'; import { BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL, defaultRemoteSharedDependencies, diff --git a/packages/cli/src/modules/build/lib/bundler/server.ts b/packages/cli/src/modules/build/lib/bundler/server.ts index 57d6bed052..e474157c7c 100644 --- a/packages/cli/src/modules/build/lib/bundler/server.ts +++ b/packages/cli/src/modules/build/lib/bundler/server.ts @@ -24,7 +24,7 @@ import { RspackDevServer } from '@rspack/dev-server'; import { targetPaths } from '@backstage/cli-common'; -import { loadCliConfig } from '../../../config/lib/config'; +import { loadCliConfig } from '../config'; import { createConfig, resolveBaseUrl, resolveEndpoint } from './config'; import { createDetectedModulesEntryPoint } from './packageDetection'; import { resolveBundlingPaths, resolveOptionalBundlingPaths } from './paths'; diff --git a/packages/cli/src/modules/build/lib/config.ts b/packages/cli/src/modules/build/lib/config.ts new file mode 100644 index 0000000000..56bfc68ced --- /dev/null +++ b/packages/cli/src/modules/build/lib/config.ts @@ -0,0 +1,128 @@ +/* + * 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 { ConfigSources, loadConfigSchema } from '@backstage/config-loader'; +import { AppConfig, ConfigReader } from '@backstage/config'; +import { targetPaths } from '@backstage/cli-common'; + +import { getPackages } from '@manypkg/get-packages'; +import { PackageGraph } from '@backstage/cli-node'; +import { resolve as resolvePath } from 'node:path'; + +type Options = { + args: string[]; + targetDir?: string; + fromPackage?: string; + withFilteredKeys?: boolean; + watch?: (newFrontendAppConfigs: AppConfig[]) => void; +}; + +export async function loadCliConfig(options: Options) { + const targetDir = options.targetDir ?? targetPaths.dir; + + const { packages } = await getPackages(targetDir); + + let localPackageNames; + if (options.fromPackage) { + if (packages.length) { + const graph = PackageGraph.fromPackages(packages); + localPackageNames = Array.from( + graph.collectPackageNames([options.fromPackage], node => { + // Workaround for Backstage main repo only, since the CLI has some artificial devDependencies + if (node.name === '@backstage/cli') { + return undefined; + } + return node.localDependencies.keys(); + }), + ); + } else { + localPackageNames = [options.fromPackage]; + } + } else { + localPackageNames = packages.map(p => p.packageJson.name); + } + + const schema = await loadConfigSchema({ + dependencies: localPackageNames, + packagePaths: [targetPaths.resolveRoot('package.json')], + }); + + const source = ConfigSources.default({ + allowMissingDefaultConfig: true, + watch: Boolean(options.watch), + rootDir: targetPaths.rootDir, + argv: options.args.flatMap(t => ['--config', resolvePath(targetDir, t)]), + }); + + const appConfigs = await new Promise((resolve, reject) => { + async function loadConfigReaderLoop() { + let loaded = false; + + try { + const abortController = new AbortController(); + for await (const { configs } of source.readConfigData({ + signal: abortController.signal, + })) { + if (loaded) { + const newFrontendAppConfigs = schema.process(configs, { + visibility: ['frontend'], + withFilteredKeys: options.withFilteredKeys, + ignoreSchemaErrors: true, + }); + options.watch?.(newFrontendAppConfigs); + } else { + resolve(configs); + loaded = true; + + if (!options.watch) { + abortController.abort(); + } + } + } + } catch (error) { + if (loaded) { + console.error(`Failed to reload configuration, ${error}`); + } else { + reject(error); + } + } + } + loadConfigReaderLoop(); + }); + + const configurationLoadedMessage = appConfigs.length + ? `Loaded config from ${appConfigs.map(c => c.context).join(', ')}` + : `No configuration files found, running without config`; + + process.stderr.write(`${configurationLoadedMessage}\n`); + + const frontendAppConfigs = schema.process(appConfigs, { + visibility: ['frontend'], + withFilteredKeys: options.withFilteredKeys, + ignoreSchemaErrors: true, + }); + const frontendConfig = ConfigReader.fromConfigs(frontendAppConfigs); + + const fullConfig = ConfigReader.fromConfigs(appConfigs); + + return { + schema, + appConfigs, + frontendConfig, + frontendAppConfigs, + fullConfig, + }; +} diff --git a/packages/cli/src/lib/optionsParser.ts b/packages/cli/src/modules/build/lib/optionsParser.ts similarity index 95% rename from packages/cli/src/lib/optionsParser.ts rename to packages/cli/src/modules/build/lib/optionsParser.ts index 45c7eac0aa..15561a2e42 100644 --- a/packages/cli/src/lib/optionsParser.ts +++ b/packages/cli/src/modules/build/lib/optionsParser.ts @@ -62,8 +62,8 @@ export function createScriptOptionsParser( // Triggers the writing of options to the result object cmd.parseOptions(argsStr.split(' ')); - (cmd as any)._storeOptionsAsProperties = currentOpts; - (cmd as any)._optionValues = currentStore; + (cmd as any)._optionValues = currentOpts; + (cmd as any)._storeOptionsAsProperties = currentStore; return result; }; diff --git a/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts b/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts index 50fb0f4e31..9f63525186 100644 --- a/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts +++ b/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts @@ -44,7 +44,7 @@ import { PackageGraphNode, runConcurrentTasks, } from '@backstage/cli-node'; -import { createTypeDistProject } from '../../../../lib/typeDistProject'; +import { createTypeDistProject } from '../typeDistProject'; // These packages aren't safe to pack in parallel since the CLI depends on them const UNSAFE_PACKAGES = [ diff --git a/packages/cli/src/modules/build/lib/packager/productionPack.ts b/packages/cli/src/modules/build/lib/packager/productionPack.ts index ce9f15388c..55eb6edc48 100644 --- a/packages/cli/src/modules/build/lib/packager/productionPack.ts +++ b/packages/cli/src/modules/build/lib/packager/productionPack.ts @@ -19,7 +19,7 @@ import npmPackList from 'npm-packlist'; import { resolve as resolvePath, posix as posixPath } from 'node:path'; import { BackstagePackageJson } from '@backstage/cli-node'; import { readEntryPoints } from '../entryPoints'; -import { getEntryPointDefaultFeatureType } from '../../../../lib/typeDistProject'; +import { getEntryPointDefaultFeatureType } from '../typeDistProject'; import { Project } from 'ts-morph'; const PKG_PATH = 'package.json'; diff --git a/packages/cli/src/modules/maintenance/lib/publishing.ts b/packages/cli/src/modules/build/lib/publishing.ts similarity index 100% rename from packages/cli/src/modules/maintenance/lib/publishing.ts rename to packages/cli/src/modules/build/lib/publishing.ts diff --git a/packages/cli/src/lib/typeDistProject.test.ts b/packages/cli/src/modules/build/lib/typeDistProject.test.ts similarity index 100% rename from packages/cli/src/lib/typeDistProject.test.ts rename to packages/cli/src/modules/build/lib/typeDistProject.test.ts diff --git a/packages/cli/src/lib/typeDistProject.ts b/packages/cli/src/modules/build/lib/typeDistProject.ts similarity index 100% rename from packages/cli/src/lib/typeDistProject.ts rename to packages/cli/src/modules/build/lib/typeDistProject.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/.gitignore b/packages/cli/src/modules/build/tests/transforms/__fixtures__/.gitignore similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/.gitignore rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/.gitignore diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-default.js b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-default.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-default.js rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-default.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-named.js b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-named.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-named.js rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/a-named.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-default.mjs b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-default.mjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-default.mjs rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-default.mjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-named.mjs b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-named.mjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-named.mjs rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/b-named.mjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-default.cjs b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-default.cjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-default.cjs rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-default.cjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-named.cjs b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-named.cjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-named.cjs rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/c-named.cjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.d.ts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.d.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.d.ts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.d.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.js b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.js rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/main.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/package.json b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/package.json similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-commonjs/package.json rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-commonjs/package.json diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/a-default.js b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/a-default.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/a-default.js rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/a-default.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/a-named.js b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/a-named.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/a-named.js rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/a-named.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/b-default.mjs b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/b-default.mjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/b-default.mjs rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/b-default.mjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/b-named.mjs b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/b-named.mjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/b-named.mjs rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/b-named.mjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/c-default.cjs b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/c-default.cjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/c-default.cjs rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/c-default.cjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/c-named.cjs b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/c-named.cjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/c-named.cjs rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/c-named.cjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/main.d.ts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/main.d.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/main.d.ts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/main.d.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/main.js b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/main.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/main.js rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/main.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/package.json b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/package.json similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-default/package.json rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-default/package.json diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/a-default.js b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/a-default.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/a-default.js rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/a-default.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/a-named.js b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/a-named.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/a-named.js rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/a-named.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/b-default.mjs b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/b-default.mjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/b-default.mjs rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/b-default.mjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/b-named.mjs b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/b-named.mjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/b-named.mjs rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/b-named.mjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/c-default.cjs b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/c-default.cjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/c-default.cjs rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/c-default.cjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/c-named.cjs b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/c-named.cjs similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/c-named.cjs rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/c-named.cjs diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/main.d.ts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/main.d.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/main.d.ts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/main.d.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/main.js b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/main.js similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/main.js rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/main.js diff --git a/packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/package.json b/packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/package.json similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/node_modules/dep-module/package.json rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/node_modules/dep-module/package.json diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/a-default.ts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/a-default.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/a-default.ts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/a-default.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/a-named.ts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/a-named.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/a-named.ts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/a-named.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/b-default.mts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/b-default.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/b-default.mts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/b-default.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/b-named.mts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/b-named.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/b-named.mts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/b-named.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/c-default.cts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/c-default.cts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/c-default.cts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/c-default.cts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/c-named.cts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/c-named.cts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/c-named.cts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/c-named.cts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/main.ts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/main.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/main.ts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/main.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/package.json b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/package.json similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/package.json rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/package.json diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/print.ts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/print.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-commonjs/print.ts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-commonjs/print.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/a-default.ts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/a-default.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/a-default.ts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/a-default.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/a-named.ts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/a-named.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/a-named.ts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/a-named.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/b-default.mts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/b-default.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/b-default.mts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/b-default.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/b-named.mts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/b-named.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/b-named.mts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/b-named.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/c-default.cts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/c-default.cts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/c-default.cts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/c-default.cts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/c-named.cts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/c-named.cts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/c-named.cts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/c-named.cts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/main.ts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/main.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/main.ts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/main.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/package.json b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/package.json similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/package.json rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/package.json diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-default/print.ts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/print.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-default/print.ts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-default/print.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/a-default-explicit.mts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/a-default-explicit.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/a-default-explicit.mts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/a-default-explicit.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/a-default.ts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/a-default.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/a-default.ts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/a-default.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/a-named-explicit.mts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/a-named-explicit.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/a-named-explicit.mts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/a-named-explicit.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/a-named.ts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/a-named.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/a-named.ts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/a-named.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/b-default.mts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/b-default.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/b-default.mts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/b-default.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/b-named.mts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/b-named.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/b-named.mts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/b-named.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/c-default.cts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/c-default.cts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/c-default.cts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/c-default.cts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/c-named.cts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/c-named.cts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/c-named.cts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/c-named.cts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/main-explicit.mts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/main-explicit.mts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/main-explicit.mts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/main-explicit.mts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/main.ts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/main.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/main.ts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/main.ts diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/package.json b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/package.json similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/package.json rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/package.json diff --git a/packages/cli/src/tests/transforms/__fixtures__/pkg-module/print.ts b/packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/print.ts similarity index 100% rename from packages/cli/src/tests/transforms/__fixtures__/pkg-module/print.ts rename to packages/cli/src/modules/build/tests/transforms/__fixtures__/pkg-module/print.ts diff --git a/packages/cli/src/tests/transforms/transforms.test.ts b/packages/cli/src/modules/build/tests/transforms/transforms.test.ts similarity index 99% rename from packages/cli/src/tests/transforms/transforms.test.ts rename to packages/cli/src/modules/build/tests/transforms/transforms.test.ts index 71661ded01..9e4fca8c63 100644 --- a/packages/cli/src/tests/transforms/transforms.test.ts +++ b/packages/cli/src/modules/build/tests/transforms/transforms.test.ts @@ -16,7 +16,7 @@ import { execFileSync } from 'node:child_process'; import { resolve as resolvePath } from 'node:path'; -import { Output, buildPackage } from '../../modules/build/lib/builder'; +import { Output, buildPackage } from '../../lib/builder'; const exportValues = { all: { diff --git a/packages/cli/src/modules/config/commands/docs.ts b/packages/cli/src/modules/config/commands/docs.ts index e7d3ac13ee..ed949c695f 100644 --- a/packages/cli/src/modules/config/commands/docs.ts +++ b/packages/cli/src/modules/config/commands/docs.ts @@ -14,20 +14,38 @@ * limitations under the License. */ +import { cli } from 'cleye'; import { JsonObject } from '@backstage/types'; import { mergeConfigSchemas } from '@backstage/config-loader'; -import { OptionValues } from 'commander'; import { JSONSchema7 as JSONSchema } from 'json-schema'; import openBrowser from 'react-dev-utils/openBrowser'; import chalk from 'chalk'; import { loadCliConfig } from '../lib/config'; +import type { CommandContext } from '../../../wiring/types'; const DOCS_URL = 'https://config.backstage.io'; -export default async (opts: OptionValues) => { +export default async ({ args, info }: CommandContext) => { + const { + flags: { package: pkg }, + } = cli( + { + help: info, + flags: { + package: { + type: String, + description: + 'Only include the schema that applies to the given package', + }, + }, + }, + undefined, + args, + ); + const { schema: appSchemas } = await loadCliConfig({ args: [], - fromPackage: opts.package, + fromPackage: pkg, mockEnv: true, }); diff --git a/packages/cli/src/modules/config/commands/print.ts b/packages/cli/src/modules/config/commands/print.ts index 98833a7946..412fdd69ff 100644 --- a/packages/cli/src/modules/config/commands/print.ts +++ b/packages/cli/src/modules/config/commands/print.ts @@ -14,36 +14,67 @@ * limitations under the License. */ -import { OptionValues } from 'commander'; +import { cli } from 'cleye'; import { stringify as stringifyYaml } from 'yaml'; import { AppConfig, ConfigReader } from '@backstage/config'; import { loadCliConfig } from '../lib/config'; import { ConfigSchema, ConfigVisibility } from '@backstage/config-loader'; +import type { CommandContext } from '../../../wiring/types'; + +export default async ({ args, info }: CommandContext) => { + const { + flags: { config, lax, frontend, withSecrets, format, package: pkg }, + } = cli( + { + help: info, + flags: { + package: { type: String, description: 'Package to print config for' }, + lax: { + type: Boolean, + description: 'Do not require environment variables to be set', + }, + frontend: { type: Boolean, description: 'Only print frontend config' }, + withSecrets: { + type: Boolean, + description: 'Include secrets in the output', + }, + format: { type: String, description: 'Output format (yaml or json)' }, + config: { + type: [String], + description: 'Config files to load instead of app-config.yaml', + }, + }, + }, + undefined, + args, + ); -export default async (opts: OptionValues) => { const { schema, appConfigs } = await loadCliConfig({ - args: opts.config, - fromPackage: opts.package, - mockEnv: opts.lax, - fullVisibility: !opts.frontend, + args: config, + fromPackage: pkg, + mockEnv: lax, + fullVisibility: !frontend, }); - const visibility = getVisibilityOption(opts); + const visibility = getVisibilityOption(frontend, withSecrets); const data = serializeConfigData(appConfigs, schema, visibility); - if (opts.format === 'json') { + if (format === 'json') { process.stdout.write(`${JSON.stringify(data, null, 2)}\n`); } else { process.stdout.write(`${stringifyYaml(data)}\n`); } }; -function getVisibilityOption(opts: OptionValues): ConfigVisibility { - if (opts.frontend && opts.withSecrets) { +function getVisibilityOption( + frontend: boolean | undefined, + withSecrets: boolean | undefined, +): ConfigVisibility { + if (frontend && withSecrets) { throw new Error('Not allowed to combine frontend and secret config'); } - if (opts.frontend) { + if (frontend) { return 'frontend'; - } else if (opts.withSecrets) { + } else if (withSecrets) { return 'secret'; } return 'backend'; diff --git a/packages/cli/src/modules/config/commands/schema.ts b/packages/cli/src/modules/config/commands/schema.ts index 73eb487d60..b41264e4b7 100644 --- a/packages/cli/src/modules/config/commands/schema.ts +++ b/packages/cli/src/modules/config/commands/schema.ts @@ -14,22 +14,41 @@ * limitations under the License. */ -import { OptionValues } from 'commander'; +import { cli } from 'cleye'; import { JSONSchema7 as JSONSchema } from 'json-schema'; import { stringify as stringifyYaml } from 'yaml'; import { loadCliConfig } from '../lib/config'; import { JsonObject } from '@backstage/types'; import { mergeConfigSchemas } from '@backstage/config-loader'; +import type { CommandContext } from '../../../wiring/types'; + +export default async ({ args, info }: CommandContext) => { + const { + flags: { merge, format, package: pkg }, + } = cli( + { + help: info, + flags: { + package: { type: String, description: 'Package to print schema for' }, + format: { type: String, description: 'Output format (yaml or json)' }, + merge: { + type: Boolean, + description: 'Merge all schemas into a single schema', + }, + }, + }, + undefined, + args, + ); -export default async (opts: OptionValues) => { const { schema } = await loadCliConfig({ args: [], - fromPackage: opts.package, + fromPackage: pkg, mockEnv: true, }); let configSchema: JsonObject | JSONSchema; - if (opts.merge) { + if (merge) { configSchema = mergeConfigSchemas( (schema.serialize().schemas as JsonObject[]).map( _ => _.value as JSONSchema, @@ -42,7 +61,7 @@ export default async (opts: OptionValues) => { configSchema = schema.serialize(); } - if (opts.format === 'json') { + if (format === 'json') { process.stdout.write(`${JSON.stringify(configSchema, null, 2)}\n`); } else { process.stdout.write(`${stringifyYaml(configSchema)}\n`); diff --git a/packages/cli/src/modules/config/commands/validate.ts b/packages/cli/src/modules/config/commands/validate.ts index ac0b9d15e0..abbbd7a176 100644 --- a/packages/cli/src/modules/config/commands/validate.ts +++ b/packages/cli/src/modules/config/commands/validate.ts @@ -14,16 +14,47 @@ * limitations under the License. */ -import { OptionValues } from 'commander'; +import { cli } from 'cleye'; import { loadCliConfig } from '../lib/config'; +import type { CommandContext } from '../../../wiring/types'; + +export default async ({ args, info }: CommandContext) => { + const { + flags: { config, lax, frontend, deprecated, strict, package: pkg }, + } = cli( + { + help: info, + flags: { + package: { + type: String, + description: 'Package to validate config for', + }, + lax: { + type: Boolean, + description: 'Do not require environment variables to be set', + }, + frontend: { + type: Boolean, + description: 'Only validate frontend config', + }, + deprecated: { type: Boolean, description: 'Output deprecated keys' }, + strict: { type: Boolean, description: 'Enable strict validation' }, + config: { + type: [String], + description: 'Config files to load instead of app-config.yaml', + }, + }, + }, + undefined, + args, + ); -export default async (opts: OptionValues) => { await loadCliConfig({ - args: opts.config, - fromPackage: opts.package, - mockEnv: opts.lax, - fullVisibility: !opts.frontend, - withDeprecatedKeys: opts.deprecated, - strict: opts.strict, + args: config, + fromPackage: pkg, + mockEnv: lax, + fullVisibility: !frontend, + withDeprecatedKeys: deprecated, + strict, }); }; diff --git a/packages/cli/src/modules/config/index.ts b/packages/cli/src/modules/config/index.ts index 3eabf5e9fc..0d90037f18 100644 --- a/packages/cli/src/modules/config/index.ts +++ b/packages/cli/src/modules/config/index.ts @@ -14,9 +14,6 @@ * limitations under the License. */ import { createCliPlugin } from '../../wiring/factory'; -import yargs from 'yargs'; -import { Command } from 'commander'; -import { lazy } from '../../lib/lazy'; export const configOption = [ '--config ', @@ -31,108 +28,33 @@ export default createCliPlugin({ reg.addCommand({ path: ['config:docs'], description: 'Browse the configuration reference documentation', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command - .option( - '--package ', - 'Only include the schema that applies to the given package', - ) - .description('Browse the configuration reference documentation') - .action(lazy(() => import('./commands/docs'), 'default')); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, + execute: { loader: () => import('./commands/docs') }, }); reg.addCommand({ path: ['config', 'docs'], description: 'Browse the configuration reference documentation', - execute: async ({ args, info }) => { - await new Command(info.usage) - .option( - '--package ', - 'Only include the schema that applies to the given package', - ) - .description(info.description) - .action(lazy(() => import('./commands/docs'), 'default')) - .parseAsync(args, { from: 'user' }); - }, + execute: { loader: () => import('./commands/docs') }, }); reg.addCommand({ path: ['config:print'], description: 'Print the app configuration for the current package', - execute: async ({ args, info }) => { - const argv = await yargs() - .options({ - package: { type: 'string' }, - lax: { type: 'boolean' }, - frontend: { type: 'boolean' }, - 'with-secrets': { type: 'boolean' }, - format: { type: 'string' }, - config: { type: 'string', array: true, default: [] }, - }) - .usage('$0', info.description) - .help() - .parse(args); - await lazy(() => import('./commands/print'), 'default')(argv); - }, + execute: { loader: () => import('./commands/print') }, }); reg.addCommand({ path: ['config:check'], description: 'Validate that the given configuration loads and matches schema', - execute: async ({ args }) => { - const argv = await yargs() - .options({ - package: { type: 'string' }, - lax: { type: 'boolean' }, - frontend: { type: 'boolean' }, - deprecated: { type: 'boolean' }, - strict: { type: 'boolean' }, - config: { - type: 'string', - array: true, - default: [], - }, - }) - .help() - .parse(args); - await lazy(() => import('./commands/validate'), 'default')(argv); - }, + execute: { loader: () => import('./commands/validate') }, }); - reg.addCommand({ path: ['config:schema'], description: 'Print the JSON schema for the given configuration', - execute: async ({ args }) => { - const argv = await yargs() - .options({ - package: { type: 'string' }, - format: { type: 'string' }, - merge: { type: 'boolean' }, - 'no-merge': { type: 'boolean' }, - }) - .help() - .parse(args); - await lazy(() => import('./commands/schema'), 'default')(argv); - }, + execute: { loader: () => import('./commands/schema') }, }); - reg.addCommand({ path: ['config', 'schema'], description: 'Print the JSON schema for the given configuration', - execute: async ({ args }) => { - const argv = await yargs() - .options({ - package: { type: 'string' }, - format: { type: 'string' }, - merge: { type: 'boolean' }, - 'no-merge': { type: 'boolean' }, - }) - .help() - .parse(args); - await lazy(() => import('./commands/schema'), 'default')(argv); - }, + execute: { loader: () => import('./commands/schema') }, }); }, }); diff --git a/packages/cli/src/modules/config/lib/config.ts b/packages/cli/src/modules/config/lib/config.ts index 1d920eb31e..763af2174f 100644 --- a/packages/cli/src/modules/config/lib/config.ts +++ b/packages/cli/src/modules/config/lib/config.ts @@ -27,11 +27,9 @@ type Options = { targetDir?: string; fromPackage?: string; mockEnv?: boolean; - withFilteredKeys?: boolean; withDeprecatedKeys?: boolean; fullVisibility?: boolean; strict?: boolean; - watch?: (newFrontendAppConfigs: AppConfig[]) => void; }; export async function loadCliConfig(options: Options) { @@ -73,48 +71,29 @@ export async function loadCliConfig(options: Options) { substitutionFunc: options.mockEnv ? async name => process.env[name] || 'x' : undefined, - watch: Boolean(options.watch), rootDir: targetPaths.rootDir, argv: options.args.flatMap(t => ['--config', resolvePath(targetDir, t)]), }); const appConfigs = await new Promise((resolve, reject) => { - async function loadConfigReaderLoop() { + async function readConfig() { let loaded = false; - try { const abortController = new AbortController(); for await (const { configs } of source.readConfigData({ signal: abortController.signal, })) { - if (loaded) { - const newFrontendAppConfigs = schema.process(configs, { - visibility: options.fullVisibility - ? ['frontend', 'backend', 'secret'] - : ['frontend'], - withFilteredKeys: options.withFilteredKeys, - withDeprecatedKeys: options.withDeprecatedKeys, - ignoreSchemaErrors: !options.strict, - }); - options.watch?.(newFrontendAppConfigs); - } else { - resolve(configs); - loaded = true; - - if (!options.watch) { - abortController.abort(); - } - } + resolve(configs); + loaded = true; + abortController.abort(); } } catch (error) { - if (loaded) { - console.error(`Failed to reload configuration, ${error}`); - } else { + if (!loaded) { reject(error); } } } - loadConfigReaderLoop(); + readConfig(); }); const configurationLoadedMessage = appConfigs.length @@ -130,7 +109,6 @@ export async function loadCliConfig(options: Options) { visibility: options.fullVisibility ? ['frontend', 'backend', 'secret'] : ['frontend'], - withFilteredKeys: options.withFilteredKeys, withDeprecatedKeys: options.withDeprecatedKeys, ignoreSchemaErrors: !options.strict, }); diff --git a/packages/cli/src/modules/create-github-app/index.ts b/packages/cli/src/modules/create-github-app/index.ts index da1e04aaac..5a3e30e870 100644 --- a/packages/cli/src/modules/create-github-app/index.ts +++ b/packages/cli/src/modules/create-github-app/index.ts @@ -15,7 +15,7 @@ */ import { createCliPlugin } from '../../wiring/factory'; import { Command } from 'commander'; -import { lazy } from '../../lib/lazy'; +import { lazy } from '../../wiring/lazy'; export default createCliPlugin({ pluginId: 'new', diff --git a/packages/cli/src/modules/info/commands/info.ts b/packages/cli/src/modules/info/commands/info.ts index c50a9da373..c423d2b1dc 100644 --- a/packages/cli/src/modules/info/commands/info.ts +++ b/packages/cli/src/modules/info/commands/info.ts @@ -14,18 +14,18 @@ * limitations under the License. */ +import { cli } from 'cleye'; import { version as cliVersion } from '../../../../package.json'; import os from 'node:os'; import { runOutput, targetPaths, findOwnPaths } from '@backstage/cli-common'; -import { Lockfile } from '../../../lib/versioning'; -import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node'; +import { + BackstagePackageJson, + Lockfile, + PackageGraph, +} from '@backstage/cli-node'; import { minimatch } from 'minimatch'; import fs from 'fs-extra'; - -interface InfoOptions { - include: string[]; - format: 'text' | 'json'; -} +import type { CommandContext } from '../../../wiring/types'; /** * Attempts to read package.json from node_modules for a given package name. @@ -52,7 +52,31 @@ function hasBackstageField(packageName: string, targetPath: string): boolean { return pkg?.backstage !== undefined; } -export default async (options: InfoOptions) => { +export default async ({ args, info }: CommandContext) => { + const { + flags: { include, format }, + } = cli( + { + help: info, + flags: { + include: { + type: [String], + description: + 'Glob patterns for additional packages to include (e.g., @spotify/backstage*)', + }, + format: { + type: String, + description: 'Output format (text or json)', + default: 'text', + }, + }, + }, + undefined, + args, + ); + + const options = { include, format: format as 'text' | 'json' }; + await new Promise(async () => { const yarnVersion = await runOutput(['yarn', '--version']); /* eslint-disable-next-line no-restricted-syntax */ diff --git a/packages/cli/src/modules/info/index.ts b/packages/cli/src/modules/info/index.ts index c14926df85..8e1fe9cbff 100644 --- a/packages/cli/src/modules/info/index.ts +++ b/packages/cli/src/modules/info/index.ts @@ -13,9 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import yargs from 'yargs'; import { createCliPlugin } from '../../wiring/factory'; -import { lazy } from '../../lib/lazy'; export default createCliPlugin({ pluginId: 'info', @@ -23,27 +21,7 @@ export default createCliPlugin({ reg.addCommand({ path: ['info'], description: 'Show helpful information for debugging and reporting bugs', - execute: async ({ args }) => { - const argv = await yargs() - .options({ - include: { - type: 'string', - array: true, - default: [], - description: - 'Glob patterns for additional packages to include (e.g., @spotify/backstage*)', - }, - format: { - type: 'string', - choices: ['text', 'json'], - default: 'text', - description: 'Output format (text or json)', - }, - }) - .help() - .parse(args); - await lazy(() => import('./commands/info'), 'default')(argv); - }, + execute: { loader: () => import('./commands/info') }, }); }, }); diff --git a/packages/cli/src/modules/lint/commands/repo/lint.ts b/packages/cli/src/modules/lint/commands/repo/lint.ts index dafcff45c6..a3ae69302c 100644 --- a/packages/cli/src/modules/lint/commands/repo/lint.ts +++ b/packages/cli/src/modules/lint/commands/repo/lint.ts @@ -24,11 +24,11 @@ import { BackstagePackageJson, Lockfile, runWorkerQueueThreads, + SuccessCache, } from '@backstage/cli-node'; import { targetPaths } from '@backstage/cli-common'; -import { createScriptOptionsParser } from '../../../../lib/optionsParser'; -import { SuccessCache } from '../../../../lib/cache/SuccessCache'; +import { createScriptOptionsParser } from '../../lib/optionsParser'; function depCount(pkg: BackstagePackageJson) { const deps = pkg.dependencies ? Object.keys(pkg.dependencies).length : 0; @@ -41,7 +41,10 @@ function depCount(pkg: BackstagePackageJson) { export async function command(opts: OptionValues, cmd: Command): Promise { let packages = await PackageGraph.listTargetPackages(); - const cache = new SuccessCache('lint', opts.successCacheDir); + const cache = SuccessCache.create({ + name: 'lint', + basePath: opts.successCacheDir, + }); const cacheContext = opts.successCache ? { entries: await cache.read(), diff --git a/packages/cli/src/modules/lint/index.ts b/packages/cli/src/modules/lint/index.ts index 6cba232da9..40c374aa3b 100644 --- a/packages/cli/src/modules/lint/index.ts +++ b/packages/cli/src/modules/lint/index.ts @@ -15,7 +15,7 @@ */ import { createCliPlugin } from '../../wiring/factory'; import { Command } from 'commander'; -import { lazy } from '../../lib/lazy'; +import { lazy } from '../../wiring/lazy'; export function registerPackageLintCommand(command: Command) { command.arguments('[directories...]'); diff --git a/packages/cli/src/modules/lint/lib/optionsParser.ts b/packages/cli/src/modules/lint/lib/optionsParser.ts new file mode 100644 index 0000000000..15561a2e42 --- /dev/null +++ b/packages/cli/src/modules/lint/lib/optionsParser.ts @@ -0,0 +1,70 @@ +/* + * 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 { Command } from 'commander'; + +export function createScriptOptionsParser( + anyCmd: Command, + commandPath: string[], +) { + // Regardless of what command instance is passed in we want to find + // the root command and resolve the path from there + let rootCmd = anyCmd; + while (rootCmd.parent) { + rootCmd = rootCmd.parent; + } + + // Now find the command that was requested + let targetCmd = rootCmd as Command | undefined; + for (const name of commandPath) { + targetCmd = targetCmd?.commands.find(c => c.name() === name) as + | Command + | undefined; + } + + if (!targetCmd) { + throw new Error( + `Could not find package command '${commandPath.join(' ')}'`, + ); + } + const cmd = targetCmd; + + const expectedScript = `backstage-cli ${commandPath.join(' ')}`; + + return (scriptStr?: string) => { + if (!scriptStr || !scriptStr.startsWith(expectedScript)) { + return undefined; + } + + const argsStr = scriptStr.slice(expectedScript.length).trim(); + + // Can't clone or copy or even use commands as prototype, so we mutate + // the necessary members instead, and then reset them once we're done + const currentOpts = (cmd as any)._optionValues; + const currentStore = (cmd as any)._storeOptionsAsProperties; + + const result: Record = {}; + (cmd as any)._storeOptionsAsProperties = false; + (cmd as any)._optionValues = result; + + // Triggers the writing of options to the result object + cmd.parseOptions(argsStr.split(' ')); + + (cmd as any)._optionValues = currentOpts; + (cmd as any)._storeOptionsAsProperties = currentStore; + + return result; + }; +} diff --git a/packages/cli/src/modules/maintenance/commands/repo/fix.ts b/packages/cli/src/modules/maintenance/commands/repo/fix.ts index 073cb69ea1..de09d8b490 100644 --- a/packages/cli/src/modules/maintenance/commands/repo/fix.ts +++ b/packages/cli/src/modules/maintenance/commands/repo/fix.ts @@ -31,8 +31,6 @@ import { } from 'node:path'; import { targetPaths } from '@backstage/cli-common'; -import { publishPreflightCheck } from '../../lib/publishing'; - const SCRIPT_EXTS = ['.js', '.jsx', '.ts', '.tsx', '.json']; /** @@ -507,8 +505,6 @@ export async function command(opts: OptionValues): Promise { fixPluginId, fixPluginPackages, fixPeerModules, - // Run the publish preflight check too, to make sure we don't uncover errors during publishing - publishPreflightCheck, ); } diff --git a/packages/cli/src/modules/maintenance/index.ts b/packages/cli/src/modules/maintenance/index.ts index 41192afdc4..e2584527f0 100644 --- a/packages/cli/src/modules/maintenance/index.ts +++ b/packages/cli/src/modules/maintenance/index.ts @@ -15,50 +15,11 @@ */ import { Command } from 'commander'; import { createCliPlugin } from '../../wiring/factory'; -import { lazy } from '../../lib/lazy'; +import { lazy } from '../../wiring/lazy'; export default createCliPlugin({ pluginId: 'maintenance', init: async reg => { - reg.addCommand({ - path: ['package', 'clean'], - description: 'Delete cache directories', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/package/clean'), 'default'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['package', 'prepack'], - description: 'Prepares a package for packaging before publishing', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/package/pack'), 'pre'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - - reg.addCommand({ - path: ['package', 'postpack'], - description: 'Restores the changes made by the prepack command', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/package/pack'), 'post'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - reg.addCommand({ path: ['repo', 'fix'], description: 'Automatically fix packages in the project', @@ -79,19 +40,6 @@ export default createCliPlugin({ }, }); - reg.addCommand({ - path: ['repo', 'clean'], - description: 'Delete cache and output directories', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/repo/clean'), 'command'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, - }); - reg.addCommand({ path: ['repo', 'list-deprecations'], description: 'List deprecations', diff --git a/packages/cli/src/modules/migrate/commands/packageExports.ts b/packages/cli/src/modules/migrate/commands/packageExports.ts index 4eb741ce4d..3307e55721 100644 --- a/packages/cli/src/modules/migrate/commands/packageExports.ts +++ b/packages/cli/src/modules/migrate/commands/packageExports.ts @@ -14,21 +14,12 @@ * limitations under the License. */ -import { - fixPackageExports, - readFixablePackages, - writeFixedPackages, -} from '../../maintenance/commands/repo/fix'; +import { cli } from 'cleye'; +import type { CommandContext } from '../../../wiring/types'; -export async function command() { - console.log( - 'The `migrate package-exports` command is deprecated, use `repo fix` instead.', +export default async ({ args, info }: CommandContext) => { + cli({ help: info }, undefined, args); + throw new Error( + 'The `migrate package-exports` command has been removed, use `repo fix` instead.', ); - const packages = await readFixablePackages(); - - for (const pkg of packages) { - fixPackageExports(pkg); - } - - await writeFixedPackages(packages); -} +}; diff --git a/packages/cli/src/modules/migrate/commands/packageLintConfigs.ts b/packages/cli/src/modules/migrate/commands/packageLintConfigs.ts index d6e6183b83..c6839fe28b 100644 --- a/packages/cli/src/modules/migrate/commands/packageLintConfigs.ts +++ b/packages/cli/src/modules/migrate/commands/packageLintConfigs.ts @@ -14,14 +14,17 @@ * limitations under the License. */ +import { cli } from 'cleye'; import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { PackageGraph } from '@backstage/cli-node'; import { runOutput } from '@backstage/cli-common'; +import type { CommandContext } from '../../../wiring/types'; const PREFIX = `module.exports = require('@backstage/cli/config/eslint-factory')`; -export async function command() { +export default async ({ args, info }: CommandContext) => { + cli({ help: info }, undefined, args); const packages = await PackageGraph.listTargetPackages(); const oldConfigs = [ @@ -86,4 +89,4 @@ export async function command() { if (hasPrettier) { await runOutput(['prettier', '--write', ...configPaths]); } -} +}; diff --git a/packages/cli/src/modules/migrate/commands/packageRole.ts b/packages/cli/src/modules/migrate/commands/packageRole.ts index 6b422c375e..8d8c04b735 100644 --- a/packages/cli/src/modules/migrate/commands/packageRole.ts +++ b/packages/cli/src/modules/migrate/commands/packageRole.ts @@ -14,13 +14,16 @@ * limitations under the License. */ +import { cli } from 'cleye'; import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { getPackages } from '@manypkg/get-packages'; import { PackageRoles } from '@backstage/cli-node'; import { targetPaths } from '@backstage/cli-common'; +import type { CommandContext } from '../../../wiring/types'; -export default async () => { +export default async ({ args, info }: CommandContext) => { + cli({ help: info }, undefined, args); const { packages } = await getPackages(targetPaths.dir); await Promise.all( diff --git a/packages/cli/src/modules/migrate/commands/packageScripts.ts b/packages/cli/src/modules/migrate/commands/packageScripts.ts index 6c4e41c3ec..0b7d9fc2ce 100644 --- a/packages/cli/src/modules/migrate/commands/packageScripts.ts +++ b/packages/cli/src/modules/migrate/commands/packageScripts.ts @@ -14,15 +14,18 @@ * limitations under the License. */ +import { cli } from 'cleye'; import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { PackageGraph, PackageRoles, PackageRole } from '@backstage/cli-node'; +import type { CommandContext } from '../../../wiring/types'; const configArgPattern = /--config[=\s][^\s$]+/; const noStartRoles: PackageRole[] = ['cli', 'common-library']; -export async function command() { +export default async ({ args, info }: CommandContext) => { + cli({ help: info }, undefined, args); const packages = await PackageGraph.listTargetPackages(); await Promise.all( @@ -56,14 +59,14 @@ export async function command() { // For test scripts we keep all existing flags except for --passWithNoTests, since that's now default const testCmd = ['test']; if (scripts.test?.startsWith('backstage-cli test')) { - const args = scripts.test + const testArgs = scripts.test .slice('backstage-cli test'.length) .split(' ') .filter(Boolean); - if (args.includes('--passWithNoTests')) { - args.splice(args.indexOf('--passWithNoTests'), 1); + if (testArgs.includes('--passWithNoTests')) { + testArgs.splice(testArgs.indexOf('--passWithNoTests'), 1); } - testCmd.push(...args); + testCmd.push(...testArgs); } const expectedScripts = { @@ -104,4 +107,4 @@ export async function command() { } }), ); -} +}; diff --git a/packages/cli/src/modules/migrate/commands/reactRouterDeps.ts b/packages/cli/src/modules/migrate/commands/reactRouterDeps.ts index 3f187dd534..702593c366 100644 --- a/packages/cli/src/modules/migrate/commands/reactRouterDeps.ts +++ b/packages/cli/src/modules/migrate/commands/reactRouterDeps.ts @@ -14,14 +14,17 @@ * limitations under the License. */ +import { cli } from 'cleye'; import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { PackageGraph, PackageRoles } from '@backstage/cli-node'; +import type { CommandContext } from '../../../wiring/types'; const REACT_ROUTER_DEPS = ['react-router', 'react-router-dom']; const REACT_ROUTER_RANGE = '6.0.0-beta.0 || ^6.3.0'; -export async function command() { +export default async ({ args, info }: CommandContext) => { + cli({ help: info }, undefined, args); const packages = await PackageGraph.listTargetPackages(); await Promise.all( @@ -56,4 +59,4 @@ export async function command() { } }), ); -} +}; diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts index cc662f9d5e..c80af07c16 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts @@ -19,7 +19,7 @@ import * as runObj from '@backstage/cli-common'; import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump'; import { registerMswTestHooks, withLogCollector } from '@backstage/test-utils'; -import { YarnInfoInspectData } from '../../../../lib/versioning/packages'; +import { YarnInfoInspectData } from '../../lib/versioning/packages'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { NotFoundError } from '@backstage/errors'; @@ -69,8 +69,8 @@ jest.mock('@backstage/cli-common', () => { }); const mockFetchPackageInfo = jest.fn(); -jest.mock('../../../../lib/versioning/packages', () => { - const actual = jest.requireActual('../../../../lib/versioning/packages'); +jest.mock('../../lib/versioning/packages', () => { + const actual = jest.requireActual('../../lib/versioning/packages'); return { ...actual, fetchPackageInfo: (name: string) => mockFetchPackageInfo(name), @@ -150,9 +150,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -245,9 +243,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -343,9 +339,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -449,9 +443,7 @@ describe('bump', () => { '.yarnrc.yml': yarnRcMock, 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -562,9 +554,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -634,9 +624,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -740,9 +728,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': customLockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -855,9 +841,7 @@ describe('bump', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -1099,9 +1083,7 @@ describe('environment variables', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -1192,9 +1174,7 @@ describe('environment variables', () => { 'custom-manifest.json': JSON.stringify(customManifest), 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -1262,9 +1242,7 @@ describe('environment variables', () => { '.yarnrc.yml': yarnRcMock, 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -1337,9 +1315,7 @@ describe('environment variables', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { @@ -1362,9 +1338,7 @@ describe('environment variables', () => { mockDir.setContent({ 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), packages: { a: { diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli/src/modules/migrate/commands/versions/bump.ts index 4283028216..8fe7013ac8 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.ts @@ -30,14 +30,16 @@ import { OptionValues } from 'commander'; import { isError, NotFoundError } from '@backstage/errors'; import { resolve as resolvePath } from 'node:path'; -import { getHasYarnPlugin } from '../../../../lib/yarnPlugin'; +import { + hasBackstageYarnPlugin, + Lockfile, + runConcurrentTasks, +} from '@backstage/cli-node'; import { fetchPackageInfo, - Lockfile, mapDependencies, YarnInfoInspectData, -} from '../../../../lib/versioning'; -import { runConcurrentTasks } from '@backstage/cli-node'; +} from '../../lib/versioning/packages'; import { getManifestByReleaseLine, getManifestByVersion, @@ -74,7 +76,7 @@ function extendsDefaultPattern(pattern: string): boolean { export default async (opts: OptionValues) => { const lockfilePath = targetPaths.resolveRoot('yarn.lock'); const lockfile = await Lockfile.load(lockfilePath); - const hasYarnPlugin = await getHasYarnPlugin(); + const yarnPluginEnabled = await hasBackstageYarnPlugin(); let pattern = opts.pattern; @@ -128,7 +130,7 @@ export default async (opts: OptionValues) => { }); } - if (hasYarnPlugin) { + if (yarnPluginEnabled) { console.log(); console.log( `Updating yarn plugin to v${releaseManifest.releaseVersion}...`, @@ -212,7 +214,7 @@ export default async (opts: OptionValues) => { const oldLockfileRange = await asLockfileVersion(oldRange); const useBackstageRange = - hasYarnPlugin && + yarnPluginEnabled && // Only use backstage:^ versions if the package is present in // the manifest for the release we're bumping to. releaseManifest.packages.find( @@ -252,7 +254,7 @@ export default async (opts: OptionValues) => { if (extendsDefaultPattern(pattern)) { await bumpBackstageJsonVersion( releaseManifest.releaseVersion, - hasYarnPlugin, + yarnPluginEnabled, ); } else { console.log( @@ -317,7 +319,7 @@ export default async (opts: OptionValues) => { console.log(); } - if (hasYarnPlugin) { + if (yarnPluginEnabled) { console.log(); console.log( chalk.blue( diff --git a/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts b/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts index 16684d36f3..6748f8c1ff 100644 --- a/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts +++ b/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts @@ -73,9 +73,7 @@ describe('versions:migrate', () => { it('should bump to the moved version when the package is moved', async () => { mockDir.setContent({ 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), node_modules: { '@backstage': { @@ -177,9 +175,7 @@ describe('versions:migrate', () => { it('should replace the occurrences of the moved package in files inside the correct package', async () => { mockDir.setContent({ 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), node_modules: { '@backstage': { @@ -264,9 +260,7 @@ describe('versions:migrate', () => { it('should replace occurrences of changed packages, and is careful', async () => { mockDir.setContent({ 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, + workspaces: ['packages/*'], }), node_modules: { '@backstage': { diff --git a/packages/cli/src/modules/migrate/index.ts b/packages/cli/src/modules/migrate/index.ts index ac56c06b7a..2dc4ed452d 100644 --- a/packages/cli/src/modules/migrate/index.ts +++ b/packages/cli/src/modules/migrate/index.ts @@ -15,7 +15,7 @@ */ import { createCliPlugin } from '../../wiring/factory'; import { Command } from 'commander'; -import { lazy } from '../../lib/lazy'; +import { lazy } from '../../wiring/lazy'; export default createCliPlugin({ pluginId: 'migrate', @@ -68,39 +68,24 @@ export default createCliPlugin({ reg.addCommand({ path: ['migrate', 'package-roles'], description: `Add package role field to packages that don't have it`, - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/packageRole'), 'default'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); + execute: { + loader: () => import('./commands/packageRole'), }, }); reg.addCommand({ path: ['migrate', 'package-scripts'], description: 'Set package scripts according to each package role', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/packageScripts'), 'command'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); + execute: { + loader: () => import('./commands/packageScripts'), }, }); reg.addCommand({ path: ['migrate', 'package-exports'], description: 'Synchronize package subpath export definitions', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/packageExports'), 'command'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); + execute: { + loader: () => import('./commands/packageExports'), }, }); @@ -108,13 +93,8 @@ export default createCliPlugin({ path: ['migrate', 'package-lint-configs'], description: 'Migrates all packages to use @backstage/cli/config/eslint-factory', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/packageLintConfigs'), 'command'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); + execute: { + loader: () => import('./commands/packageLintConfigs'), }, }); @@ -122,13 +102,8 @@ export default createCliPlugin({ path: ['migrate', 'react-router-deps'], description: 'Migrates the react-router dependencies for all packages to be peer dependencies', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command.action( - lazy(() => import('./commands/reactRouterDeps'), 'command'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); + execute: { + loader: () => import('./commands/reactRouterDeps'), }, }); }, diff --git a/packages/cli/src/lib/versioning/packages.test.ts b/packages/cli/src/modules/migrate/lib/versioning/packages.test.ts similarity index 98% rename from packages/cli/src/lib/versioning/packages.test.ts rename to packages/cli/src/modules/migrate/lib/versioning/packages.test.ts index e823ce8769..8946b8ba2f 100644 --- a/packages/cli/src/lib/versioning/packages.test.ts +++ b/packages/cli/src/modules/migrate/lib/versioning/packages.test.ts @@ -104,9 +104,7 @@ describe('mapDependencies', () => { it('should read dependencies', async () => { mockDir.setContent({ 'package.json': JSON.stringify({ - workspaces: { - packages: ['pkgs/*'], - }, + workspaces: ['pkgs/*'], }), pkgs: { a: { diff --git a/packages/cli/src/lib/versioning/packages.ts b/packages/cli/src/modules/migrate/lib/versioning/packages.ts similarity index 100% rename from packages/cli/src/lib/versioning/packages.ts rename to packages/cli/src/modules/migrate/lib/versioning/packages.ts diff --git a/packages/cli/src/lib/versioning/yarn.ts b/packages/cli/src/modules/migrate/lib/versioning/yarn.ts similarity index 100% rename from packages/cli/src/lib/versioning/yarn.ts rename to packages/cli/src/modules/migrate/lib/versioning/yarn.ts diff --git a/packages/cli/src/modules/new/index.ts b/packages/cli/src/modules/new/index.ts index 120e77c7c0..ac21b825fe 100644 --- a/packages/cli/src/modules/new/index.ts +++ b/packages/cli/src/modules/new/index.ts @@ -15,7 +15,7 @@ */ import { createCliPlugin } from '../../wiring/factory'; import { Command } from 'commander'; -import { lazy } from '../../lib/lazy'; +import { lazy } from '../../wiring/lazy'; import { NotImplementedError } from '@backstage/errors'; export default createCliPlugin({ diff --git a/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts b/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts index 511e656c91..dd6f43b9a7 100644 --- a/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts +++ b/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts @@ -24,11 +24,11 @@ import startCase from 'lodash/startCase'; import upperCase from 'lodash/upperCase'; import upperFirst from 'lodash/upperFirst'; import lowerFirst from 'lodash/lowerFirst'; -import { Lockfile } from '../../../../lib/versioning'; +import { Lockfile } from '@backstage/cli-node'; import { targetPaths } from '@backstage/cli-common'; -import { createPackageVersionProvider } from '../../../../lib/version'; -import { getHasYarnPlugin } from '../../../../lib/yarnPlugin'; +import { createPackageVersionProvider } from '../version'; +import { hasBackstageYarnPlugin } from '@backstage/cli-node'; const builtInHelpers = { camelCase, @@ -55,9 +55,9 @@ export class PortableTemplater { /* ignored */ } - const hasYarnPlugin = await getHasYarnPlugin(); + const yarnPluginEnabled = await hasBackstageYarnPlugin(); const versionProvider = createPackageVersionProvider(lockfile, { - preferBackstageProtocol: hasYarnPlugin, + preferBackstageProtocol: yarnPluginEnabled, }); const templater = new PortableTemplater( diff --git a/packages/cli/src/lib/version.test.ts b/packages/cli/src/modules/new/lib/version.test.ts similarity index 99% rename from packages/cli/src/lib/version.test.ts rename to packages/cli/src/modules/new/lib/version.test.ts index d582c397c3..86b92e35c3 100644 --- a/packages/cli/src/lib/version.test.ts +++ b/packages/cli/src/modules/new/lib/version.test.ts @@ -15,7 +15,7 @@ */ import { packageVersions, createPackageVersionProvider } from './version'; -import { Lockfile } from './versioning'; +import { Lockfile } from '@backstage/cli-node'; import corePluginApiPkg from '@backstage/core-plugin-api/package.json'; import { createMockDirectory } from '@backstage/backend-test-utils'; diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/modules/new/lib/version.ts similarity index 62% rename from packages/cli/src/lib/version.ts rename to packages/cli/src/modules/new/lib/version.ts index aa8556534f..0849e8a9e8 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/modules/new/lib/version.ts @@ -14,13 +14,8 @@ * limitations under the License. */ -import fs from 'fs-extra'; import semver from 'semver'; -import { findOwnPaths } from '@backstage/cli-common'; -import { Lockfile } from './versioning'; - -/* eslint-disable-next-line no-restricted-syntax */ -const ownPaths = findOwnPaths(__dirname); +import { Lockfile } from '@backstage/cli-node'; /* eslint-disable @backstage/no-relative-monorepo-imports */ /* @@ -35,28 +30,28 @@ This does not create an actual dependency on these packages and does not bring i Rollup will extract the value of the version field in each package at build time without leaving any imports in place. */ -import { version as backendPluginApi } from '../../../../packages/backend-plugin-api/package.json'; -import { version as backendTestUtils } from '../../../../packages/backend-test-utils/package.json'; -import { version as catalogClient } from '../../../../packages/catalog-client/package.json'; -import { version as cli } from '../../../../packages/cli/package.json'; -import { version as config } from '../../../../packages/config/package.json'; -import { version as coreAppApi } from '../../../../packages/core-app-api/package.json'; -import { version as coreComponents } from '../../../../packages/core-components/package.json'; -import { version as corePluginApi } from '../../../../packages/core-plugin-api/package.json'; -import { version as devUtils } from '../../../../packages/dev-utils/package.json'; -import { version as errors } from '../../../../packages/errors/package.json'; -import { version as frontendDefaults } from '../../../../packages/frontend-defaults/package.json'; -import { version as frontendPluginApi } from '../../../../packages/frontend-plugin-api/package.json'; -import { version as frontendTestUtils } from '../../../../packages/frontend-test-utils/package.json'; -import { version as testUtils } from '../../../../packages/test-utils/package.json'; -import { version as scaffolderNode } from '../../../../plugins/scaffolder-node/package.json'; -import { version as scaffolderNodeTestUtils } from '../../../../plugins/scaffolder-node-test-utils/package.json'; -import { version as authBackend } from '../../../../plugins/auth-backend/package.json'; -import { version as authBackendModuleGuestProvider } from '../../../../plugins/auth-backend-module-guest-provider/package.json'; -import { version as catalogNode } from '../../../../plugins/catalog-node/package.json'; -import { version as theme } from '../../../../packages/theme/package.json'; -import { version as types } from '../../../../packages/types/package.json'; -import { version as backendDefaults } from '../../../../packages/backend-defaults/package.json'; +import { version as backendPluginApi } from '../../../../../../packages/backend-plugin-api/package.json'; +import { version as backendTestUtils } from '../../../../../../packages/backend-test-utils/package.json'; +import { version as catalogClient } from '../../../../../../packages/catalog-client/package.json'; +import { version as cli } from '../../../../../../packages/cli/package.json'; +import { version as config } from '../../../../../../packages/config/package.json'; +import { version as coreAppApi } from '../../../../../../packages/core-app-api/package.json'; +import { version as coreComponents } from '../../../../../../packages/core-components/package.json'; +import { version as corePluginApi } from '../../../../../../packages/core-plugin-api/package.json'; +import { version as devUtils } from '../../../../../../packages/dev-utils/package.json'; +import { version as errors } from '../../../../../../packages/errors/package.json'; +import { version as frontendDefaults } from '../../../../../../packages/frontend-defaults/package.json'; +import { version as frontendPluginApi } from '../../../../../../packages/frontend-plugin-api/package.json'; +import { version as frontendTestUtils } from '../../../../../../packages/frontend-test-utils/package.json'; +import { version as testUtils } from '../../../../../../packages/test-utils/package.json'; +import { version as scaffolderNode } from '../../../../../../plugins/scaffolder-node/package.json'; +import { version as scaffolderNodeTestUtils } from '../../../../../../plugins/scaffolder-node-test-utils/package.json'; +import { version as authBackend } from '../../../../../../plugins/auth-backend/package.json'; +import { version as authBackendModuleGuestProvider } from '../../../../../../plugins/auth-backend-module-guest-provider/package.json'; +import { version as catalogNode } from '../../../../../../plugins/catalog-node/package.json'; +import { version as theme } from '../../../../../../packages/theme/package.json'; +import { version as types } from '../../../../../../packages/types/package.json'; +import { version as backendDefaults } from '../../../../../../packages/backend-defaults/package.json'; export const packageVersions: Record = { '@backstage/backend-defaults': backendDefaults, @@ -84,14 +79,6 @@ export const packageVersions: Record = { '@backstage/plugin-catalog-node': catalogNode, }; -export function findVersion() { - const pkgContent = fs.readFileSync(ownPaths.resolve('package.json'), 'utf8'); - return JSON.parse(pkgContent).version; -} - -export const version = findVersion(); -export const isDev = fs.pathExistsSync(ownPaths.resolve('src')); - export function createPackageVersionProvider( lockfile?: Lockfile, options?: { diff --git a/packages/cli/src/modules/test/commands/package/test.ts b/packages/cli/src/modules/test/commands/package/test.ts index c4acfb287b..9c13019bb5 100644 --- a/packages/cli/src/modules/test/commands/package/test.ts +++ b/packages/cli/src/modules/test/commands/package/test.ts @@ -87,11 +87,6 @@ export default async (_opts: OptionValues, cmd: Command) => { }--no-node-snapshot`; } - // This ensures that the process doesn't exit too early before stdout is flushed - if (args.includes('--help')) { - (process.stdout as any)._handle.setBlocking(true); - } - // Because of the ongoing migration to v30 of jest, jest is no longer hard-depended to allow // opt-in migration. Users instead need to add jest as a devDependency themselves and specify // the version they want. This prints a helpful error message if jest is not found, i.e. they diff --git a/packages/cli/src/modules/test/commands/repo/test.ts b/packages/cli/src/modules/test/commands/repo/test.ts index 1f57279935..bb614798b4 100644 --- a/packages/cli/src/modules/test/commands/repo/test.ts +++ b/packages/cli/src/modules/test/commands/repo/test.ts @@ -22,7 +22,7 @@ import yargs from 'yargs'; import { run as runJest, yargsOptions as jestYargsOptions } from 'jest-cli'; import { relative as relativePath } from 'node:path'; import { Command, OptionValues } from 'commander'; -import { Lockfile, PackageGraph } from '@backstage/cli-node'; +import { Lockfile, PackageGraph, SuccessCache } from '@backstage/cli-node'; import { runCheck, @@ -31,7 +31,6 @@ import { findOwnPaths, isChildPath, } from '@backstage/cli-common'; -import { SuccessCache } from '../../../../lib/cache/SuccessCache'; type JestProject = { displayName: string; @@ -305,11 +304,9 @@ export async function command(opts: OptionValues, cmd: Command): Promise { }--no-node-snapshot`; } - // This ensures that the process doesn't exit too early before stdout is flushed if (args.includes('--jest-help')) { removeOptionArg(args, '--jest-help'); args.push('--help'); - (process.stdout as any)._handle.setBlocking(true); } // This code path is enabled by the --successCache flag, which is specific to @@ -333,7 +330,10 @@ export async function command(opts: OptionValues, cmd: Command): Promise { ); } - const cache = new SuccessCache('test', opts.successCacheDir); + const cache = SuccessCache.create({ + name: 'test', + basePath: opts.successCacheDir, + }); const graph = await getPackageGraph(); // Shared state for the bridge diff --git a/packages/cli/src/modules/test/index.ts b/packages/cli/src/modules/test/index.ts index ecc546aee3..f003d9c054 100644 --- a/packages/cli/src/modules/test/index.ts +++ b/packages/cli/src/modules/test/index.ts @@ -15,7 +15,7 @@ */ import { createCliPlugin } from '../../wiring/factory'; import { Command } from 'commander'; -import { lazy } from '../../lib/lazy'; +import { lazy } from '../../wiring/lazy'; export default createCliPlugin({ pluginId: 'test', diff --git a/packages/cli/src/modules/translations/commands/export.ts b/packages/cli/src/modules/translations/commands/export.ts index 0329b1241a..4ee5658489 100644 --- a/packages/cli/src/modules/translations/commands/export.ts +++ b/packages/cli/src/modules/translations/commands/export.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { cli } from 'cleye'; import { targetPaths } from '@backstage/cli-common'; import fs from 'fs-extra'; import { dirname, resolve as resolvePath } from 'node:path'; @@ -28,16 +29,38 @@ import { } from '../lib/extractTranslations'; import { DEFAULT_LANGUAGE, + DEFAULT_MESSAGE_PATTERN, formatMessagePath, validatePattern, } from '../lib/messageFilePath'; +import type { CommandContext } from '../../../wiring/types'; -interface ExportOptions { - output: string; - pattern: string; -} +export default async ({ args, info }: CommandContext) => { + const { + flags: { output, pattern }, + } = cli( + { + help: info, + flags: { + output: { + type: String, + default: 'translations', + description: 'Output directory for exported messages and manifest', + }, + pattern: { + type: String, + default: DEFAULT_MESSAGE_PATTERN, + description: + 'File path pattern for message files, with {id} and {lang} placeholders', + }, + }, + }, + undefined, + args, + ); + + const options = { output, pattern }; -export default async (options: ExportOptions) => { validatePattern(options.pattern); const targetPackageJson = await readTargetPackage( diff --git a/packages/cli/src/modules/translations/commands/import.ts b/packages/cli/src/modules/translations/commands/import.ts index c104275f46..67b600b495 100644 --- a/packages/cli/src/modules/translations/commands/import.ts +++ b/packages/cli/src/modules/translations/commands/import.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { cli } from 'cleye'; import { targetPaths } from '@backstage/cli-common'; import fs from 'fs-extra'; import { @@ -27,11 +28,7 @@ import { createMessagePathParser, formatMessagePath, } from '../lib/messageFilePath'; - -interface ImportOptions { - input: string; - output: string; -} +import type { CommandContext } from '../../../wiring/types'; interface ManifestRefEntry { package: string; @@ -44,7 +41,31 @@ interface Manifest { refs: Record; } -export default async (options: ImportOptions) => { +export default async ({ args, info }: CommandContext) => { + const { + flags: { input, output }, + } = cli( + { + help: info, + flags: { + input: { + type: String, + default: 'translations', + description: + 'Input directory containing the manifest and translated message files', + }, + output: { + type: String, + default: 'src/translations/resources.ts', + description: 'Output path for the generated wiring module', + }, + }, + }, + undefined, + args, + ); + + const options = { input, output }; await readTargetPackage(targetPaths.dir, targetPaths.rootDir); const inputDir = resolvePath(targetPaths.dir, options.input); diff --git a/packages/cli/src/modules/translations/index.ts b/packages/cli/src/modules/translations/index.ts index 702b0f4f49..43263831d5 100644 --- a/packages/cli/src/modules/translations/index.ts +++ b/packages/cli/src/modules/translations/index.ts @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import yargs from 'yargs'; import { createCliPlugin } from '../../wiring/factory'; -import { lazy } from '../../lib/lazy'; -import { DEFAULT_MESSAGE_PATTERN } from './lib/messageFilePath'; export default createCliPlugin({ pluginId: 'translations', @@ -25,51 +22,14 @@ export default createCliPlugin({ path: ['translations', 'export'], description: 'Export translation messages from an app and all of its frontend plugins to JSON files', - execute: async ({ args }) => { - const argv = await yargs() - .options({ - output: { - type: 'string', - default: 'translations', - description: - 'Output directory for exported messages and manifest', - }, - pattern: { - type: 'string', - default: DEFAULT_MESSAGE_PATTERN, - description: - 'File path pattern for message files, with {id} and {lang} placeholders', - }, - }) - .help() - .parse(args); - await lazy(() => import('./commands/export'), 'default')(argv); - }, + execute: { loader: () => import('./commands/export') }, }); reg.addCommand({ path: ['translations', 'import'], description: 'Generate translation resource wiring from translated JSON files', - execute: async ({ args }) => { - const argv = await yargs() - .options({ - input: { - type: 'string', - default: 'translations', - description: - 'Input directory containing the manifest and translated message files', - }, - output: { - type: 'string', - default: 'src/translations/resources.ts', - description: 'Output path for the generated wiring module', - }, - }) - .help() - .parse(args); - await lazy(() => import('./commands/import'), 'default')(argv); - }, + execute: { loader: () => import('./commands/import') }, }); }, }); diff --git a/packages/cli/src/wiring/CliInitializer.test.ts b/packages/cli/src/wiring/CliInitializer.test.ts index 9ee252cfc5..e04a429ede 100644 --- a/packages/cli/src/wiring/CliInitializer.test.ts +++ b/packages/cli/src/wiring/CliInitializer.test.ts @@ -67,6 +67,31 @@ describe('CliInitializer', () => { expect(process.exit).toHaveBeenCalledWith(0); }); + it('should run commands using a loader', async () => { + expect.assertions(2); + process.argv = ['node', 'cli', 'test', '--verbose']; + const initializer = new CliInitializer(); + initializer.add( + createCliPlugin({ + pluginId: 'test', + init: async reg => + reg.addCommand({ + path: ['test'], + description: 'test', + execute: { + loader: async () => ({ + default: async ({ args }) => { + expect(args).toEqual(['--verbose']); + }, + }), + }, + }), + }), + ); + await initializer.run(); + expect(process.exit).toHaveBeenCalledWith(0); + }); + it('should pass positional args to the subcommand if nested', async () => { expect.assertions(2); process.argv = [ diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index eab7961b40..c07fbb26f3 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -18,9 +18,9 @@ import { CommandGraph } from './CommandGraph'; import { CliFeature, OpaqueCliPlugin } from './types'; import { CommandRegistry } from './CommandRegistry'; import { Command } from 'commander'; -import { version } from '../lib/version'; +import { version } from './version'; import chalk from 'chalk'; -import { exitWithError } from '../lib/errors'; +import { exitWithError } from './errors'; import { ForwardedError } from '@backstage/errors'; import { isPromise } from 'node:util/types'; @@ -118,13 +118,25 @@ export class CliInitializer { } positionalArgs.push(nonProcessArgs[argIndex]); } - await node.command.execute({ + const context = { args: [...positionalArgs, ...args.unknown], info: { usage: [programName, ...node.command.path].join(' '), description: node.command.description, }, - }); + }; + + if (typeof node.command.execute === 'function') { + await node.command.execute(context); + } else { + const mod = await node.command.execute.loader(); + // Handle CJS double-wrapping of default exports + const fn = + typeof mod.default === 'function' + ? mod.default + : (mod.default as any).default; + await fn(context); + } process.exit(0); } catch (error: unknown) { exitWithError(error); @@ -144,7 +156,7 @@ export class CliInitializer { exitWithError(new ForwardedError('Unhandled rejection', rejection)); }); - program.parse(process.argv); + await program.parseAsync(process.argv); } } diff --git a/packages/cli/src/lib/errors.ts b/packages/cli/src/wiring/errors.ts similarity index 100% rename from packages/cli/src/lib/errors.ts rename to packages/cli/src/wiring/errors.ts diff --git a/packages/cli/src/lib/lazy.ts b/packages/cli/src/wiring/lazy.ts similarity index 96% rename from packages/cli/src/lib/lazy.ts rename to packages/cli/src/wiring/lazy.ts index d1255ea1bc..64a4d43ef4 100644 --- a/packages/cli/src/lib/lazy.ts +++ b/packages/cli/src/wiring/lazy.ts @@ -15,7 +15,7 @@ */ import { assertError } from '@backstage/errors'; -import { exitWithError } from '../lib/errors'; +import { exitWithError } from './errors'; type ActionFunc = (...args: any[]) => Promise; type ActionExports = { diff --git a/packages/cli/src/wiring/types.ts b/packages/cli/src/wiring/types.ts index fc2c2b454d..e0f697990f 100644 --- a/packages/cli/src/wiring/types.ts +++ b/packages/cli/src/wiring/types.ts @@ -15,23 +15,31 @@ */ import { OpaqueType } from '@internal/opaque'; +export interface CommandContext { + args: string[]; + info: { + /** + * The usage string of the current command, for example: "backstage-cli repo test" + */ + usage: string; + /** + * The description provided for the command + */ + description: string; + }; +} + +export type CommandExecuteFn = (context: CommandContext) => Promise; + export interface BackstageCommand { path: string[]; description: string; deprecated?: boolean; - execute: (context: { - args: string[]; - info: { - /** - * The usage string of the current command, for example: "backstage-cli repo test" - */ - usage: string; - /** - * The description provided for the command - */ - description: string; - }; - }) => Promise; + execute: + | CommandExecuteFn + | { + loader: () => Promise<{ default: CommandExecuteFn }>; + }; } export type CliFeature = CliPlugin; diff --git a/packages/cli/src/wiring/version.ts b/packages/cli/src/wiring/version.ts new file mode 100644 index 0000000000..d7d0b3a2fe --- /dev/null +++ b/packages/cli/src/wiring/version.ts @@ -0,0 +1,29 @@ +/* + * 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 fs from 'fs-extra'; +import { findOwnPaths } from '@backstage/cli-common'; + +/* eslint-disable-next-line no-restricted-syntax */ +const ownPaths = findOwnPaths(__dirname); + +export function findVersion() { + const pkgContent = fs.readFileSync(ownPaths.resolve('package.json'), 'utf8'); + return JSON.parse(pkgContent).version; +} + +export const version = findVersion(); +export const isDev = fs.pathExistsSync(ownPaths.resolve('src')); diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index 77b4d0f9cb..38ccd0f874 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/codemods +## 0.1.55-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + ## 0.1.54 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 6a8a464a37..c531c4ee69 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/codemods", - "version": "0.1.54", + "version": "0.1.55-next.0", "description": "A collection of codemods for Backstage projects", "backstage": { "role": "cli" diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index 97831338a6..22a9f2c76f 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/config-loader +## 1.10.9-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + ## 1.10.8 ### Patch Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 985bdef1a5..18b16787ab 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/config-loader", - "version": "1.10.8", + "version": "1.10.9-next.0", "description": "Config loading functionality used by Backstage backend, and CLI", "backstage": { "role": "node-library" diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index b9d741ea9c..42d0581f64 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/core-app-api +## 1.19.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + ## 1.19.5 ### Patch Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 242db797ad..231ee0f4c1 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-app-api", - "version": "1.19.5", + "version": "1.19.6-next.0", "description": "Core app API used by Backstage apps", "backstage": { "role": "web-library" diff --git a/packages/core-compat-api/CHANGELOG.md b/packages/core-compat-api/CHANGELOG.md index 7a45beca2d..f05a32f697 100644 --- a/packages/core-compat-api/CHANGELOG.md +++ b/packages/core-compat-api/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/core-compat-api +## 0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + +## 0.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + ## 0.5.8 ### Patch Changes diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index 1a03a36bd2..605c6b2bd9 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-compat-api", - "version": "0.5.8", + "version": "0.5.9-next.1", "backstage": { "role": "web-library" }, diff --git a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx index c5138dac3d..f9e8793613 100644 --- a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx +++ b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx @@ -156,7 +156,7 @@ describe('collectLegacyRoutes', () => { if={isKind('component')} children={ - + } /> diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index a9b0d68ddf..4407f55bd7 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/core-components +## 0.18.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/theme@0.7.2 + - @backstage/version-bridge@1.0.12 + ## 0.18.7 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 0ba7a89ceb..02f9b782c1 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-components", - "version": "0.18.7", + "version": "0.18.8-next.0", "description": "Core components used by Backstage plugins and apps", "backstage": { "role": "web-library" diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index eebb94a45f..61c5d409c2 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/core-plugin-api +## 1.12.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + ## 1.12.3 ### Patch Changes diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 202f532b95..d199bfa8da 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-plugin-api", - "version": "1.12.3", + "version": "1.12.4-next.0", "description": "Core API used by Backstage plugins", "backstage": { "role": "web-library" diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 39c6951d85..5923a75a26 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/create-app +## 0.7.10-next.1 + +### Patch Changes + +- a9d23c4: Properly support `package.json` `workspaces` field +- ebd4630: Replace deprecated `workspaces.packages` with `workspaces` in `package.json` + + This change is **not** required, but you can edit your main `package.json`, to fit the more modern & more common pattern: + + ```diff + - "workspaces": { + - "packages": [ + "workspaces": [ + "packages/*", + "plugins/*" + - ] + - }, + ], + ``` + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + +## 0.7.10-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + ## 0.7.9 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 4841169251..8a4526d63b 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/create-app", - "version": "0.7.9", + "version": "0.7.10-next.1", "description": "A CLI that helps you create your own Backstage app", "backstage": { "role": "cli" diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 4e723ea078..229ed5ffa0 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -22,12 +22,10 @@ "prettier:check": "prettier --check .", "new": "backstage-cli new" }, - "workspaces": { - "packages": [ - "packages/*", - "plugins/*" - ] - }, + "workspaces": [ + "packages/*", + "plugins/*" + ], "devDependencies": { "@backstage/cli": "^{{version '@backstage/cli'}}", "@backstage/e2e-test-utils": "^{{version '@backstage/e2e-test-utils'}}", diff --git a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx index f75d984366..750dfe6cea 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx @@ -128,7 +128,7 @@ const overviewContent = ( {entityWarningContent} - + @@ -298,7 +298,7 @@ const userPage = ( {entityWarningContent} - + @@ -314,7 +314,7 @@ const groupPage = ( {entityWarningContent} - + @@ -336,7 +336,7 @@ const systemPage = ( {entityWarningContent} - + @@ -383,7 +383,7 @@ const domainPage = ( {entityWarningContent} - + diff --git a/packages/create-app/templates/next-app/package.json.hbs b/packages/create-app/templates/next-app/package.json.hbs index 80ff4a9a06..27d5880c11 100644 --- a/packages/create-app/templates/next-app/package.json.hbs +++ b/packages/create-app/templates/next-app/package.json.hbs @@ -44,12 +44,10 @@ } } }, - "workspaces": { - "packages": [ - "packages/*", - "plugins/*" - ] - }, + "workspaces": [ + "packages/*", + "plugins/*" + ], "devDependencies": { "@backstage/cli": "^{{version '@backstage/cli'}}", "@backstage/e2e-test-utils": "^{{version '@backstage/e2e-test-utils'}}", diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 51957793b4..f00681aa3c 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/dev-utils +## 1.1.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/plugin-catalog-react@2.1.0-next.1 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/theme@0.7.2 + +## 1.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/plugin-catalog-react@2.0.1-next.0 + - @backstage/app-defaults@1.7.6-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/theme@0.7.2 + ## 1.1.20 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 0f59ec8d27..7ddfd34b57 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/dev-utils", - "version": "1.1.20", + "version": "1.1.21-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 7fbdca51c4..525bc47606 100644 --- a/packages/e2e-test/CHANGELOG.md +++ b/packages/e2e-test/CHANGELOG.md @@ -1,5 +1,14 @@ # e2e-test +## 0.2.38-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/create-app@0.7.10-next.0 + - @backstage/errors@1.2.7 + ## 0.2.37 ### Patch Changes diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 1a2142d6e9..782fea0710 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -1,6 +1,6 @@ { "name": "e2e-test", - "version": "0.2.37", + "version": "0.2.38-next.0", "description": "E2E test for verifying Backstage packages", "backstage": { "role": "cli" diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index fa9e3d5c84..71b385bcfe 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/eslint-plugin +## 0.2.2-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 + ## 0.2.1 ### Patch Changes diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index bfc3996ee3..89e08206ce 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/eslint-plugin", - "version": "0.2.1", + "version": "0.2.2-next.0", "description": "Backstage ESLint plugin", "publishConfig": { "access": "public" diff --git a/packages/frontend-app-api/CHANGELOG.md b/packages/frontend-app-api/CHANGELOG.md index 0ca0df5ac2..d35e1a6dfc 100644 --- a/packages/frontend-app-api/CHANGELOG.md +++ b/packages/frontend-app-api/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/frontend-app-api +## 0.15.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + ## 0.15.0 ### Minor Changes diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index b3e978990c..997c3c6110 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.15.0", + "version": "0.15.1-next.0", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-defaults/CHANGELOG.md b/packages/frontend-defaults/CHANGELOG.md index b157fd2907..9168af715c 100644 --- a/packages/frontend-defaults/CHANGELOG.md +++ b/packages/frontend-defaults/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/frontend-defaults +## 0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-app@0.4.1-next.0 + - @backstage/config@1.3.6 + - @backstage/core-components@0.18.8-next.0 + - @backstage/errors@1.2.7 + - @backstage/frontend-app-api@0.15.1-next.0 + ## 0.4.0 ### Minor Changes diff --git a/packages/frontend-defaults/package.json b/packages/frontend-defaults/package.json index 26411b6d98..92951da7c5 100644 --- a/packages/frontend-defaults/package.json +++ b/packages/frontend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-defaults", - "version": "0.4.0", + "version": "0.4.1-next.0", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-dynamic-feature-loader/CHANGELOG.md b/packages/frontend-dynamic-feature-loader/CHANGELOG.md index 1525b390d8..c616f235e4 100644 --- a/packages/frontend-dynamic-feature-loader/CHANGELOG.md +++ b/packages/frontend-dynamic-feature-loader/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/frontend-dynamic-feature-loader +## 0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/module-federation-common@0.1.2-next.0 + - @backstage/config@1.3.6 + - @backstage/frontend-plugin-api@0.14.2-next.0 + +## 0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/config@1.3.6 + - @backstage/module-federation-common@0.1.0 + ## 0.1.9 ### Patch Changes diff --git a/packages/frontend-dynamic-feature-loader/package.json b/packages/frontend-dynamic-feature-loader/package.json index 49007bdd39..860fe56bb5 100644 --- a/packages/frontend-dynamic-feature-loader/package.json +++ b/packages/frontend-dynamic-feature-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-dynamic-feature-loader", - "version": "0.1.9", + "version": "0.1.10-next.1", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-dynamic-feature-loader/src/loader.test.tsx b/packages/frontend-dynamic-feature-loader/src/loader.test.tsx index cb066e8b21..081b3c730a 100644 --- a/packages/frontend-dynamic-feature-loader/src/loader.test.tsx +++ b/packages/frontend-dynamic-feature-loader/src/loader.test.tsx @@ -165,7 +165,7 @@ describe('dynamicFrontendFeaturesLoader', () => { shareConfig: { singleton: true, requiredVersion: '*', eager: true }, }, { - name: '@mui/material/styles/', + name: '@mui/material/styles', version: '5.16.14', lib: async () => ({ default: {} }), shareConfig: { singleton: true, requiredVersion: '*', eager: true }, diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/Api.client.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/Api.client.ts index 8623dc3c33..9802afba0f 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/Api.client.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/Api.client.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ // ****************************************************************** // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** + import { DiscoveryApi } from '../types/discovery'; import { FetchApi } from '../types/fetch'; import crossFetch from 'cross-fetch'; diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/index.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/index.ts index fc7c83b736..c2e931caf8 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/index.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/index.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/index.ts index dc3055033d..19501a5dcb 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/index.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorError.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorError.model.ts index e0265e95d7..853f88cc14 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorRequest.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorRequest.model.ts index 3eb5e15740..1591911bc9 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorResponse.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorResponse.model.ts index edbcc32df7..1eff0557ed 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ModelError.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ModelError.model.ts index 958fde7d0b..9d5c36325e 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ModelError.model.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,6 @@ import { ErrorResponse } from '../models/ErrorResponse.model'; */ export interface ModelError { [key: string]: any; - error: ErrorError; request?: ErrorRequest; response: ErrorResponse; diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/Remote.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/Remote.model.ts index 7440110ad8..ff5cae705f 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/Remote.model.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/Remote.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/RemoteInfo.model.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/RemoteInfo.model.ts index 72edba90d4..6e8bdeff0a 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/RemoteInfo.model.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/RemoteInfo.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/index.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/index.ts index 0c54a6585b..4cf296a0b9 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/index.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/pluginId.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/pluginId.ts index eaea330f99..465ea2d806 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/pluginId.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/generated/pluginId.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/frontend-dynamic-feature-loader/src/schema/openapi/index.ts b/packages/frontend-dynamic-feature-loader/src/schema/openapi/index.ts index 196aad553a..fb49a2b9c0 100644 --- a/packages/frontend-dynamic-feature-loader/src/schema/openapi/index.ts +++ b/packages/frontend-dynamic-feature-loader/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * Copyright 2026 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/frontend-internal/CHANGELOG.md b/packages/frontend-internal/CHANGELOG.md index 958c0f3fb6..4005f83616 100644 --- a/packages/frontend-internal/CHANGELOG.md +++ b/packages/frontend-internal/CHANGELOG.md @@ -1,5 +1,14 @@ # @internal/frontend +## 0.0.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + ## 0.0.17 ### Patch Changes diff --git a/packages/frontend-internal/package.json b/packages/frontend-internal/package.json index 57b4eba729..cab3779acd 100644 --- a/packages/frontend-internal/package.json +++ b/packages/frontend-internal/package.json @@ -1,6 +1,6 @@ { "name": "@internal/frontend", - "version": "0.0.17", + "version": "0.0.18-next.0", "backstage": { "role": "web-library", "inline": true diff --git a/packages/frontend-plugin-api/CHANGELOG.md b/packages/frontend-plugin-api/CHANGELOG.md index 7238a08512..ed6349d9e4 100644 --- a/packages/frontend-plugin-api/CHANGELOG.md +++ b/packages/frontend-plugin-api/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/frontend-plugin-api +## 0.14.2-next.0 + +### Patch Changes + +- 9c81af9: Made the `pluginId` property optional in the `FrontendFeature` type, allowing plugins published against older versions of the framework to be used without type errors. +- Updated dependencies + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + ## 0.14.0 ### Minor Changes diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index 7b4ae8f36b..5002ed98a0 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.14.0", + "version": "0.14.2-next.0", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-test-utils/CHANGELOG.md b/packages/frontend-test-utils/CHANGELOG.md index 040cdac005..49a007e798 100644 --- a/packages/frontend-test-utils/CHANGELOG.md +++ b/packages/frontend-test-utils/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/frontend-test-utils +## 0.5.1-next.1 + +### Patch Changes + +- 479282f: Fixed type inference of `TestApiPair` when using tuple syntax by wrapping `MockWithApiFactory` in `NoInfer`. +- Updated dependencies + - @backstage/plugin-app@0.4.1-next.1 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + +## 0.5.1-next.0 + +### Patch Changes + +- 909c742: Switched `MockTranslationApi` and related test utility imports from `@backstage/core-plugin-api/alpha` to the stable `@backstage/frontend-plugin-api` export. The `TranslationApi` type in the API report is now sourced from a single package. This has no effect on runtime behavior. +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-app@0.4.1-next.0 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/frontend-app-api@0.15.1-next.0 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/types@1.2.2 + - @backstage/version-bridge@1.0.12 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + ## 0.5.0 ### Minor Changes diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index 6e7f487654..bcea275124 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.5.0", + "version": "0.5.1-next.1", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-test-utils/report.api.md b/packages/frontend-test-utils/report.api.md index 1cba02f393..a17c2ed483 100644 --- a/packages/frontend-test-utils/report.api.md +++ b/packages/frontend-test-utils/report.api.md @@ -443,7 +443,7 @@ export type RenderTestAppOptions = { // @public export type TestApiPair = | readonly [ApiRef, TApi extends infer TImpl ? Partial : never] - | MockWithApiFactory; + | MockWithApiFactory>; // @public export type TestApiPairs = { diff --git a/packages/frontend-test-utils/src/apis/TestApiProvider.test.tsx b/packages/frontend-test-utils/src/apis/TestApiProvider.test.tsx new file mode 100644 index 0000000000..1bb5673751 --- /dev/null +++ b/packages/frontend-test-utils/src/apis/TestApiProvider.test.tsx @@ -0,0 +1,95 @@ +/* + * 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 { createApiRef } from '@backstage/frontend-plugin-api'; +import { TestApiProvider } from './TestApiProvider'; +import { mockApis } from './mockApis'; +import { render, screen } from '@testing-library/react'; + +const xApiRef = createApiRef<{ a: string; b: number }>({ + id: 'x', +}); +const yApiRef = createApiRef({ + id: 'y', +}); + +describe('TestApiProvider', () => { + it('should provide tuple APIs and check types', () => { + render( + +
    + , + ); + }); + + it('should allow partial API implementations', () => { + render( + +
    + , + ); + }); + + it('should reject mismatched types in tuple syntax', () => { + render( + // @ts-expect-error - a should be a string, not a number + +
    + , + ); + }); + + it('should accept MockWithApiFactory entries', () => { + render( + +
    + , + ); + }); + + it('should accept a mix of tuples and MockWithApiFactory entries', () => { + render( + +
    + , + ); + }); + + it('should allow empty APIs', () => { + render( + +
    + , + ); + }); + + it('should provide APIs at runtime', async () => { + const alertApi = mockApis.alert(); + + render( + + rendered + , + ); + + expect(await screen.findByText('rendered')).toBeInTheDocument(); + }); +}); diff --git a/packages/frontend-test-utils/src/apis/TestApiProvider.tsx b/packages/frontend-test-utils/src/apis/TestApiProvider.tsx index 215e8c8a71..d0eca87a33 100644 --- a/packages/frontend-test-utils/src/apis/TestApiProvider.tsx +++ b/packages/frontend-test-utils/src/apis/TestApiProvider.tsx @@ -29,7 +29,7 @@ import { */ export type TestApiPair = | readonly [ApiRef, TApi extends infer TImpl ? Partial : never] - | MockWithApiFactory; + | MockWithApiFactory>; /** * Represents an array of mock API implementation. diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index c5b3247473..507d66ce4e 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/integration-react +## 1.2.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@2.0.0-next.1 + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + +## 1.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.21.0-next.0 + - @backstage/config@1.3.6 + - @backstage/core-plugin-api@1.12.4-next.0 + ## 1.2.15 ### Patch Changes diff --git a/packages/integration-react/dev/DevPage.tsx b/packages/integration-react/dev/DevPage.tsx index d03baca1d7..1c94ef984a 100644 --- a/packages/integration-react/dev/DevPage.tsx +++ b/packages/integration-react/dev/DevPage.tsx @@ -16,7 +16,7 @@ import { ScmIntegration, ScmIntegrationsGroup } from '@backstage/integration'; import Typography from '@material-ui/core/Typography'; -import { scmIntegrationsApiRef } from '../src/ScmIntegrationsApi'; +import { scmIntegrationsApiRef } from '../src/api/ScmIntegrationsApi'; import { Content } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; @@ -48,10 +48,6 @@ export const DevPage = () => { Azure - - Bitbucket - - Bitbucket Cloud diff --git a/packages/integration-react/dev/index.tsx b/packages/integration-react/dev/index.tsx index 65486d1f06..fbff7ef0c6 100644 --- a/packages/integration-react/dev/index.tsx +++ b/packages/integration-react/dev/index.tsx @@ -15,7 +15,7 @@ */ import { createDevApp } from '@backstage/dev-utils'; import { ScmIntegrations } from '@backstage/integration'; -import { scmIntegrationsApiRef } from '../src/ScmIntegrationsApi'; +import { scmIntegrationsApiRef } from '../src/api/ScmIntegrationsApi'; import { DevPage } from './DevPage'; import { configApiRef, createApiFactory } from '@backstage/core-plugin-api'; diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index a2c4cd7e78..ba38b2fc33 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration-react", - "version": "1.2.15", + "version": "1.2.16-next.1", "description": "Frontend package for managing integrations towards external systems", "backstage": { "role": "web-library" diff --git a/packages/integration-react/src/api/ScmIntegrationsApi.test.ts b/packages/integration-react/src/api/ScmIntegrationsApi.test.ts index 55acd29eec..0a1f1b1b8f 100644 --- a/packages/integration-react/src/api/ScmIntegrationsApi.test.ts +++ b/packages/integration-react/src/api/ScmIntegrationsApi.test.ts @@ -26,6 +26,6 @@ describe('scmIntegrationsApiRef', () => { it('should be instantiated', () => { const i = ScmIntegrationsApi.fromConfig(new ConfigReader({})); - expect(i.list().length).toBe(8); // The default ones + expect(i.list().length).toBe(7); // The default ones }); }); diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index 888613617f..1ca9d407af 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/integration +## 2.0.0-next.1 + +### Major Changes + +- 527cf88: **BREAKING** Removed deprecated Azure DevOps, Bitbucket, Gerrit and GitHub code: + + - For Azure DevOps, the long deprecated `token` string and `credential` object have been removed from the `config.d.ts`. Use the `credentials` array object instead. + - For Bitbucket, the long deprecated `bitbucket` object has been removed from the `config.d.ts`. Use the `bitbucketCloud` or `bitbucketServer` objects instead. + - For Gerrit, the `parseGerritGitilesUrl` function has been removed, use `parseGitilesUrlRef` instead. The `buildGerritGitilesArchiveUrl` function has also been removed, use `buildGerritGitilesArchiveUrlFromLocation` instead. + - For GitHub, the `getGitHubRequestOptions` function has been removed. + +### Patch Changes + +- 993a598: Fixed Azure integration config schema visibility annotations to use per-field `@visibility secret` instead of `@deepVisibility secret` on parent objects, so that non-secret fields like `clientId`, `tenantId`, `organizations`, and `managedIdentityClientId` are no longer incorrectly marked as secret. +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + +## 1.21.0-next.0 + +### Minor Changes + +- d933f62: Add configurable throttling and retry mechanism for GitLab integration. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + ## 1.20.0 ### Minor Changes diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index add237a6b9..8ea8dc2caf 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -27,40 +27,20 @@ export interface Config { * @visibility frontend */ host: string; - /** - * Token used to authenticate requests. - * @visibility secret - * @deprecated Use `credentials` instead. - */ - token?: string; - - /** - * The credential to use for requests. - * - * If no credential is specified anonymous access is used. - * - * @deepVisibility secret - * @deprecated Use `credentials` instead. - */ - credential?: { - clientId?: string; - clientSecret?: string; - tenantId?: string; - personalAccessToken?: string; - }; /** * The credentials to use for requests. If multiple credentials are specified the first one that matches the organization is used. * If no organization matches the first credential without an organization is used. * * If no credentials are specified at all, either a default credential (for Azure DevOps) or anonymous access (for Azure DevOps Server) is used. - * @deepVisibility secret */ credentials?: { organizations?: string[]; clientId?: string; + /** @visibility secret */ clientSecret?: string; tenantId?: string; + /** @visibility secret */ personalAccessToken?: string; managedIdentityClientId?: string; }[]; @@ -111,7 +91,6 @@ export interface Config { endpoint?: string; /** * Optional credential to use for Azure Active Directory authentication. - * @deepVisibility secret */ aadCredential?: { /** @@ -126,48 +105,12 @@ export interface Config { /** * The client secret for the Azure AD application. + * @visibility secret */ clientSecret: string; }; }>; - /** - * Integration configuration for Bitbucket - * @deprecated replaced by bitbucketCloud and bitbucketServer - */ - bitbucket?: Array<{ - /** - * The hostname of the given Bitbucket instance - * @visibility frontend - */ - host: string; - /** - * Token used to authenticate requests. - * @visibility secret - */ - token?: string; - /** - * The base url for the Bitbucket API, for example https://api.bitbucket.org/2.0 - * @visibility frontend - */ - apiBaseUrl?: string; - /** - * The username to use for authenticated requests. - * @visibility secret - */ - username?: string; - /** - * Bitbucket app password used to authenticate requests. - * @visibility secret - */ - appPassword?: string; - /** - * PGP signing key for signing commits. - * @visibility secret - */ - commitSigningKey?: string; - }>; - /** Integration configuration for Bitbucket Cloud */ bitbucketCloud?: Array<{ /** diff --git a/packages/integration/package.json b/packages/integration/package.json index c3e0b07340..0fbd80b1b3 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration", - "version": "1.20.0", + "version": "2.0.0-next.1", "description": "Helpers for managing integrations towards external systems", "backstage": { "role": "common-library" diff --git a/packages/integration/report.api.md b/packages/integration/report.api.md index 83c75db4d1..df1c7e9112 100644 --- a/packages/integration/report.api.md +++ b/packages/integration/report.api.md @@ -201,8 +201,6 @@ export class AzureIntegration implements ScmIntegration { // @public export type AzureIntegrationConfig = { host: string; - token?: string; - credential?: AzureDevOpsCredential; credentials?: AzureDevOpsCredential[]; commitSigningKey?: string; }; @@ -255,37 +253,6 @@ export type BitbucketCloudIntegrationConfig = { commitSigningKey?: string; }; -// @public @deprecated -export class BitbucketIntegration implements ScmIntegration { - constructor(integrationConfig: BitbucketIntegrationConfig); - // (undocumented) - get config(): BitbucketIntegrationConfig; - // (undocumented) - static factory: ScmIntegrationsFactory; - // (undocumented) - resolveEditUrl(url: string): string; - // (undocumented) - resolveUrl(options: { - url: string; - base: string; - lineNumber?: number; - }): string; - // (undocumented) - get title(): string; - // (undocumented) - get type(): string; -} - -// @public @deprecated -export type BitbucketIntegrationConfig = { - host: string; - apiBaseUrl: string; - token?: string; - username?: string; - appPassword?: string; - commitSigningKey?: string; -}; - // @public export class BitbucketServerIntegration implements ScmIntegration { constructor(integrationConfig: BitbucketServerIntegrationConfig); @@ -317,14 +284,6 @@ export type BitbucketServerIntegrationConfig = { commitSigningKey?: string; }; -// @public @deprecated -export function buildGerritGitilesArchiveUrl( - config: GerritIntegrationConfig, - project: string, - branch: string, - filePath: string, -): string; - // @public export function buildGerritGitilesArchiveUrlFromLocation( config: GerritIntegrationConfig, @@ -426,14 +385,6 @@ export function getAzureDownloadUrl(url: string): string; // @public export function getAzureFileFetchUrl(url: string): string; -// @public @deprecated -export function getAzureRequestOptions( - config: AzureIntegrationConfig, - additionalHeaders?: Record, -): Promise<{ - headers: Record; -}>; - // @public export function getBitbucketCloudDefaultBranch( url: string, @@ -465,31 +416,6 @@ export function getBitbucketCloudRequestOptions( headers: Record; }>; -// @public @deprecated -export function getBitbucketDefaultBranch( - url: string, - config: BitbucketIntegrationConfig, -): Promise; - -// @public @deprecated -export function getBitbucketDownloadUrl( - url: string, - config: BitbucketIntegrationConfig, -): Promise; - -// @public @deprecated -export function getBitbucketFileFetchUrl( - url: string, - config: BitbucketIntegrationConfig, -): string; - -// @public @deprecated -export function getBitbucketRequestOptions( - config: BitbucketIntegrationConfig, -): { - headers: Record; -}; - // @public export function getBitbucketServerDefaultBranch( url: string, @@ -579,14 +505,6 @@ export function getGithubFileFetchUrl( credentials: GithubCredentials, ): string; -// @public @deprecated -export function getGitHubRequestOptions( - config: GithubIntegrationConfig, - credentials: GithubCredentials, -): { - headers: Record; -}; - // @public export function getGitilesAuthenticationUrl( config: GerritIntegrationConfig, @@ -854,8 +772,6 @@ export interface IntegrationsByType { azure: ScmIntegrationsGroup; // (undocumented) azureBlobStorage: ScmIntegrationsGroup; - // @deprecated (undocumented) - bitbucket: ScmIntegrationsGroup; // (undocumented) bitbucketCloud: ScmIntegrationsGroup; // (undocumented) @@ -874,16 +790,6 @@ export interface IntegrationsByType { harness: ScmIntegrationsGroup; } -// @public @deprecated -export function parseGerritGitilesUrl( - config: GerritIntegrationConfig, - url: string, -): { - branch: string; - filePath: string; - project: string; -}; - // @public export function parseGerritJsonResponse(response: Response): Promise; @@ -989,16 +895,6 @@ export function readBitbucketCloudIntegrationConfigs( configs: Config[], ): BitbucketCloudIntegrationConfig[]; -// @public @deprecated -export function readBitbucketIntegrationConfig( - config: Config, -): BitbucketIntegrationConfig; - -// @public @deprecated -export function readBitbucketIntegrationConfigs( - configs: Config[], -): BitbucketIntegrationConfig[]; - // @public export function readBitbucketServerIntegrationConfig( config: Config, @@ -1085,8 +981,6 @@ export interface ScmIntegrationRegistry azure: ScmIntegrationsGroup; // (undocumented) azureBlobStorage: ScmIntegrationsGroup; - // @deprecated (undocumented) - bitbucket: ScmIntegrationsGroup; // (undocumented) bitbucketCloud: ScmIntegrationsGroup; // (undocumented) @@ -1120,8 +1014,6 @@ export class ScmIntegrations implements ScmIntegrationRegistry { get azure(): ScmIntegrationsGroup; // (undocumented) get azureBlobStorage(): ScmIntegrationsGroup; - // @deprecated (undocumented) - get bitbucket(): ScmIntegrationsGroup; // (undocumented) get bitbucketCloud(): ScmIntegrationsGroup; // (undocumented) diff --git a/packages/integration/src/ScmIntegrations.test.ts b/packages/integration/src/ScmIntegrations.test.ts index 7f75ab44fc..254d774a80 100644 --- a/packages/integration/src/ScmIntegrations.test.ts +++ b/packages/integration/src/ScmIntegrations.test.ts @@ -21,8 +21,6 @@ import { BitbucketCloudIntegration, BitbucketCloudIntegrationConfig, } from './bitbucketCloud'; -import { BitbucketIntegrationConfig } from './bitbucket'; -import { BitbucketIntegration } from './bitbucket/BitbucketIntegration'; import { BitbucketServerIntegration, BitbucketServerIntegrationConfig, @@ -62,10 +60,6 @@ describe('ScmIntegrations', () => { host: 'azureblobstorage.local', } as AzureBlobStorageIntegrationConfig); - const bitbucket = new BitbucketIntegration({ - host: 'bitbucket.local', - } as BitbucketIntegrationConfig); - const bitbucketCloud = new BitbucketCloudIntegration({ host: 'bitbucket.org', } as BitbucketCloudIntegrationConfig); @@ -103,7 +97,6 @@ describe('ScmIntegrations', () => { awsCodeCommit: basicIntegrations([awsCodeCommit], item => item.config.host), azure: basicIntegrations([azure], item => item.config.host), azureBlobStorage: basicIntegrations([azureBlob], item => item.config.host), - bitbucket: basicIntegrations([bitbucket], item => item.config.host), bitbucketCloud: basicIntegrations([bitbucketCloud], item => item.title), bitbucketServer: basicIntegrations( [bitbucketServer], @@ -126,7 +119,6 @@ describe('ScmIntegrations', () => { expect(i.azureBlobStorage.byUrl('https://azureblobstorage.local')).toBe( azureBlob, ); - expect(i.bitbucket.byUrl('https://bitbucket.local')).toBe(bitbucket); expect(i.bitbucketCloud.byUrl('https://bitbucket.org')).toBe( bitbucketCloud, ); @@ -147,7 +139,6 @@ describe('ScmIntegrations', () => { awsCodeCommit, azure, azureBlob, - bitbucket, bitbucketCloud, bitbucketServer, gerrit, @@ -166,7 +157,6 @@ describe('ScmIntegrations', () => { expect(i.azureBlobStorage.byUrl('https://azureblobstorage.local')).toBe( azureBlob, ); - expect(i.byUrl('https://bitbucket.local')).toBe(bitbucket); expect(i.byUrl('https://bitbucket.org')).toBe(bitbucketCloud); expect(i.byUrl('https://bitbucket-server.local')).toBe(bitbucketServer); expect(i.byUrl('https://gerrit.local')).toBe(gerrit); @@ -179,7 +169,6 @@ describe('ScmIntegrations', () => { expect(i.byHost('awscodecommit.local')).toBe(awsCodeCommit); expect(i.byHost('azure.local')).toBe(azure); expect(i.byHost('azureblobstorage.local')).toBe(azureBlob); - expect(i.byHost('bitbucket.local')).toBe(bitbucket); expect(i.byHost('bitbucket.org')).toBe(bitbucketCloud); expect(i.byHost('bitbucket-server.local')).toBe(bitbucketServer); expect(i.byHost('gerrit.local')).toBe(gerrit); diff --git a/packages/integration/src/ScmIntegrations.ts b/packages/integration/src/ScmIntegrations.ts index 573975da0e..26a278fdbf 100644 --- a/packages/integration/src/ScmIntegrations.ts +++ b/packages/integration/src/ScmIntegrations.ts @@ -19,7 +19,6 @@ import { AwsS3Integration } from './awsS3/AwsS3Integration'; import { AwsCodeCommitIntegration } from './awsCodeCommit/AwsCodeCommitIntegration'; import { AzureIntegration } from './azure/AzureIntegration'; import { BitbucketCloudIntegration } from './bitbucketCloud/BitbucketCloudIntegration'; -import { BitbucketIntegration } from './bitbucket/BitbucketIntegration'; import { BitbucketServerIntegration } from './bitbucketServer/BitbucketServerIntegration'; import { GerritIntegration } from './gerrit/GerritIntegration'; import { GithubIntegration } from './github/GithubIntegration'; @@ -42,10 +41,6 @@ export interface IntegrationsByType { awsCodeCommit: ScmIntegrationsGroup; azureBlobStorage: ScmIntegrationsGroup; azure: ScmIntegrationsGroup; - /** - * @deprecated in favor of `bitbucketCloud` and `bitbucketServer` - */ - bitbucket: ScmIntegrationsGroup; bitbucketCloud: ScmIntegrationsGroup; bitbucketServer: ScmIntegrationsGroup; gerrit: ScmIntegrationsGroup; @@ -70,7 +65,6 @@ export class ScmIntegrations implements ScmIntegrationRegistry { awsCodeCommit: AwsCodeCommitIntegration.factory({ config }), azureBlobStorage: AzureBlobStorageIntergation.factory({ config }), azure: AzureIntegration.factory({ config }), - bitbucket: BitbucketIntegration.factory({ config }), bitbucketCloud: BitbucketCloudIntegration.factory({ config }), bitbucketServer: BitbucketServerIntegration.factory({ config }), gerrit: GerritIntegration.factory({ config }), @@ -102,13 +96,6 @@ export class ScmIntegrations implements ScmIntegrationRegistry { return this.byType.azure; } - /** - * @deprecated in favor of `bitbucketCloud()` and `bitbucketServer()` - */ - get bitbucket(): ScmIntegrationsGroup { - return this.byType.bitbucket; - } - get bitbucketCloud(): ScmIntegrationsGroup { return this.byType.bitbucketCloud; } @@ -148,20 +135,10 @@ export class ScmIntegrations implements ScmIntegrationRegistry { } byUrl(url: string | URL): ScmIntegration | undefined { - let candidates = Object.values(this.byType) + const candidates = Object.values(this.byType) .map(i => i.byUrl(url)) .filter(Boolean); - // Do not return deprecated integrations if there are other options - if (candidates.length > 1) { - const filteredCandidates = candidates.filter( - x => !(x instanceof BitbucketIntegration), - ); - if (filteredCandidates.length !== 0) { - candidates = filteredCandidates; - } - } - return candidates[0]; } diff --git a/packages/integration/src/azure/AzureIntegration.test.ts b/packages/integration/src/azure/AzureIntegration.test.ts index c30283f432..e6a903814a 100644 --- a/packages/integration/src/azure/AzureIntegration.test.ts +++ b/packages/integration/src/azure/AzureIntegration.test.ts @@ -25,7 +25,7 @@ describe('AzureIntegration', () => { azure: [ { host: 'h.com', - token: 'token', + credentials: [{ personalAccessToken: 'token' }], }, ], }, diff --git a/packages/integration/src/azure/config.test.ts b/packages/integration/src/azure/config.test.ts index c1ddf1590f..cd2d916837 100644 --- a/packages/integration/src/azure/config.test.ts +++ b/packages/integration/src/azure/config.test.ts @@ -276,50 +276,6 @@ describe('readAzureIntegrationConfig', () => { expect(output).toEqual({ host: 'dev.azure.com' }); }); - it('maps deprecated token to credentials', () => { - const output = readAzureIntegrationConfig( - buildConfig({ - host: 'dev.azure.com', - token: 't', - }), - ); - - expect(output).toEqual({ - host: 'dev.azure.com', - credentials: [ - { - kind: 'PersonalAccessToken', - personalAccessToken: 't', - }, - ], - }); - }); - - it('maps deprecated credential to credentials', () => { - const output = readAzureIntegrationConfig( - buildConfig({ - host: 'dev.azure.com', - credential: { - clientId: 'id', - clientSecret: 'secret', - tenantId: 'tenantId', - }, - }), - ); - - expect(output).toEqual({ - host: 'dev.azure.com', - credentials: [ - { - kind: 'ClientSecret', - clientId: 'id', - clientSecret: 'secret', - tenantId: 'tenantId', - }, - ], - }); - }); - it('rejects config when host is not valid', () => { expect(() => readAzureIntegrationConfig(buildConfig({ ...valid, host: 7 })), diff --git a/packages/integration/src/azure/config.ts b/packages/integration/src/azure/config.ts index 09b97755ec..3d6244d69a 100644 --- a/packages/integration/src/azure/config.ts +++ b/packages/integration/src/azure/config.ts @@ -32,24 +32,6 @@ export type AzureIntegrationConfig = { */ host: string; - /** - * The authorization token to use for requests. - * - * If no token is specified, anonymous access is used. - * - * @deprecated Use `credentials` instead. - */ - token?: string; - - /** - * The credential to use for requests. - * - * If no credential is specified anonymous access is used. - * - * @deprecated Use `credentials` instead. - */ - credential?: AzureDevOpsCredential; - /** * The credentials to use for requests. If multiple credentials are specified the first one that matches the organization is used. * If not organization matches the first credential without an organization is used. @@ -236,9 +218,17 @@ function asAzureDevOpsCredential( export function readAzureIntegrationConfig( config: Config, ): AzureIntegrationConfig { + deprecatedConfigCheck(config); + const host = config.getOptionalString('host') ?? AZURE_HOST; - let credentialConfigs = config + if (!isValidHost(host)) { + throw new Error( + `Invalid Azure integration config, '${host}' is not a valid host`, + ); + } + + const credentialConfigs = config .getOptionalConfigArray('credentials') ?.map(credential => { const result: Partial = { @@ -257,54 +247,6 @@ export function readAzureIntegrationConfig( return result; }); - const token = config.getOptionalString('token')?.trim(); - - if ( - config.getOptional('credential') !== undefined && - config.getOptional('credentials') !== undefined - ) { - throw new Error( - `Invalid Azure integration config, 'credential' and 'credentials' cannot be used together. Use 'credentials' instead.`, - ); - } - - if ( - config.getOptional('token') !== undefined && - config.getOptional('credentials') !== undefined - ) { - throw new Error( - `Invalid Azure integration config, 'token' and 'credentials' cannot be used together. Use 'credentials' instead.`, - ); - } - - if (token !== undefined) { - const mapped = [{ personalAccessToken: token }]; - credentialConfigs = credentialConfigs?.concat(mapped) ?? mapped; - } - - if (config.getOptional('credential') !== undefined) { - const mapped = [ - { - organizations: config.getOptionalStringArray( - 'credential.organizations', - ), - token: config.getOptionalString('credential.token')?.trim(), - tenantId: config.getOptionalString('credential.tenantId'), - clientId: config.getOptionalString('credential.clientId'), - clientSecret: config - .getOptionalString('credential.clientSecret') - ?.trim(), - }, - ]; - credentialConfigs = credentialConfigs?.concat(mapped) ?? mapped; - } - - if (!isValidHost(host)) { - throw new Error( - `Invalid Azure integration config, '${host}' is not a valid host`, - ); - } - let credentials: AzureDevOpsCredential[] | undefined = undefined; if (credentialConfigs !== undefined) { const errors = credentialConfigs @@ -415,3 +357,19 @@ export function readAzureIntegrationConfigs( return result; } + +/** + * These config sections have been removed but to ensure they + * don't leak sensitive tokens we have this check in place + * to throw an error if found + * + * @internal + * @deprecated To be removed at a later date + */ +function deprecatedConfigCheck(config: Config) { + if (config.getOptional('credential') || config.getOptional('token')) { + throw new Error( + `Invalid Azure integration config, 'credential' and 'token' have been removed. Use 'credentials' instead.`, + ); + } +} diff --git a/packages/integration/src/azure/deprecated.test.ts b/packages/integration/src/azure/deprecated.test.ts deleted file mode 100644 index 4411ef5f2f..0000000000 --- a/packages/integration/src/azure/deprecated.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { getAzureRequestOptions } from './deprecated'; -import { DateTime } from 'luxon'; -import { - AccessToken, - ClientSecretCredential, - ManagedIdentityCredential, -} from '@azure/identity'; - -jest.mock('@azure/identity'); - -const MockedClientSecretCredential = ClientSecretCredential as jest.MockedClass< - typeof ClientSecretCredential ->; - -const MockedManagedIdentityCredential = - ManagedIdentityCredential as jest.MockedClass< - typeof ManagedIdentityCredential - >; - -describe('azure core', () => { - beforeEach(() => { - jest.resetAllMocks(); - - MockedClientSecretCredential.prototype.getToken.mockImplementation(() => - Promise.resolve({ - expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), - token: 'fake-client-secret-token', - } as AccessToken), - ); - MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() => - Promise.resolve({ - expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), - token: 'fake-managed-identity-token', - } as AccessToken), - ); - }); - - describe('getAzureRequestOptions', () => { - it('should not add authorization header when not using token or credential', async () => { - expect(await getAzureRequestOptions({ host: '' })).toEqual( - expect.objectContaining({ - headers: expect.not.objectContaining({ - Authorization: expect.anything(), - }), - }), - ); - }); - - it('should add authorization header when using a personal access token', async () => { - const pat = '0123456789'; - const encoded = Buffer.from(`:${pat}`).toString('base64'); - expect( - await getAzureRequestOptions({ - host: '', - credentials: [ - { - kind: 'PersonalAccessToken', - personalAccessToken: pat, - }, - ], - }), - ).toEqual( - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: `Basic ${encoded}`, - }), - }), - ); - }); - - it('should add authorization header when using a client secret', async () => { - expect( - await getAzureRequestOptions({ - host: '', - credentials: [ - { - kind: 'ClientSecret', - clientId: 'fake-id', - clientSecret: 'fake-secret', - tenantId: 'fake-tenant', - }, - ], - }), - ).toEqual( - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: 'Bearer fake-client-secret-token', - }), - }), - ); - }); - - it('should add authorization header when using a managed identity', async () => { - expect( - await getAzureRequestOptions({ - host: '', - credentials: [ - { - kind: 'ManagedIdentity', - clientId: 'fake-id', - }, - ], - }), - ).toEqual( - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: 'Bearer fake-managed-identity-token', - }), - }), - ); - }); - }); -}); diff --git a/packages/integration/src/azure/deprecated.ts b/packages/integration/src/azure/deprecated.ts deleted file mode 100644 index fd19f3dc14..0000000000 --- a/packages/integration/src/azure/deprecated.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { AzureIntegrationConfig } from './config'; -import { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider'; - -/** - * Gets the request options necessary to make requests to a given provider. - * - * @param config - The relevant provider config - * @param additionalHeaders - Additional headers for the request - * @public - * @deprecated Use {@link AzureDevOpsCredentialsProvider} instead. - */ -export async function getAzureRequestOptions( - config: AzureIntegrationConfig, - additionalHeaders?: Record, -): Promise<{ headers: Record }> { - const headers: Record = additionalHeaders - ? { ...additionalHeaders } - : {}; - - /* - * Since we do not have a way to determine which organization the request is for, - * we will use the first credential that does not have an organization specified. - */ - const credentialConfig = config.credentials?.filter( - credential => - credential.organizations === undefined || - credential.organizations.length === 0, - )[0]; - - if (credentialConfig) { - const credentialsProvider = - CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential( - credentialConfig, - ); - const credentials = await credentialsProvider.getCredentials(); - - return { - headers: { - ...credentials?.headers, - ...headers, - }, - }; - } - - return { headers }; -} diff --git a/packages/integration/src/azure/index.ts b/packages/integration/src/azure/index.ts index a447c8ffd8..b04ead862f 100644 --- a/packages/integration/src/azure/index.ts +++ b/packages/integration/src/azure/index.ts @@ -38,5 +38,3 @@ export { export * from './types'; export { DefaultAzureDevOpsCredentialsProvider } from './DefaultAzureDevOpsCredentialsProvider'; - -export * from './deprecated'; diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.test.ts b/packages/integration/src/bitbucket/BitbucketIntegration.test.ts deleted file mode 100644 index 575e1c9b6f..0000000000 --- a/packages/integration/src/bitbucket/BitbucketIntegration.test.ts +++ /dev/null @@ -1,114 +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 { ConfigReader } from '@backstage/config'; -import { BitbucketIntegration } from './BitbucketIntegration'; - -describe('BitbucketIntegration', () => { - describe('factory', () => { - it('works', () => { - const integrations = BitbucketIntegration.factory({ - config: new ConfigReader({ - integrations: { - bitbucket: [ - { - host: 'h.com', - apiBaseUrl: 'a', - token: 't', - username: 'u', - appPassword: 'p', - }, - ], - }, - }), - }); - expect(integrations.list().length).toBe(2); // including default - expect(integrations.list()[0].config.host).toBe('h.com'); - expect(integrations.list()[1].config.host).toBe('bitbucket.org'); - }); - - it('falls back to bitbucketCloud+bitbucketServer', () => { - const integrations = BitbucketIntegration.factory({ - config: new ConfigReader({ - integrations: { - bitbucketCloud: [ - { - username: 'u', - appPassword: 'p', - }, - ], - bitbucketServer: [ - { - host: 'h.com', - apiBaseUrl: 'a', - token: 't', - }, - ], - }, - }), - }); - expect(integrations.list().length).toBe(2); // including default - expect(integrations.list()[0].config.host).toBe('bitbucket.org'); - expect(integrations.list()[1].config.host).toBe('h.com'); - }); - }); - - it('returns the basics', () => { - const integration = new BitbucketIntegration({ host: 'h.com' } as any); - expect(integration.type).toBe('bitbucket'); - expect(integration.title).toBe('h.com'); - }); - - it('resolves url line number correctly for Bitbucket Cloud', () => { - const integration = new BitbucketIntegration({ - host: 'bitbucket.org', - } as any); - - expect( - integration.resolveUrl({ - url: './a.yaml', - base: 'https://bitbucket.org/my-owner/my-project/src/master/README.md', - lineNumber: 14, - }), - ).toBe( - 'https://bitbucket.org/my-owner/my-project/src/master/a.yaml#lines-14', - ); - }); - - it('resolves url line number correctly for Bitbucket Server', () => { - const integration = new BitbucketIntegration({ host: 'h.com' } as any); - - expect( - integration.resolveUrl({ - url: './a.yaml', - base: 'https://bitbucket.org/my-owner/my-project/src/master/README.md', - lineNumber: 14, - }), - ).toBe('https://bitbucket.org/my-owner/my-project/src/master/a.yaml#14'); - }); - - it('resolve edit URL', () => { - const integration = new BitbucketIntegration({ host: 'h.com' } as any); - - expect( - integration.resolveEditUrl( - 'https://bitbucket.org/my-owner/my-project/src/master/README.md', - ), - ).toBe( - 'https://bitbucket.org/my-owner/my-project/src/master/README.md?mode=edit&spa=0&at=master', - ); - }); -}); diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.ts b/packages/integration/src/bitbucket/BitbucketIntegration.ts deleted file mode 100644 index d92532b54a..0000000000 --- a/packages/integration/src/bitbucket/BitbucketIntegration.ts +++ /dev/null @@ -1,99 +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 parseGitUrl from 'git-url-parse'; -import { basicIntegrations, defaultScmResolveUrl } from '../helpers'; -import { ScmIntegration, ScmIntegrationsFactory } from '../types'; -import { - BitbucketIntegrationConfig, - readBitbucketIntegrationConfigs, -} from './config'; - -/** - * A Bitbucket based integration. - * - * @public - * @deprecated replaced by the integrations bitbucketCloud and bitbucketServer. - */ -export class BitbucketIntegration implements ScmIntegration { - static factory: ScmIntegrationsFactory = ({ - config, - }) => { - const configs = readBitbucketIntegrationConfigs( - config.getOptionalConfigArray('integrations.bitbucket') ?? [ - // if integrations.bitbucket was not used assume the use was migrated to the new configs - // and backport for the deprecated integration to be usable for other parts of the system - // until these got migrated - ...(config.getOptionalConfigArray('integrations.bitbucketCloud') ?? []), - ...(config.getOptionalConfigArray('integrations.bitbucketServer') ?? - []), - ], - ); - return basicIntegrations( - configs.map(c => new BitbucketIntegration(c)), - i => i.config.host, - ); - }; - - constructor(private readonly integrationConfig: BitbucketIntegrationConfig) {} - - get type(): string { - return 'bitbucket'; - } - - get title(): string { - return this.integrationConfig.host; - } - - get config(): BitbucketIntegrationConfig { - return this.integrationConfig; - } - - resolveUrl(options: { - url: string; - base: string; - lineNumber?: number; - }): string { - const resolved = defaultScmResolveUrl(options); - if (!options.lineNumber) { - return resolved; - } - - const url = new URL(resolved); - - if (this.integrationConfig.host === 'bitbucket.org') { - // Bitbucket Cloud uses the syntax #lines-{start}[:{end}][,...] - url.hash = `lines-${options.lineNumber}`; - } else { - // Bitbucket Server uses the syntax #{start}[-{end}][,...] - url.hash = `${options.lineNumber}`; - } - - return url.toString(); - } - - resolveEditUrl(url: string): string { - const urlData = parseGitUrl(url); - const editUrl = new URL(url); - - editUrl.searchParams.set('mode', 'edit'); - // TODO: Not sure what spa=0 does, at least bitbucket.org doesn't support it - // but this is taken over from the initial implementation. - editUrl.searchParams.set('spa', '0'); - editUrl.searchParams.set('at', urlData.ref); - return editUrl.toString(); - } -} diff --git a/packages/integration/src/bitbucket/config.test.ts b/packages/integration/src/bitbucket/config.test.ts deleted file mode 100644 index 8dd20cade7..0000000000 --- a/packages/integration/src/bitbucket/config.test.ts +++ /dev/null @@ -1,175 +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 { Config, ConfigReader } from '@backstage/config'; -import { loadConfigSchema } from '@backstage/config-loader'; -import { - BitbucketIntegrationConfig, - readBitbucketIntegrationConfig, - readBitbucketIntegrationConfigs, -} from './config'; - -describe('readBitbucketIntegrationConfig', () => { - function buildConfig(data: Partial): Config { - return new ConfigReader(data); - } - - async function buildFrontendConfig( - data: Partial, - ): Promise { - const fullSchema = await loadConfigSchema({ - dependencies: ['@backstage/integration'], - }); - const serializedSchema = fullSchema.serialize() as { - schemas: { value: { properties?: { integrations?: object } } }[]; - }; - const schema = await loadConfigSchema({ - serialized: { - ...serializedSchema, // only include schemas that apply to integrations - schemas: serializedSchema.schemas.filter( - s => s.value?.properties?.integrations, - ), - }, - }); - const processed = schema.process( - [{ data: { integrations: { bitbucket: [data] } }, context: 'app' }], - { visibility: ['frontend'] }, - ); - return new ConfigReader( - (processed[0].data as any).integrations.bitbucket[0], - ); - } - - it('reads all values', () => { - const output = readBitbucketIntegrationConfig( - buildConfig({ - host: 'a.com', - apiBaseUrl: 'https://a.com/api', - token: 't\n\n\n', - username: 'u', - appPassword: '\n\n\np', - }), - ); - expect(output).toEqual({ - host: 'a.com', - apiBaseUrl: 'https://a.com/api', - token: 't', - username: 'u', - appPassword: 'p', - }); - }); - - it('inserts the defaults if missing', () => { - const output = readBitbucketIntegrationConfig(buildConfig({})); - expect(output).toEqual( - expect.objectContaining({ - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', - }), - ); - }); - - it('rejects funky configs', () => { - const valid: any = { - host: 'a.com', - apiBaseUrl: 'https://a.com/api', - token: 't', - username: 'u', - appPassword: 'p', - }; - expect(() => - readBitbucketIntegrationConfig(buildConfig({ ...valid, host: 7 })), - ).toThrow(/host/); - expect(() => - readBitbucketIntegrationConfig(buildConfig({ ...valid, apiBaseUrl: 7 })), - ).toThrow(/apiBaseUrl/); - expect(() => - readBitbucketIntegrationConfig(buildConfig({ ...valid, token: 7 })), - ).toThrow(/token/); - expect(() => - readBitbucketIntegrationConfig(buildConfig({ ...valid, username: 7 })), - ).toThrow(/username/); - expect(() => - readBitbucketIntegrationConfig(buildConfig({ ...valid, appPassword: 7 })), - ).toThrow(/appPassword/); - }); - - it('works on the frontend', async () => { - expect( - readBitbucketIntegrationConfig( - await buildFrontendConfig({ - host: 'a.com', - apiBaseUrl: 'https://a.com/api', - token: 't', - username: 'u', - appPassword: 'p', - }), - ), - ).toEqual({ - host: 'a.com', - apiBaseUrl: 'https://a.com/api', - }); - }); -}); - -describe('readBitbucketIntegrationConfigs', () => { - function buildConfig(data: Partial[]): Config[] { - return data.map(item => new ConfigReader(item)); - } - - it('reads all values', () => { - const output = readBitbucketIntegrationConfigs( - buildConfig([ - { - host: 'a.com', - apiBaseUrl: 'https://a.com/api', - token: 't', - username: 'u', - appPassword: 'p', - }, - ]), - ); - expect(output).toContainEqual({ - host: 'a.com', - apiBaseUrl: 'https://a.com/api', - token: 't', - username: 'u', - appPassword: 'p', - }); - }); - - it('adds a default Bitbucket Cloud entry when missing', () => { - const output = readBitbucketIntegrationConfigs(buildConfig([])); - expect(output).toEqual([ - { - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', - }, - ]); - }); - - it('injects the correct Bitbucket Cloud API base URL when missing', () => { - const output = readBitbucketIntegrationConfigs( - buildConfig([{ host: 'bitbucket.org' }]), - ); - expect(output).toEqual([ - { - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', - }, - ]); - }); -}); diff --git a/packages/integration/src/bitbucket/config.ts b/packages/integration/src/bitbucket/config.ts deleted file mode 100644 index 6939034e7d..0000000000 --- a/packages/integration/src/bitbucket/config.ts +++ /dev/null @@ -1,136 +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 { Config } from '@backstage/config'; -import { trimEnd } from 'lodash'; -import { isValidHost } from '../helpers'; - -const BITBUCKET_HOST = 'bitbucket.org'; -const BITBUCKET_API_BASE_URL = 'https://api.bitbucket.org/2.0'; - -/** - * The configuration parameters for a single Bitbucket API provider. - * - * @public - * @deprecated bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. - */ -export type BitbucketIntegrationConfig = { - /** - * The host of the target that this matches on, e.g. "bitbucket.org" - */ - host: string; - - /** - * The base URL of the API of this provider, e.g. "https://api.bitbucket.org/2.0", - * with no trailing slash. - * - * Values omitted at the optional property at the app-config will be deduced - * from the "host" value. - */ - apiBaseUrl: string; - - /** - * The authorization token to use for requests to a Bitbucket Server provider. - * - * See https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html - * - * If no token is specified, anonymous access is used. - */ - token?: string; - - /** - * The username to use for requests to Bitbucket Cloud (bitbucket.org). - */ - username?: string; - - /** - * Authentication with Bitbucket Cloud (bitbucket.org) is done using app passwords. - * - * See https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/ - */ - appPassword?: string; - - /** - * Signing key for commits - */ - commitSigningKey?: string; -}; - -/** - * Reads a single Bitbucket integration config. - * - * @param config - The config object of a single integration - * @public - * @deprecated bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. - */ -export function readBitbucketIntegrationConfig( - config: Config, -): BitbucketIntegrationConfig { - const host = config.getOptionalString('host') ?? BITBUCKET_HOST; - let apiBaseUrl = config.getOptionalString('apiBaseUrl'); - const token = config.getOptionalString('token')?.trim(); - const username = config.getOptionalString('username'); - const appPassword = config.getOptionalString('appPassword')?.trim(); - - if (!isValidHost(host)) { - throw new Error( - `Invalid Bitbucket integration config, '${host}' is not a valid host`, - ); - } - - if (apiBaseUrl) { - apiBaseUrl = trimEnd(apiBaseUrl, '/'); - } else if (host === BITBUCKET_HOST) { - apiBaseUrl = BITBUCKET_API_BASE_URL; - } else { - apiBaseUrl = `https://${host}/rest/api/1.0`; - } - - return { - host, - apiBaseUrl, - token, - username, - appPassword, - commitSigningKey: config.getOptionalString('commitSigningKey'), - }; -} - -/** - * Reads a set of Bitbucket integration configs, and inserts some defaults for - * public Bitbucket if not specified. - * - * @param configs - All of the integration config objects - * @public - * @deprecated bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. - */ -export function readBitbucketIntegrationConfigs( - configs: Config[], -): BitbucketIntegrationConfig[] { - // First read all the explicit integrations - const result = configs.map(readBitbucketIntegrationConfig); - - // If no explicit bitbucket.org integration was added, put one in the list as - // a convenience - if (!result.some(c => c.host === BITBUCKET_HOST)) { - result.push({ - host: BITBUCKET_HOST, - apiBaseUrl: BITBUCKET_API_BASE_URL, - }); - } - - return result; -} diff --git a/packages/integration/src/bitbucket/core.test.ts b/packages/integration/src/bitbucket/core.test.ts deleted file mode 100644 index f1b4e04f15..0000000000 --- a/packages/integration/src/bitbucket/core.test.ts +++ /dev/null @@ -1,304 +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 { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import { registerMswTestHooks } from '../helpers'; -import { BitbucketIntegrationConfig } from './config'; -import { - getBitbucketDefaultBranch, - getBitbucketDownloadUrl, - getBitbucketFileFetchUrl, - getBitbucketRequestOptions, -} from './core'; - -describe('bitbucket core', () => { - const worker = setupServer(); - registerMswTestHooks(worker); - - describe('getBitbucketRequestOptions', () => { - it('inserts a token when needed', () => { - const withToken: BitbucketIntegrationConfig = { - host: '', - apiBaseUrl: '', - token: 'A', - }; - const withoutToken: BitbucketIntegrationConfig = { - host: '', - apiBaseUrl: '', - }; - expect( - (getBitbucketRequestOptions(withToken).headers as any).Authorization, - ).toEqual('Bearer A'); - expect( - (getBitbucketRequestOptions(withoutToken).headers as any).Authorization, - ).toBeUndefined(); - }); - - it('insert basic auth when needed', () => { - const withUsernameAndPassword: BitbucketIntegrationConfig = { - host: '', - apiBaseUrl: '', - username: 'some-user', - appPassword: 'my-secret', - }; - const withoutUsernameAndPassword: BitbucketIntegrationConfig = { - host: '', - apiBaseUrl: '', - }; - expect( - (getBitbucketRequestOptions(withUsernameAndPassword).headers as any) - .Authorization, - ).toEqual('Basic c29tZS11c2VyOm15LXNlY3JldA=='); - expect( - (getBitbucketRequestOptions(withoutUsernameAndPassword).headers as any) - .Authorization, - ).toBeUndefined(); - }); - }); - - describe('getBitbucketFileFetchUrl', () => { - it('rejects targets that do not look like URLs', () => { - const config: BitbucketIntegrationConfig = { host: '', apiBaseUrl: '' }; - expect(() => getBitbucketFileFetchUrl('a/b', config)).toThrow( - /Incorrect URL: a\/b/, - ); - }); - - it('happy path for Bitbucket Cloud', () => { - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', - }; - expect( - getBitbucketFileFetchUrl( - 'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml', - config, - ), - ).toEqual( - 'https://api.bitbucket.org/2.0/repositories/org-name/repo-name/src/master/templates/my-template.yaml', - ); - }); - - it('happy path for Bitbucket Server', () => { - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://bitbucket.mycompany.net/rest/api/1.0', - }; - expect( - getBitbucketFileFetchUrl( - 'https://bitbucket.mycompany.net/projects/a/repos/b/browse/path/to/c.yaml', - config, - ), - ).toEqual( - 'https://bitbucket.mycompany.net/rest/api/1.0/projects/a/repos/b/raw/path/to/c.yaml?at=', - ); - }); - }); - - describe('getBitbucketDownloadUrl', () => { - it('add path param if a path is specified for Bitbucket Server', async () => { - const defaultBranchResponse = { - displayId: 'main', - }; - worker.use( - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json(defaultBranchResponse), - ), - ), - ); - - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', - }; - const result = await getBitbucketDownloadUrl( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs', - config, - ); - expect(result).toEqual( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=main&prefix=backstage-mock&path=docs', - ); - }); - - it('does not double encode the filepath', async () => { - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', - }; - const result = await getBitbucketDownloadUrl( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/%2Fdocs?at=some-branch', - config, - ); - expect(result).toEqual( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=some-branch&prefix=backstage-mock&path=%2Fdocs', - ); - }); - - it('do not add path param if no path is specified for Bitbucket Server', async () => { - const defaultBranchResponse = { - displayId: 'main', - }; - worker.use( - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json(defaultBranchResponse), - ), - ), - ); - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', - }; - const result = await getBitbucketDownloadUrl( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse', - config, - ); - - expect(result).toEqual( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=main&prefix=backstage-mock', - ); - }); - - it('get by branch for Bitbucket Server', async () => { - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', - }; - const result = await getBitbucketDownloadUrl( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch', - config, - ); - expect(result).toEqual( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=tgz&at=some-branch&prefix=backstage-mock&path=docs', - ); - }); - - it('do not add path param for Bitbucket Cloud', async () => { - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', - }; - const result = await getBitbucketDownloadUrl( - 'https://bitbucket.org/backstage/mock/src/master', - config, - ); - expect(result).toEqual( - 'https://bitbucket.org/backstage/mock/get/master.tar.gz', - ); - }); - }); - - describe('getBitbucketDefaultBranch', () => { - it('return default branch for Bitbucket Cloud', async () => { - const repoInfoResponse = { - mainbranch: { - name: 'main', - }, - }; - worker.use( - rest.get( - 'https://api.bitbucket.org/2.0/repositories/backstage/mock', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json(repoInfoResponse), - ), - ), - ); - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', - }; - const defaultBranch = await getBitbucketDefaultBranch( - 'https://bitbucket.org/backstage/mock/src/main', - config, - ); - expect(defaultBranch).toEqual('main'); - }); - - it('return default branch for Bitbucket Server', async () => { - const defaultBranchResponse = { - displayId: 'main', - }; - worker.use( - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json(defaultBranchResponse), - ), - ), - ); - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', - }; - const defaultBranch = await getBitbucketDefaultBranch( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/README.md', - config, - ); - expect(defaultBranch).toEqual('main'); - }); - - it('return default branch for Bitbucket Server for bitbucket version 5.11', async () => { - const defaultBranchResponse = { - displayId: 'main', - }; - worker.use( - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch', - (_, res, ctx) => - res( - ctx.status(404), - ctx.set('Content-Type', 'application/json'), - ctx.json(defaultBranchResponse), - ), - ), - rest.get( - 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json(defaultBranchResponse), - ), - ), - ); - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', - }; - const defaultBranch = await getBitbucketDefaultBranch( - 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/README.md', - config, - ); - expect(defaultBranch).toEqual('main'); - }); - }); -}); diff --git a/packages/integration/src/bitbucket/core.ts b/packages/integration/src/bitbucket/core.ts deleted file mode 100644 index 5c5533155d..0000000000 --- a/packages/integration/src/bitbucket/core.ts +++ /dev/null @@ -1,183 +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 fetch from 'cross-fetch'; -import parseGitUrl from 'git-url-parse'; -import { BitbucketIntegrationConfig } from './config'; - -/** - * Given a URL pointing to a path on a provider, returns the default branch. - * - * @param url - A URL pointing to a path - * @param config - The relevant provider config - * @public - * @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. - */ -export async function getBitbucketDefaultBranch( - url: string, - config: BitbucketIntegrationConfig, -): Promise { - const { name: repoName, owner: project, resource } = parseGitUrl(url); - - const isHosted = resource === 'bitbucket.org'; - // Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp184 - let branchUrl = isHosted - ? `${config.apiBaseUrl}/repositories/${project}/${repoName}` - : `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/default-branch`; - - let response = await fetch(branchUrl, getBitbucketRequestOptions(config)); - - if (response.status === 404 && !isHosted) { - // First try the new format, and then if it gets specifically a 404 it should try the old format - // (to support old Atlassian Bitbucket v5.11.1 format ) - branchUrl = `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/branches/default`; - response = await fetch(branchUrl, getBitbucketRequestOptions(config)); - } - - if (!response.ok) { - const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`; - throw new Error(message); - } - - let defaultBranch; - if (isHosted) { - const repoInfo = await response.json(); - defaultBranch = repoInfo.mainbranch.name; - } else { - const { displayId } = await response.json(); - defaultBranch = displayId; - } - if (!defaultBranch) { - throw new Error( - `Failed to read default branch from ${branchUrl}. ` + - `Response ${response.status} ${response.json()}`, - ); - } - return defaultBranch; -} - -/** - * Given a URL pointing to a path on a provider, returns a URL that is suitable - * for downloading the subtree. - * - * @param url - A URL pointing to a path - * @param config - The relevant provider config - * @public - * @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. - */ -export async function getBitbucketDownloadUrl( - url: string, - config: BitbucketIntegrationConfig, -): Promise { - const { - name: repoName, - owner: project, - ref, - protocol, - resource, - filepath, - } = parseGitUrl(url); - - const isHosted = resource === 'bitbucket.org'; - - let branch = ref; - if (!branch) { - branch = await getBitbucketDefaultBranch(url, config); - } - // path will limit the downloaded content - // /docs will only download the docs folder and everything below it - // /docs/index.md will download the docs folder and everything below it - const path = filepath - ? `&path=${encodeURIComponent(decodeURIComponent(filepath))}` - : ''; - const archiveUrl = isHosted - ? `${protocol}://${resource}/${project}/${repoName}/get/${branch}.tar.gz` - : `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/archive?format=tgz&at=${branch}&prefix=${project}-${repoName}${path}`; - - return archiveUrl; -} - -/** - * Given a URL pointing to a file on a provider, returns a URL that is suitable - * for fetching the contents of the data. - * - * @remarks - * - * Converts - * from: https://bitbucket.org/orgname/reponame/src/master/file.yaml - * to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml - * - * @param url - A URL pointing to a file - * @param config - The relevant provider config - * @public - * @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. - */ -export function getBitbucketFileFetchUrl( - url: string, - config: BitbucketIntegrationConfig, -): string { - try { - const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url); - if ( - !owner || - !name || - (filepathtype !== 'browse' && - filepathtype !== 'raw' && - filepathtype !== 'src') - ) { - throw new Error('Invalid Bitbucket URL or file path'); - } - - const pathWithoutSlash = filepath.replace(/^\//, ''); - - if (config.host === 'bitbucket.org') { - if (!ref) { - throw new Error('Invalid Bitbucket URL or file path'); - } - return `${config.apiBaseUrl}/repositories/${owner}/${name}/src/${ref}/${pathWithoutSlash}`; - } - return `${config.apiBaseUrl}/projects/${owner}/repos/${name}/raw/${pathWithoutSlash}?at=${ref}`; - } catch (e) { - throw new Error(`Incorrect URL: ${url}, ${e}`); - } -} - -/** - * Gets the request options necessary to make requests to a given provider. - * - * @param config - The relevant provider config - * @public - * @deprecated no longer in use, bitbucket integration replaced by integrations bitbucketCloud and bitbucketServer. - */ -export function getBitbucketRequestOptions( - config: BitbucketIntegrationConfig, -): { headers: Record } { - const headers: Record = {}; - - if (config.token) { - headers.Authorization = `Bearer ${config.token}`; - } else if (config.username && config.appPassword) { - const buffer = Buffer.from( - `${config.username}:${config.appPassword}`, - 'utf8', - ); - headers.Authorization = `Basic ${buffer.toString('base64')}`; - } - - return { - headers, - }; -} diff --git a/packages/integration/src/bitbucketCloud/core.ts b/packages/integration/src/bitbucketCloud/core.ts index 9beb3bd748..b1f95ad2d0 100644 --- a/packages/integration/src/bitbucketCloud/core.ts +++ b/packages/integration/src/bitbucketCloud/core.ts @@ -16,6 +16,7 @@ import fetch from 'cross-fetch'; import parseGitUrl from 'git-url-parse'; +import { parseGitUrlSafe } from '../helpers'; import { BitbucketCloudIntegrationConfig } from './config'; import { DateTime } from 'luxon'; @@ -165,7 +166,7 @@ export async function getBitbucketCloudDownloadUrl( ref, protocol, resource, - } = parseGitUrl(url); + } = parseGitUrlSafe(url); let branch = ref; if (!branch) { @@ -193,7 +194,7 @@ export function getBitbucketCloudFileFetchUrl( config: BitbucketCloudIntegrationConfig, ): string { try { - const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url); + const { owner, name, ref, filepathtype, filepath } = parseGitUrlSafe(url); if (!owner || !name || (filepathtype !== 'src' && filepathtype !== 'raw')) { throw new Error('Invalid Bitbucket Cloud URL or file path'); } diff --git a/packages/integration/src/bitbucketServer/core.ts b/packages/integration/src/bitbucketServer/core.ts index 8741b8f0e7..5d042d5f02 100644 --- a/packages/integration/src/bitbucketServer/core.ts +++ b/packages/integration/src/bitbucketServer/core.ts @@ -16,6 +16,7 @@ import fetch from 'cross-fetch'; import parseGitUrl from 'git-url-parse'; +import { parseGitUrlSafe } from '../helpers'; import { BitbucketServerIntegrationConfig } from './config'; /** @@ -74,7 +75,12 @@ export async function getBitbucketServerDownloadUrl( url: string, config: BitbucketServerIntegrationConfig, ): Promise { - const { name: repoName, owner: project, ref, filepath } = parseGitUrl(url); + const { + name: repoName, + owner: project, + ref, + filepath, + } = parseGitUrlSafe(url); let branch = ref; if (!branch) { @@ -108,7 +114,7 @@ export function getBitbucketServerFileFetchUrl( config: BitbucketServerIntegrationConfig, ): string { try { - const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url); + const { owner, name, ref, filepathtype, filepath } = parseGitUrlSafe(url); if ( !owner || !name || diff --git a/packages/integration/src/gerrit/core.test.ts b/packages/integration/src/gerrit/core.test.ts index 5899196f65..b22b5f2caf 100644 --- a/packages/integration/src/gerrit/core.test.ts +++ b/packages/integration/src/gerrit/core.test.ts @@ -20,7 +20,6 @@ import fetch from 'cross-fetch'; import { registerMswTestHooks } from '../helpers'; import { GerritIntegrationConfig } from './config'; import { - buildGerritGitilesArchiveUrl, buildGerritGitilesArchiveUrlFromLocation, buildGerritGitilesUrl, getGerritBranchApiUrl, @@ -28,7 +27,6 @@ import { getGerritRequestOptions, parseGerritJsonResponse, parseGitilesUrlRef, - parseGerritGitilesUrl, getGerritFileContentsApiUrl, } from './core'; @@ -36,86 +34,6 @@ describe('gerrit core', () => { const worker = setupServer(); registerMswTestHooks(worker); - describe('buildGerritGitilesArchiveUrl', () => { - const config: GerritIntegrationConfig = { - host: 'gerrit.com', - baseUrl: 'https://gerrit.com', - gitilesBaseUrl: 'https://gerrit.com/gitiles', - }; - const configWithPath: GerritIntegrationConfig = { - host: 'gerrit.com', - baseUrl: 'https://gerrit.com/gerrit', - gitilesBaseUrl: 'https://gerrit.com/gerrit/plugins/gitiles', - }; - const configWithDedicatedGitiles: GerritIntegrationConfig = { - host: 'gerrit.com', - baseUrl: 'https://gerrit.com/gerrit', - gitilesBaseUrl: 'https://dedicated-gitiles-server.com/gerrit/gitiles', - }; - it('can create an archive url for a branch', () => { - expect(buildGerritGitilesArchiveUrl(config, 'repo', 'dev', '')).toEqual( - 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz', - ); - - expect(buildGerritGitilesArchiveUrl(config, 'repo', 'dev', '/')).toEqual( - 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz', - ); - }); - it('can create an archive url for a specific directory', () => { - expect( - buildGerritGitilesArchiveUrl(config, 'repo', 'dev', 'docs'), - ).toEqual( - 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', - ); - }); - it('can create an authenticated url when auth is enabled', () => { - const authConfig = { - ...config, - username: 'username', - password: 'password', - }; - expect( - buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'), - ).toEqual( - 'https://gerrit.com/a/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', - ); - }); - it('can create an authenticated url when auth is enabled and an url-path is used', () => { - const authConfig = { - ...configWithPath, - username: 'username', - password: 'password', - }; - expect( - buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'), - ).toEqual( - 'https://gerrit.com/gerrit/a/plugins/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', - ); - }); - it('Cannot build an authenticated url when a dedicated Gitiles server is used', () => { - const authConfig = { - ...configWithDedicatedGitiles, - username: 'username', - password: 'password', - }; - expect(() => - buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'), - ).toThrow( - 'Since the baseUrl (Gerrit) is not part of the gitilesBaseUrl, an authentication URL could not be constructed.', - ); - }); - it('Build a non-authenticated url when a dedicated Gitiles server is used', () => { - const authConfig = { - ...configWithDedicatedGitiles, - }; - expect( - buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'), - ).toEqual( - 'https://dedicated-gitiles-server.com/gerrit/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', - ); - }); - }); - describe('buildGerritGitilesArchiveUrlFromLocation', () => { const config: GerritIntegrationConfig = { host: 'gerrit.com', @@ -238,6 +156,7 @@ describe('gerrit core', () => { ).toBeUndefined(); }); }); + describe('parseGitilesUrlRef', () => { const config: GerritIntegrationConfig = { host: 'gerrit.com', @@ -360,64 +279,6 @@ describe('gerrit core', () => { }); }); }); - describe('parseGerritGitilesUrl', () => { - it('can parse a valid gitiles urls.', () => { - const config: GerritIntegrationConfig = { - host: 'gerrit.com', - gitilesBaseUrl: 'https://gerrit.com/gitiles', - }; - const { branch, filePath, project } = parseGerritGitilesUrl( - config, - 'https://gerrit.com/gitiles/web/project/+/refs/heads/master/README.md', - ); - expect(project).toEqual('web/project'); - expect(branch).toEqual('master'); - expect(filePath).toEqual('README.md'); - - const { filePath: rootPath } = parseGerritGitilesUrl( - config, - 'https://gerrit.com/gitiles/web/project/+/refs/heads/master', - ); - expect(rootPath).toEqual('/'); - }); - it('can parse a valid authenticated gitiles url.', () => { - const config: GerritIntegrationConfig = { - host: 'gerrit.com', - gitilesBaseUrl: 'https://gerrit.com/gitiles', - }; - const { branch, filePath, project } = parseGerritGitilesUrl( - config, - 'https://gerrit.com/a/gitiles/web/project/+/refs/heads/master/README.md', - ); - expect(project).toEqual('web/project'); - expect(branch).toEqual('master'); - expect(filePath).toEqual('README.md'); - - const { filePath: rootPath } = parseGerritGitilesUrl( - config, - 'https://gerrit.com/gitiles/web/project/+/refs/heads/master', - ); - expect(rootPath).toEqual('/'); - }); - it('throws on incorrect gitiles urls.', () => { - const config: GerritIntegrationConfig = { - host: 'gerrit.com', - gitilesBaseUrl: 'https://gerrit.com', - }; - expect(() => - parseGerritGitilesUrl( - config, - 'https://gerrit.com/+/refs/heads/master/README.md', - ), - ).toThrow(/project/); - expect(() => - parseGerritGitilesUrl( - config, - 'https://gerrit.com/web/project/+/refs/changes/1/11/master/README.md', - ), - ).toThrow(/branch/); - }); - }); describe('getGerritBranchApiUrl', () => { it('can create an url for anonymous access.', () => { @@ -450,6 +311,45 @@ describe('gerrit core', () => { 'https://gerrit.com/a/projects/web%2Fproject/branches/master', ); }); + it('throws when ref type is not a branch (tag).', () => { + const config: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com', + gitilesBaseUrl: 'https://gerrit.com', + }; + expect(() => + getGerritBranchApiUrl( + config, + 'https://gerrit.com/web/project/+/refs/tags/v1.0.0/README.md', + ), + ).toThrow('Unsupported gitiles ref type: tag'); + }); + it('throws when ref type is not a branch (sha).', () => { + const config: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com', + gitilesBaseUrl: 'https://gerrit.com', + }; + expect(() => + getGerritBranchApiUrl( + config, + 'https://gerrit.com/web/project/+/157f862803d45b9d269f0e390f88aece1ded51e8/README.md', + ), + ).toThrow('Unsupported gitiles ref type: sha'); + }); + it('throws when ref type is not a branch (head).', () => { + const config: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com', + gitilesBaseUrl: 'https://gerrit.com', + }; + expect(() => + getGerritBranchApiUrl( + config, + 'https://gerrit.com/web/project/+/HEAD/README.md', + ), + ).toThrow('Unsupported gitiles ref type: head'); + }); }); describe('getGerritCloneRepoUrl', () => { diff --git a/packages/integration/src/gerrit/core.ts b/packages/integration/src/gerrit/core.ts index ae6a5baa10..e8db1305ed 100644 --- a/packages/integration/src/gerrit/core.ts +++ b/packages/integration/src/gerrit/core.ts @@ -18,70 +18,6 @@ import { GerritIntegrationConfig } from './config'; const GERRIT_BODY_PREFIX = ")]}'"; -/** - * Parse a Gitiles URL and return branch, file path and project. - * - * @remarks - * - * Gerrit only handles code reviews so it does not have a native way to browse - * or showing the content of gits. Image if Github only had the "pull requests" - * tab. - * - * Any source code browsing is instead handled by optional services outside - * Gerrit. The url format chosen for the Gerrit url reader is the one used by - * the Gitiles project. Gerrit will work perfectly with Backstage without - * having Gitiles installed but there are some places in the Backstage GUI - * with links to the url used by the url reader. These will not work unless - * the urls point to an actual Gitiles installation. - * - * Gitiles url: - * https://g.com/optional_path/\{project\}/+/refs/heads/\{branch\}/\{filePath\} - * https://g.com/a/optional_path/\{project\}/+/refs/heads/\{branch\}/\{filePath\} - * - * - * @param url - An URL pointing to a file stored in git. - * @public - * @deprecated `parseGerritGitilesUrl` is deprecated. Use - * {@link parseGitilesUrlRef} instead. - */ -export function parseGerritGitilesUrl( - config: GerritIntegrationConfig, - url: string, -): { branch: string; filePath: string; project: string } { - const baseUrlParse = new URL(config.gitilesBaseUrl!); - const urlParse = new URL(url); - - // Remove the gerrit authentication prefix '/a/' from the url - // In case of the gitilesBaseUrl is https://review.gerrit.com/plugins/gitiles - // and the url provided is https://review.gerrit.com/a/plugins/gitiles/... - // remove the prefix only if the pathname start with '/a/' - const urlPath = urlParse.pathname - .substring(urlParse.pathname.startsWith('/a/') ? 2 : 0) - .replace(baseUrlParse.pathname, ''); - - const parts = urlPath.split('/').filter(p => !!p); - - const projectEndIndex = parts.indexOf('+'); - - if (projectEndIndex <= 0) { - throw new Error(`Unable to parse project from url: ${url}`); - } - const project = trimStart(parts.slice(0, projectEndIndex).join('/'), '/'); - - const branchIndex = parts.indexOf('heads'); - if (branchIndex <= 0) { - throw new Error(`Unable to parse branch from url: ${url}`); - } - const branch = parts[branchIndex + 1]; - const filePath = parts.slice(branchIndex + 2).join('/'); - - return { - branch, - filePath: filePath === '' ? '/' : filePath, - project, - }; -} - /** * Parses Gitiles urls and returns the following: * @@ -231,30 +167,6 @@ export function buildGerritEditUrl( )}`; } -/** - * Build a Gerrit Gitiles archive url that targets a specific branch and path - * - * @param config - A Gerrit provider config. - * @param project - The name of the git project - * @param branch - The branch we will target. - * @param filePath - The absolute file path. - * @public - * @deprecated `buildGerritGitilesArchiveUrl` is deprecated. Use - * {@link buildGerritGitilesArchiveUrlFromLocation} instead. - */ -export function buildGerritGitilesArchiveUrl( - config: GerritIntegrationConfig, - project: string, - branch: string, - filePath: string, -): string { - const archiveName = - filePath === '/' || filePath === '' ? '.tar.gz' : `/${filePath}.tar.gz`; - return `${getGitilesAuthenticationUrl( - config, - )}/${project}/+archive/refs/heads/${branch}${archiveName}`; -} - /** * Build a Gerrit Gitiles archive url from a Gitiles url. * @@ -350,11 +262,15 @@ export function getGerritBranchApiUrl( config: GerritIntegrationConfig, url: string, ) { - const { branch, project } = parseGerritGitilesUrl(config, url); + const { ref, refType, project } = parseGitilesUrlRef(config, url); + + if (refType !== 'branch') { + throw new Error(`Unsupported gitiles ref type: ${refType}`); + } return `${config.baseUrl}${getAuthenticationPrefix( config, - )}projects/${encodeURIComponent(project)}/branches/${branch}`; + )}projects/${encodeURIComponent(project)}/branches/${ref}`; } /** @@ -367,7 +283,7 @@ export function getGerritCloneRepoUrl( config: GerritIntegrationConfig, url: string, ) { - const { project } = parseGerritGitilesUrl(config, url); + const { project } = parseGitilesUrlRef(config, url); return `${config.cloneUrl}${getAuthenticationPrefix(config)}${project}`; } diff --git a/packages/integration/src/gerrit/index.ts b/packages/integration/src/gerrit/index.ts index 534315cfba..5af75e36ab 100644 --- a/packages/integration/src/gerrit/index.ts +++ b/packages/integration/src/gerrit/index.ts @@ -19,7 +19,6 @@ export { readGerritIntegrationConfigs, } from './config'; export { - buildGerritGitilesArchiveUrl, buildGerritGitilesArchiveUrlFromLocation, getGitilesAuthenticationUrl, getGerritBranchApiUrl, @@ -28,7 +27,6 @@ export { getGerritProjectsApiUrl, getGerritRequestOptions, parseGerritJsonResponse, - parseGerritGitilesUrl, parseGitilesUrlRef, } from './core'; diff --git a/packages/integration/src/github/core.test.ts b/packages/integration/src/github/core.test.ts index 982a096b43..d129f57838 100644 --- a/packages/integration/src/github/core.test.ts +++ b/packages/integration/src/github/core.test.ts @@ -15,7 +15,7 @@ */ import { GithubIntegrationConfig } from './config'; -import { getGithubFileFetchUrl, getGitHubRequestOptions } from './core'; +import { getGithubFileFetchUrl } from './core'; import { GithubCredentials } from './types'; describe('github core', () => { @@ -35,28 +35,6 @@ describe('github core', () => { type: 'token', }; - describe('getGitHubRequestOptions', () => { - it('inserts a token when needed', () => { - const withToken: GithubIntegrationConfig = { - host: '', - rawBaseUrl: '', - token: 'A', - }; - const withoutToken: GithubIntegrationConfig = { - host: '', - rawBaseUrl: '', - }; - expect( - (getGitHubRequestOptions(withToken, appCredentials).headers as any) - .Authorization, - ).toEqual('token A'); - expect( - (getGitHubRequestOptions(withoutToken, noCredentials).headers as any) - .Authorization, - ).toBeUndefined(); - }); - }); - describe('getGithubFileFetchUrl', () => { it('rejects targets that do not look like URLs', () => { const config: GithubIntegrationConfig = { host: '', apiBaseUrl: '' }; @@ -176,5 +154,49 @@ describe('github core', () => { ), ).toEqual('https://ghe.mycompany.net/raw/a/b/branchname/path/to/c.yaml'); }); + + it('rejects URLs with encoded path traversal sequences', () => { + const config: GithubIntegrationConfig = { + host: 'github.com', + apiBaseUrl: 'https://api.github.com', + }; + expect(() => + getGithubFileFetchUrl( + 'https://github.com/octocat/Hello-World/blob/main/%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fuser/repos', + config, + tokenCredentials, + ), + ).toThrow(/path traversal/); + }); + + it('rejects URLs with literal path traversal in filepath', () => { + const config: GithubIntegrationConfig = { + host: 'github.com', + apiBaseUrl: 'https://api.github.com', + }; + // Literal ../ is normalized by the URL constructor before git-url-parse + // sees it, so it fails with the existing validation instead + expect(() => + getGithubFileFetchUrl( + 'https://github.com/octocat/Hello-World/blob/main/../../user/repos', + config, + tokenCredentials, + ), + ).toThrow(/Incorrect URL/); + }); + + it('rejects raw endpoint URLs with path traversal', () => { + const config: GithubIntegrationConfig = { + host: 'github.com', + rawBaseUrl: 'https://raw.githubusercontent.com', + }; + expect(() => + getGithubFileFetchUrl( + 'https://github.com/octocat/Hello-World/blob/main/%2e%2e%2f%2e%2e%2fuser/repos', + config, + tokenCredentials, + ), + ).toThrow(/path traversal/); + }); }); }); diff --git a/packages/integration/src/github/core.ts b/packages/integration/src/github/core.ts index a755f3383c..9599338497 100644 --- a/packages/integration/src/github/core.ts +++ b/packages/integration/src/github/core.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import parseGitUrl from 'git-url-parse'; import { GithubIntegrationConfig } from './config'; +import { parseGitUrlSafe } from '../helpers'; import { GithubCredentials } from './types'; /** @@ -39,7 +39,7 @@ export function getGithubFileFetchUrl( credentials: GithubCredentials, ): string { try { - const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url); + const { owner, name, ref, filepathtype, filepath } = parseGitUrlSafe(url); if ( !owner || !name || @@ -63,30 +63,6 @@ export function getGithubFileFetchUrl( } } -/** - * Gets the request options necessary to make requests to a given provider. - * - * @deprecated This function is no longer used internally - * @param config - The relevant provider config - * @public - */ -export function getGitHubRequestOptions( - config: GithubIntegrationConfig, - credentials: GithubCredentials, -): { headers: Record } { - const headers: Record = {}; - - if (chooseEndpoint(config, credentials) === 'api') { - headers.Accept = 'application/vnd.github.v3.raw'; - } - - if (credentials.token) { - headers.Authorization = `token ${credentials.token}`; - } - - return { headers }; -} - export function chooseEndpoint( config: GithubIntegrationConfig, credentials: GithubCredentials, diff --git a/packages/integration/src/github/index.ts b/packages/integration/src/github/index.ts index ea6f18f2e0..450ac38a63 100644 --- a/packages/integration/src/github/index.ts +++ b/packages/integration/src/github/index.ts @@ -19,7 +19,7 @@ export { readGithubIntegrationConfigs, } from './config'; export type { GithubAppConfig, GithubIntegrationConfig } from './config'; -export { getGithubFileFetchUrl, getGitHubRequestOptions } from './core'; +export { getGithubFileFetchUrl } from './core'; export { DefaultGithubCredentialsProvider } from './DefaultGithubCredentialsProvider'; export { GithubAppCredentialsMux, diff --git a/packages/integration/src/helpers.test.ts b/packages/integration/src/helpers.test.ts index f1dd2e7ac8..b12236b031 100644 --- a/packages/integration/src/helpers.test.ts +++ b/packages/integration/src/helpers.test.ts @@ -19,6 +19,7 @@ import { basicIntegrations, defaultScmResolveUrl, isValidHost, + parseGitUrlSafe, } from './helpers'; describe('basicIntegrations', () => { @@ -317,3 +318,99 @@ describe('defaultScmResolveUrl', () => { ).toBe('https://b.com/b.yaml'); }); }); + +describe('parseGitUrlSafe', () => { + it('parses a valid GitHub blob URL', () => { + const result = parseGitUrlSafe( + 'https://github.com/owner/repo/blob/main/path/to/file.yaml', + ); + expect(result.owner).toBe('owner'); + expect(result.name).toBe('repo'); + expect(result.ref).toBe('main'); + expect(result.filepath).toBe('path/to/file.yaml'); + }); + + it('rejects URLs with encoded path traversal in filepath', () => { + expect(() => + parseGitUrlSafe( + 'https://github.com/octocat/Hello-World/blob/main/%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fuser/repos', + ), + ).toThrow('path traversal'); + }); + + it('rejects URLs with double-encoded path traversal in filepath', () => { + expect(() => + parseGitUrlSafe( + 'https://github.com/octocat/Hello-World/blob/main/foo%2f%2e%2e%2fbar%2f%2e%2e%2f%2e%2e%2fsecret', + ), + ).toThrow('path traversal'); + }); + + it('rejects URLs with uppercase %2E encoding in filepath', () => { + expect(() => + parseGitUrlSafe( + 'https://github.com/octocat/Hello-World/blob/main/%2E%2E%2F%2E%2E%2Fuser/repos', + ), + ).toThrow('path traversal'); + }); + + it('rejects URLs with mixed-case percent encoding', () => { + expect(() => + parseGitUrlSafe( + 'https://github.com/octocat/Hello-World/blob/main/%2E%2e%2Fuser/repos', + ), + ).toThrow('path traversal'); + }); + + it('rejects URLs where git-url-parse leaves percent-encoded traversal segments', () => { + expect(() => + parseGitUrlSafe( + 'https://github.com/octocat/Hello-World/blob/main/%252e%252e%252fuser/repos', + ), + ).toThrow('path traversal'); + }); + + it('rejects URLs with triple-encoded path traversal', () => { + expect(() => + parseGitUrlSafe( + 'https://github.com/octocat/Hello-World/blob/main/%25252e%25252e%25252fuser/repos', + ), + ).toThrow('path traversal'); + }); + + it('rejects URLs with literal .. in the middle of the filepath', () => { + // URL normalization resolves foo/../bar to just bar, so the filepath + // won't contain traversal. But we verify parseGitUrlSafe still handles + // the resolved path safely. + const result = parseGitUrlSafe( + 'https://github.com/owner/repo/blob/main/foo/../bar', + ); + expect(result.filepath).toBe('bar'); + }); + + it('handles literal ../ that gets normalized away by URL parsing', () => { + // Literal ../../../ gets resolved by the URL constructor before + // git-url-parse sees it. This mangles owner/name but the filepath + // is empty, so parseGitUrlSafe doesn't reject it. Downstream + // functions reject these via their own validation. + const result = parseGitUrlSafe( + 'https://github.com/owner/repo/blob/main/../../../user/repos', + ); + expect(result.filepath).toBe(''); + expect(result.owner).not.toBe('owner'); + }); + + it('allows filenames that contain dots but are not traversal', () => { + const result = parseGitUrlSafe( + 'https://github.com/owner/repo/blob/main/path/to/some..file.yaml', + ); + expect(result.filepath).toBe('path/to/some..file.yaml'); + }); + + it('allows URLs without a filepath', () => { + const result = parseGitUrlSafe('https://github.com/owner/repo'); + expect(result.owner).toBe('owner'); + expect(result.name).toBe('repo'); + expect(result.filepath).toBe(''); + }); +}); diff --git a/packages/integration/src/helpers.ts b/packages/integration/src/helpers.ts index f56d360325..9a7012d9ce 100644 --- a/packages/integration/src/helpers.ts +++ b/packages/integration/src/helpers.ts @@ -18,6 +18,38 @@ import parseGitUrl from 'git-url-parse'; import { trimEnd } from 'lodash'; import { ScmIntegration, ScmIntegrationsGroup } from './types'; +/** + * Wraps git-url-parse and rejects URLs whose filepath contains path traversal + * segments. Without this check, a URL like + * `https://github.com/o/r/blob/main/%2e%2e%2f%2e%2e%2fuser/repos` would be + * decoded to `../../user/repos` and could escape the expected API path when + * interpolated into provider API URLs. + */ +export function parseGitUrlSafe(url: string) { + const parsed = parseGitUrl(url); + if (parsed.filepath) { + let decoded = parsed.filepath; + let previous; + do { + previous = decoded; + try { + decoded = decodeURIComponent(decoded); + } catch { + break; + } + } while (decoded !== previous); + + if ( + decoded.split('/').some(segment => segment === '..' || segment === '.') + ) { + throw new Error( + 'Invalid SCM URL: path traversal is not allowed in the URL', + ); + } + } + return parsed; +} + /** Checks whether the given argument is a valid URL hostname */ export function isValidHost(host: string): boolean { const check = new URL('http://example.com'); @@ -84,7 +116,7 @@ export function defaultScmResolveUrl(options: { if (url.startsWith('/')) { // If it is an absolute path, move relative to the repo root - const { href, filepath } = parseGitUrl(base); + const { href, filepath } = parseGitUrlSafe(base); updated = new URL(href); diff --git a/packages/integration/src/index.ts b/packages/integration/src/index.ts index a625f5d83c..1c0919fcbd 100644 --- a/packages/integration/src/index.ts +++ b/packages/integration/src/index.ts @@ -24,7 +24,6 @@ export * from './awsS3'; export * from './awsCodeCommit'; export * from './azureBlobStorage'; export * from './azure'; -export * from './bitbucket'; export * from './bitbucketCloud'; export * from './bitbucketServer'; export * from './gerrit'; diff --git a/packages/integration/src/registry.ts b/packages/integration/src/registry.ts index 2e8111903b..6aca5b197b 100644 --- a/packages/integration/src/registry.ts +++ b/packages/integration/src/registry.ts @@ -19,7 +19,6 @@ import { AwsS3Integration } from './awsS3/AwsS3Integration'; import { AwsCodeCommitIntegration } from './awsCodeCommit'; import { AzureIntegration } from './azure/AzureIntegration'; import { BitbucketCloudIntegration } from './bitbucketCloud/BitbucketCloudIntegration'; -import { BitbucketIntegration } from './bitbucket/BitbucketIntegration'; import { BitbucketServerIntegration } from './bitbucketServer/BitbucketServerIntegration'; import { GerritIntegration } from './gerrit/GerritIntegration'; import { GithubIntegration } from './github/GithubIntegration'; @@ -39,10 +38,6 @@ export interface ScmIntegrationRegistry awsCodeCommit: ScmIntegrationsGroup; azureBlobStorage: ScmIntegrationsGroup; azure: ScmIntegrationsGroup; - /** - * @deprecated in favor of `bitbucketCloud` and `bitbucketServer` - */ - bitbucket: ScmIntegrationsGroup; bitbucketCloud: ScmIntegrationsGroup; bitbucketServer: ScmIntegrationsGroup; gerrit: ScmIntegrationsGroup; diff --git a/packages/module-federation-common/CHANGELOG.md b/packages/module-federation-common/CHANGELOG.md index 4c15a4b215..9017b9b96d 100644 --- a/packages/module-federation-common/CHANGELOG.md +++ b/packages/module-federation-common/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/module-federation-common +## 0.1.2-next.0 + +### Patch Changes + +- 0cb5646: Fixed the `@mui/material/styles` shared dependency key by removing a trailing slash that caused module resolution failures with MUI package exports. +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.2 + ## 0.1.0 ### Minor Changes diff --git a/packages/module-federation-common/package.json b/packages/module-federation-common/package.json index d0dc128548..1670773e3e 100644 --- a/packages/module-federation-common/package.json +++ b/packages/module-federation-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/module-federation-common", - "version": "0.1.0", + "version": "0.1.2-next.0", "description": "Helper library for module federation", "backstage": { "role": "common-library" diff --git a/packages/module-federation-common/src/defaults.ts b/packages/module-federation-common/src/defaults.ts index 27e0e292cf..644d4ee480 100644 --- a/packages/module-federation-common/src/defaults.ts +++ b/packages/module-federation-common/src/defaults.ts @@ -55,7 +55,7 @@ const defaultSharedDependencies = { // MUI v5 // not setting import: false for MUI packages as this // will break once Backstage moves to BUI - '@mui/material/styles/': { + '@mui/material/styles': { host: {}, remote: {}, }, diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index 8f1d51361f..0be622296b 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/repo-tools +## 0.17.0-next.1 + +### Minor Changes + +- 0fbcf23: Added support for OpenAPI 3.1 to all `schema openapi` commands. The commands now auto-detect the OpenAPI version from the spec file and use the appropriate generator, supporting both OpenAPI 3.0.x and 3.1.x specifications. + +### Patch Changes + +- 426edbe: Fixed `generate-catalog-info` command failing with "too many arguments" when invoked by lint-staged via the pre-commit hook. +- d5779e5: Updated the CLI report parser to support cleye-style help output, and strip ANSI escape codes from captured output. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + - @backstage/cli-node@0.2.19-next.1 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/errors@1.2.7 + +## 0.16.6-next.0 + +### Patch Changes + +- 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1 +- 2a51546: Fixed prettier existence checks in OpenAPI commands to use `fs.pathExists` instead of checking the resolved path string, which was always truthy. +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- 18a946c: Updated `@microsoft/api-extractor` to `7.57.3` and added tests for `getTsDocConfig` +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/cli-node@0.2.19-next.0 + - @backstage/config-loader@1.10.9-next.0 + - @backstage/backend-plugin-api@1.7.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/errors@1.2.7 + ## 0.16.4 ### Patch Changes diff --git a/packages/repo-tools/openapitools.json b/packages/repo-tools/openapitools.json index 65c0d8ead7..c7593e7a28 100644 --- a/packages/repo-tools/openapitools.json +++ b/packages/repo-tools/openapitools.json @@ -2,6 +2,6 @@ "$schema": "../../node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { - "version": "6.5.0" + "version": "7.18.0" } } diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index bc27ae4a42..a9809f614d 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/repo-tools", - "version": "0.16.4", + "version": "0.17.0-next.1", "description": "CLI for Backstage repo tooling ", "backstage": { "role": "cli" diff --git a/packages/repo-tools/src/commands/api-reports/cli-reports/runCliExtraction.ts b/packages/repo-tools/src/commands/api-reports/cli-reports/runCliExtraction.ts index 146c83bcfb..ae49c95f39 100644 --- a/packages/repo-tools/src/commands/api-reports/cli-reports/runCliExtraction.ts +++ b/packages/repo-tools/src/commands/api-reports/cli-reports/runCliExtraction.ts @@ -27,7 +27,14 @@ import { generateCliReport } from './generateCliReport'; import { logApiReportInstructions } from '../common'; function parseHelpPage(helpPageContent: string) { - const [, usage] = helpPageContent.match(/^\s*Usage: (.*)$/im) ?? []; + let usage: string | undefined; + + // Commander format: "Usage: backstage-cli ..." + const commanderUsage = helpPageContent.match(/^\s*Usage: (.*)$/im); + if (commanderUsage) { + usage = commanderUsage[1]; + } + const lines = helpPageContent.split(/\r?\n/); let options = new Array(); @@ -39,8 +46,8 @@ function parseHelpPage(helpPageContent: string) { lines.shift(); } if (lines.length > 0) { - // Start of a new section, e.g. "Options:" - const sectionName = lines.shift(); + // Start of a new section, e.g. "Options:" or "FLAGS:" + const sectionName = lines.shift()?.toLocaleLowerCase('en-US'); // Take lines until we hit the next section or the end const sectionEndIndex = lines.findIndex( line => line && !line.match(/^\s/), @@ -53,12 +60,18 @@ function parseHelpPage(helpPageContent: string) { .map(line => line.match(/^\s{1,8}(.*?)\s\s+/)?.[1]) .filter(Boolean) as string[]; - if (sectionName?.toLocaleLowerCase('en-US') === 'options:') { + if (sectionName === 'options:' || sectionName === 'flags:') { options = sectionItems; - } else if (sectionName?.toLocaleLowerCase('en-US') === 'commands:') { + } else if (sectionName === 'commands:') { commands = sectionItems; - } else if (sectionName?.toLocaleLowerCase('en-US') === 'arguments:') { + } else if (sectionName === 'arguments:') { commandArguments = sectionItems; + } else if (sectionName === 'usage:') { + // cleye format: usage line is inside the USAGE: section + const usageLine = sectionLines.find(l => l.trim().length > 0)?.trim(); + if (usageLine) { + usage = usageLine; + } } else { throw new Error(`Unknown CLI section: ${sectionName}`); } diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index dab7d6df0d..371d0609ff 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -226,6 +226,7 @@ export function registerCommands(program: Command) { 'CI run checks that there are no changes to catalog-info.yaml files', ) .description('Create or fix info yaml files for all backstage packages') + .allowExcessArguments(true) .action( lazy( () => import('./generate-catalog-info/generate-catalog-info'), 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 c5b2807fbd..42e9a9e682 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 @@ -26,6 +26,7 @@ import { import { deduplicateImports } from '../../../../../lib/openapi/dedupe-imports'; import { targetPaths } from '@backstage/cli-common'; import { + getOpenApiGeneratorKey, getPathToCurrentOpenApiSpec, toGeneratorAdditionalProperties, } from '../../../../../lib/openapi/helpers'; @@ -43,6 +44,7 @@ async function generate( const additionalProperties = toGeneratorAdditionalProperties({ initialValue: clientAdditionalProperties, }); + const generatorKey = await getOpenApiGeneratorKey(resolvedOpenapiPath); await fs.emptyDir(resolvedOutputDirectory); @@ -68,7 +70,7 @@ async function generate( 'templates/typescript-backstage-client.yaml', ), '--generator-key', - 'v3.0', + generatorKey, additionalProperties ? `--additional-properties=${additionalProperties}` : '', @@ -111,7 +113,12 @@ async function generate( } fs.removeSync(resolve(resolvedOutputDirectory, '.openapi-generator-ignore')); + fs.removeSync(resolve(resolvedOutputDirectory, '.gitattributes')); + fs.rmSync(resolve(resolvedOutputDirectory, 'docs'), { + recursive: true, + force: true, + }); fs.rmSync(resolve(resolvedOutputDirectory, '.openapi-generator'), { recursive: true, force: true, diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts index e913fd3a8c..daaf619450 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts @@ -29,6 +29,7 @@ import { import { deduplicateImports } from '../../../../../lib/openapi/dedupe-imports'; import { targetPaths } from '@backstage/cli-common'; import { + getOpenApiGeneratorKey, getPathToCurrentOpenApiSpec, getRelativePathToFile, toGeneratorAdditionalProperties, @@ -103,6 +104,7 @@ async function generate( const additionalProperties = toGeneratorAdditionalProperties({ initialValue: serverAdditionalProperties, }); + const generatorKey = await getOpenApiGeneratorKey(resolvedOpenapiPath); await exec( 'node', @@ -121,7 +123,7 @@ async function generate( 'templates/typescript-backstage-server.yaml', ), '--generator-key', - 'v3.0', + generatorKey, additionalProperties ? `--additional-properties=${additionalProperties}` : '', @@ -160,7 +162,12 @@ async function generate( } fs.removeSync(resolve(resolvedOutputDirectory, '.openapi-generator-ignore')); + fs.removeSync(resolve(resolvedOutputDirectory, '.gitattributes')); + fs.rmSync(resolve(resolvedOutputDirectory, 'docs'), { + recursive: true, + force: true, + }); fs.rmSync(resolve(resolvedOutputDirectory, '.openapi-generator'), { recursive: true, force: true, diff --git a/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts b/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts index 34c8589d54..91f43c4cee 100644 --- a/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts +++ b/packages/repo-tools/src/commands/repo/schema/openapi/lint.ts @@ -49,13 +49,15 @@ async function lint(directoryPath: string, config?: { strict: boolean }) { { extends: [oas, ruleset], rules: { - 'allow-reserved-in-params': { - given: '$.paths..parameters[*]', + 'allow-reserved-in-query-params': { + given: '$.paths..parameters[?(@.in == "query")]', then: { field: 'allowReserved', function: truthy, }, severity: 'error', + message: + 'Query parameters must specify allowReserved (true or false)', }, }, overrides: [ diff --git a/packages/repo-tools/src/commands/util.ts b/packages/repo-tools/src/commands/util.ts index 3ca8fa2f17..bb29cdf441 100644 --- a/packages/repo-tools/src/commands/util.ts +++ b/packages/repo-tools/src/commands/util.ts @@ -14,58 +14,63 @@ * limitations under the License. */ -import { spawn } from 'node:child_process'; -import os from 'node:os'; -import pLimit from 'p-limit'; +import { spawnSync } from 'node:child_process'; +import { randomUUID } from 'node:crypto'; +import { openSync, closeSync, readFileSync, unlinkSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; -// Some commands launch full node processes doing heavy work, which at high -// concurrency levels risk exhausting system resources. Placing the limiter here -// at the root level ensures that the concurrency boundary applies globally, not -// just per-runner. -const limiter = pLimit(os.cpus().length); +// Matches ANSI SGR escape sequences (e.g. bold, color, reset) +const ansiPattern = new RegExp(`${String.fromCharCode(0x1b)}\\[[0-9;]*m`, 'g'); +/** + * Redirect stdout to a temp file so that Node.js creates a SyncWriteStream + * (synchronous writes) in the child instead of an async pipe stream. This + * prevents data loss when child processes call process.exit() before the + * async stream buffer has been flushed. + * + * Uses spawnSync which blocks the event loop, so no concurrency limiter is + * needed — each call naturally runs sequentially. + */ export function createBinRunner(cwd: string, path: string) { - return async (...command: string[]) => - limiter( - () => - new Promise((resolve, reject) => { - // Handle the case where path is empty and the script path is the first command argument - const args = path ? [path, ...command] : command; - const child = spawn('node', args, { - cwd, - stdio: ['ignore', 'pipe', 'pipe'], - }); + return async (...command: string[]) => { + const args = path ? [path, ...command] : command; + const outPath = join(tmpdir(), `backstage-cli-out-${randomUUID()}.txt`); + const outFd = openSync(outPath, 'w'); - let stdout = ''; - let stderr = ''; + try { + const result = spawnSync('node', args, { + cwd, + env: { ...process.env, NO_COLOR: '1' }, + stdio: ['ignore', outFd, 'pipe'], + }); - child.stdout?.on('data', data => { - stdout += data.toString(); - }); + closeSync(outFd); + const stdout = readFileSync(outPath, 'utf8').replace(ansiPattern, ''); - child.stderr?.on('data', data => { - stderr += data.toString(); - }); + if (result.error) { + throw new Error(`Process error: ${result.error.message}`); + } - child.on('error', err => { - reject(new Error(`Process error: ${err.message}`)); - }); + const stderr = result.stderr?.toString() ?? ''; - child.on('close', (code, signal) => { - if (signal) { - reject( - new Error( - `Process was killed with signal ${signal}\n${stderr}`, - ), - ); - } else if (code !== 0) { - reject(new Error(`Process exited with code ${code}\n${stderr}`)); - } else if (stderr.trim()) { - reject(new Error(`Command printed error output: ${stderr}`)); - } else { - resolve(stdout); - } - }); - }), - ); + if (result.signal) { + throw new Error( + `Process was killed with signal ${result.signal}\n${stderr}`, + ); + } else if (result.status !== 0) { + throw new Error(`Process exited with code ${result.status}\n${stderr}`); + } else if (stderr.trim()) { + throw new Error(`Command printed error output: ${stderr}`); + } + + return stdout; + } finally { + try { + unlinkSync(outPath); + } catch { + /* ignore cleanup errors */ + } + } + }; } diff --git a/packages/repo-tools/src/lib/openapi/helpers.ts b/packages/repo-tools/src/lib/openapi/helpers.ts index d6b6927660..c4cd1564c6 100644 --- a/packages/repo-tools/src/lib/openapi/helpers.ts +++ b/packages/repo-tools/src/lib/openapi/helpers.ts @@ -70,3 +70,32 @@ export function toGeneratorAdditionalProperties({ .map(([key, value]) => `${key}=${value}`) .join(','); } + +export async function getOpenApiGeneratorKey( + specPath: string, +): Promise { + const yaml = (await loadAndValidateOpenApiYaml(specPath)) as any; + const version = yaml.openapi; + + if (!version) { + throw new Error(`Could not determine OpenAPI version from ${specPath}`); + } + + const semver = /^(\d+)\.(\d+)\.(\d+)(-.+)?$/.exec(version); + if (!semver) { + throw new Error(`Invalid OpenAPI version format ${version} in ${specPath}`); + } + const [, major, minor] = semver; + const supportedVersions = ['3.0', '3.1']; + + const majorMinor = `${major}.${minor}`; + if (!supportedVersions.includes(majorMinor)) { + throw new Error( + `Unsupported OpenAPI version ${version} in ${specPath}. Supported versions are: ${supportedVersions.join( + ', ', + )}`, + ); + } + + return `v${majorMinor}`; +} diff --git a/packages/scaffolder-internal/CHANGELOG.md b/packages/scaffolder-internal/CHANGELOG.md index 3ae462b888..13e8c5694c 100644 --- a/packages/scaffolder-internal/CHANGELOG.md +++ b/packages/scaffolder-internal/CHANGELOG.md @@ -1,5 +1,13 @@ # @internal/scaffolder +## 0.0.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-scaffolder-react@1.19.8-next.0 + ## 0.0.18 ### Patch Changes diff --git a/packages/scaffolder-internal/package.json b/packages/scaffolder-internal/package.json index d249b46969..32d77a2b31 100644 --- a/packages/scaffolder-internal/package.json +++ b/packages/scaffolder-internal/package.json @@ -1,6 +1,6 @@ { "name": "@internal/scaffolder", - "version": "0.0.18", + "version": "0.0.19-next.0", "backstage": { "role": "web-library", "inline": true diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 5dab0b834b..efc871e9c6 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,47 @@ # techdocs-cli-embedded-app +## 0.2.118-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.13.0-next.1 + - @backstage/cli@0.36.0-next.1 + - @backstage/plugin-techdocs@1.17.1-next.1 + - @backstage/plugin-catalog@1.34.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/integration-react@1.2.16-next.1 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + +## 0.2.118-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.12.1-next.0 + - @backstage/cli@0.35.5-next.0 + - @backstage/frontend-plugin-api@0.14.2-next.0 + - @backstage/plugin-catalog@1.33.1-next.0 + - @backstage/plugin-techdocs@1.17.1-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-components@0.18.8-next.0 + - @backstage/frontend-defaults@0.4.1-next.0 + - @backstage/integration-react@1.2.16-next.0 + - @backstage/test-utils@1.7.16-next.0 + - @backstage/theme@0.7.2 + - @backstage/plugin-app-react@0.2.1-next.0 + - @backstage/plugin-techdocs-react@1.3.9-next.0 + ## 0.2.117 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 027ab9f5a0..7c70e1a47c 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.117", + "version": "0.2.118-next.1", "backstage": { "role": "frontend" }, diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index 6c96f392ba..5eff559128 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,29 @@ # @techdocs/cli +## 1.10.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.2.0-next.1 + - @backstage/plugin-techdocs-node@1.14.3-next.1 + - @backstage/backend-defaults@0.16.0-next.1 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + +## 1.10.6-next.0 + +### Patch Changes + +- 70fc178: Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`. +- de62a9d: Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages. +- Updated dependencies + - @backstage/cli-common@0.2.0-next.0 + - @backstage/backend-defaults@0.15.3-next.0 + - @backstage/catalog-model@1.7.6 + - @backstage/config@1.3.6 + - @backstage/plugin-techdocs-node@1.14.3-next.0 + ## 1.10.5 ### Patch Changes diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 8299e76a2f..8b6a70f952 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,6 +1,6 @@ { "name": "@techdocs/cli", - "version": "1.10.5", + "version": "1.10.6-next.1", "description": "Utility CLI for managing TechDocs sites in Backstage.", "backstage": { "role": "cli" diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 6d01201f32..86a6b08bb6 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/test-utils +## 1.7.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.3.6 + - @backstage/core-app-api@1.19.6-next.0 + - @backstage/core-plugin-api@1.12.4-next.0 + - @backstage/theme@0.7.2 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-common@0.9.6 + - @backstage/plugin-permission-react@0.4.41-next.0 + ## 1.7.15 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 138b27126c..c5b4f771d2 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/test-utils", - "version": "1.7.15", + "version": "1.7.16-next.0", "description": "Utilities to test Backstage plugins and apps.", "backstage": { "role": "web-library" diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 5911274c45..bcf0ee3734 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,5 +1,125 @@ # @backstage/ui +## 0.13.0-next.1 + +### Minor Changes + +- 768f09d: **BREAKING**: Simplified the neutral background prop API for container components. The explicit `neutral-1`, `neutral-2`, `neutral-3`, and `neutral-auto` values have been removed from `ProviderBg`. They are replaced by a single `'neutral'` value that always auto-increments from the parent context, making it impossible to skip or pin to an explicit neutral level. + + **Migration:** + + Replace any explicit `bg="neutral-1"`, `bg="neutral-2"`, `bg="neutral-3"`, or `bg="neutral-auto"` props with `bg="neutral"`. To achieve a specific neutral level in stories or tests, use nested containers — each additional `bg="neutral"` wrapper increments by one level. + + ```tsx + // Before + ... + + // After + + ... + + ``` + + **Affected components:** Box, Flex, Grid, Card, Accordion, Popover, Tooltip, Dialog, Menu + +- b42fcdc: **BREAKING**: Removed `--bui-bg-popover` CSS token. Popover, Tooltip, Menu, and Dialog now use `--bui-bg-app` for their outer shell and `Box bg="neutral-1"` for content areas, providing better theme consistency and eliminating a redundant token. + + **Migration:** + + Replace any usage of `--bui-bg-popover` with `--bui-bg-neutral-1` (for content surfaces) or `--bui-bg-app` (for outer shells): + + ```diff + - background: var(--bui-bg-popover); + + background: var(--bui-bg-neutral-1); + ``` + + **Affected components:** Popover, Tooltip, Menu, Dialog + +- bd3a76e: **BREAKING**: Data attributes rendered by components are now always lowercase. This affects CSS selectors targeting camelCase data attributes. + + **Migration:** + + Update any custom CSS selectors that target camelCase data attributes to use lowercase instead: + + ```diff + - [data-startCollapsed='true'] { ... } + + [data-startcollapsed='true'] { ... } + ``` + + **Affected components:** SearchField + +- 95702ab: **BREAKING**: Removed deprecated types `ComponentDefinition`, `ClassNamesMap`, `DataAttributeValues`, and `DataAttributesMap` from the public API. These were internal styling infrastructure types that have been replaced by the `defineComponent` system. + + **Migration:** + + Remove any direct usage of these types. Component definitions now use `defineComponent()` and their shapes are not part of the public API contract. + + ```diff + - import type { ComponentDefinition, ClassNamesMap } from '@backstage/ui'; + ``` + + If you were reading `definition.dataAttributes`, use `definition.propDefs` instead — props with `dataAttribute: true` in `propDefs` are the equivalent. + +### Patch Changes + +- 58224d3: Fixed neutral-1 hover & pressed state in light mode. +- 95702ab: Migrated all components from `useStyles` to `useDefinition` hook. Exported `OwnProps` types for each component, enabling better type composition for consumers. + + **Affected components:** Avatar, Checkbox, Container, Dialog, FieldError, FieldLabel, Flex, FullPage, Grid, HeaderPage, Link, Menu, PasswordField, PluginHeader, Popover, RadioGroup, SearchField, Select, Skeleton, Switch, Table, TablePagination, Tabs, TagGroup, Text, TextField, ToggleButton, ToggleButtonGroup, Tooltip, VisuallyHidden + +- 4c2c350: Removed the `transition` on `Container` padding to prevent an unwanted animation when the viewport is resized. + + Affected components: Container + +- d4fa5b4: Fixed tab `matchStrategy` matching to ignore query parameters and hash fragments in tab `href` values. Previously, tabs with query params in their `href` (e.g., `/page?group=foo`) would never show as active since matching compared the full `href` string against `location.pathname` which never includes query params. + + **Affected components:** Tabs, PluginHeader + +- 36987db: Fixed handling of the `style` prop on `Button`, `ButtonIcon`, and `ButtonLink` so that it is now correctly forwarded to the underlying element instead of being silently dropped. + + **Affected components:** Button, ButtonIcon, ButtonLink + +- 95702ab: Fixed Link variant default from `'body'` to `'body-medium'` to match actual CSS selectors. The previous default did not correspond to a valid variant value. + + **Affected components:** Link + +- 9027b10: Fixed scroll overflow in Menu and Select popover content when constrained by viewport height. + + **Affected components:** Menu, Select + +- 4105a78: Merged the internal `PluginHeaderToolbar` component into `PluginHeader`, removing the separate component and its associated types (`PluginHeaderToolbarOwnProps`, `PluginHeaderToolbarProps`) and definition (`PluginHeaderToolbarDefinition`). This is an internal refactor with no changes to the public API of `PluginHeader`. + + **Affected components:** PluginHeader + +- b303857: Fixed `isRequired` prop not being passed to the underlying React Aria field components in TextField, SearchField, and PasswordField. Previously, `isRequired` was consumed locally for the secondary label text but never forwarded, which meant the input elements lacked `aria-required="true"` and React Aria's built-in required validation was not activated. + + **Affected components:** TextField, SearchField, PasswordField + +- cd3cb0f: Improved `useBreakpoint` performance by sharing a single set of `matchMedia` listeners across all component instances instead of creating independent listeners per hook call. +- 36987db: Extended `AlertProps`, `ContainerProps`, `DialogBodyProps`, and `FieldLabelProps` with native div element props to allow passing attributes like `aria-*` and `data-*`. + + **Affected components:** Alert, Container, DialogBody, FieldLabel + +- Updated dependencies + - @backstage/version-bridge@1.0.12 + +## 0.12.1-next.0 + +### Patch Changes + +- a1f4bee: Made Accordion a `bg` provider so nested components like Button auto-increment their background level. Updated `useDefinition` to resolve `bg` `propDef` defaults for provider components. +- 8909359: Fixed focus-visible outline styles for Menu and Select components. + + **Affected components:** Menu, Select + +- 0f462f8: Improved type safety in `useDefinition` by centralizing prop resolution and strengthening the `BgPropsConstraint` to require that `bg` provider components declare `children` as a required prop in their OwnProps type. +- 8909359: Added proper cursor styles for RadioGroup items. + + **Affected components:** RadioGroup + +- Updated dependencies + - @backstage/version-bridge@1.0.12 + ## 0.12.0 ### Minor Changes diff --git a/packages/ui/package.json b/packages/ui/package.json index 39e99b4be7..1440a40831 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/ui", - "version": "0.12.0", + "version": "0.13.0-next.1", "backstage": { "role": "web-library" }, @@ -50,12 +50,14 @@ "@remixicon/react": "^4.6.0", "@tanstack/react-table": "^8.21.3", "clsx": "^2.1.1", - "react-aria-components": "^1.14.0" + "react-aria-components": "^1.14.0", + "use-sync-external-store": "^1.4.0" }, "devDependencies": { "@backstage/cli": "workspace:^", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", + "@types/use-sync-external-store": "^0.0.6", "eslint-plugin-storybook": "^10.3.0-alpha.1", "glob": "^11.0.1", "globals": "^15.11.0", diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 5922155e93..7e8c8833ee 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -5,14 +5,14 @@ ```ts import type { ButtonProps as ButtonProps_2 } from 'react-aria-components'; import { CellProps as CellProps_2 } from 'react-aria-components'; -import { CheckboxProps as CheckboxProps_2 } from 'react-aria-components'; +import type { CheckboxProps as CheckboxProps_2 } from 'react-aria-components'; import { ColumnProps as ColumnProps_2 } from 'react-aria-components'; import type { ColumnSize } from '@react-types/table'; import type { ColumnStaticSize } from '@react-types/table'; -import { ComponentProps } from 'react'; +import type { ComponentProps } from 'react'; +import type { ComponentPropsWithoutRef } from 'react'; import type { ComponentPropsWithRef } from 'react'; import type { CSSProperties } from 'react'; -import { DetailedHTMLProps } from 'react'; import type { DialogTriggerProps as DialogTriggerProps_2 } from 'react-aria-components'; import type { DisclosureGroupProps } from 'react-aria-components'; import type { DisclosurePanelProps } from 'react-aria-components'; @@ -20,7 +20,6 @@ import type { DisclosureProps } from 'react-aria-components'; import type { ElementType } from 'react'; import { ForwardRefExoticComponent } from 'react'; import type { HeadingProps } from 'react-aria-components'; -import { HTMLAttributes } from 'react'; import { JSX as JSX_2 } from 'react/jsx-runtime'; import type { LinkProps as LinkProps_2 } from 'react-aria-components'; import type { ListBoxItemProps } from 'react-aria-components'; @@ -36,15 +35,15 @@ import type { RadioProps as RadioProps_2 } from 'react-aria-components'; import type { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RefAttributes } from 'react'; -import { RowProps } from 'react-aria-components'; +import { RowProps as RowProps_2 } from 'react-aria-components'; import type { SearchFieldProps as SearchFieldProps_2 } from 'react-aria-components'; import type { SelectProps as SelectProps_2 } from 'react-aria-components'; import type { SeparatorProps } from 'react-aria-components'; import type { SortDescriptor as SortDescriptor_2 } from 'react-stately'; import type { SubmenuTriggerProps as SubmenuTriggerProps_2 } from 'react-aria-components'; import type { SwitchProps as SwitchProps_2 } from 'react-aria-components'; -import { TableBodyProps } from 'react-aria-components'; -import { TableHeaderProps } from 'react-aria-components'; +import { TableBodyProps as TableBodyProps_2 } from 'react-aria-components'; +import { TableHeaderProps as TableHeaderProps_2 } from 'react-aria-components'; import { TableProps as TableProps_2 } from 'react-aria-components'; import type { TabListProps as TabListProps_2 } from 'react-aria-components'; import type { TabPanelProps as TabPanelProps_2 } from 'react-aria-components'; @@ -56,7 +55,7 @@ import type { TagProps as TagProps_2 } from 'react-aria-components'; import type { TextFieldProps as TextFieldProps_2 } from 'react-aria-components'; import type { ToggleButtonGroupProps as ToggleButtonGroupProps_2 } from 'react-aria-components'; import type { ToggleButtonProps as ToggleButtonProps_2 } from 'react-aria-components'; -import { TooltipProps as TooltipProps_2 } from 'react-aria-components'; +import type { TooltipProps as TooltipProps_2 } from 'react-aria-components'; import { TooltipTriggerComponentProps } from 'react-aria-components'; // @public (undocumented) @@ -76,7 +75,7 @@ export const AccordionDefinition: { readonly propDefs: { readonly bg: { readonly dataAttribute: true; - readonly default: 'neutral-auto'; + readonly default: 'neutral'; }; readonly children: {}; readonly className: {}; @@ -244,7 +243,13 @@ export type AlertOwnProps = { }; // @public -export interface AlertProps extends MarginProps, AlertOwnProps {} +export interface AlertProps + extends MarginProps, + AlertOwnProps, + Omit< + React.ComponentPropsWithoutRef<'div'>, + keyof AlertOwnProps | keyof MarginProps + > {} // @public (undocumented) export type AlignItems = 'stretch' | 'start' | 'center' | 'end'; @@ -256,23 +261,41 @@ export const Avatar: ForwardRefExoticComponent< // @public export const AvatarDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-AvatarRoot'; readonly image: 'bui-AvatarImage'; readonly fallback: 'bui-AvatarFallback'; }; - readonly dataAttributes: { - readonly size: readonly ['small', 'medium', 'large']; + readonly propDefs: { + readonly size: { + readonly dataAttribute: true; + readonly default: 'medium'; + }; + readonly purpose: { + readonly default: 'informative'; + }; + readonly src: {}; + readonly name: {}; + readonly className: {}; }; }; // @public (undocumented) -export interface AvatarProps extends React.ComponentPropsWithoutRef<'div'> { - name: string; - purpose?: 'decoration' | 'informative'; - size?: 'x-small' | 'small' | 'medium' | 'large' | 'x-large'; +export type AvatarOwnProps = { src: string; -} + name: string; + size?: 'x-small' | 'small' | 'medium' | 'large' | 'x-large'; + purpose?: 'decoration' | 'informative'; + className?: string; +}; + +// @public (undocumented) +export interface AvatarProps + extends Omit, 'children' | 'className'>, + AvatarOwnProps {} // @public (undocumented) export interface BgContextValue { @@ -424,7 +447,6 @@ export const ButtonDefinition: { readonly iconEnd: {}; readonly children: {}; readonly className: {}; - readonly style: {}; }; }; @@ -458,7 +480,6 @@ export const ButtonIconDefinition: { }; readonly icon: {}; readonly className: {}; - readonly style: {}; }; }; @@ -469,12 +490,11 @@ export type ButtonIconOwnProps = { icon?: ReactElement; loading?: boolean; className?: string; - style?: CSSProperties; }; // @public export interface ButtonIconProps - extends Omit, + extends Omit, ButtonIconOwnProps {} // @public (undocumented) @@ -505,7 +525,6 @@ export const ButtonLinkDefinition: { readonly iconEnd: {}; readonly children: {}; readonly className: {}; - readonly style: {}; }; }; @@ -517,12 +536,11 @@ export type ButtonLinkOwnProps = { iconEnd?: ReactElement; children?: ReactNode; className?: string; - style?: CSSProperties; }; // @public export interface ButtonLinkProps - extends Omit, + extends Omit, ButtonLinkOwnProps {} // @public (undocumented) @@ -535,12 +553,11 @@ export type ButtonOwnProps = { loading?: boolean; children?: ReactNode; className?: string; - style?: CSSProperties; }; // @public export interface ButtonProps - extends Omit, + extends Omit, ButtonOwnProps {} // @public @@ -548,6 +565,12 @@ export const Card: ForwardRefExoticComponent< CardProps & RefAttributes >; +// @public (undocumented) +export type CardBaseProps = { + children?: ReactNode; + className?: string; +}; + // @public export const CardBody: ForwardRefExoticComponent< CardBodyProps & RefAttributes @@ -578,6 +601,16 @@ export interface CardBodyProps extends CardBodyOwnProps, React.HTMLAttributes {} +// @public (undocumented) +export type CardButtonVariant = { + onPress: NonNullable; + href?: never; + label: string; + target?: never; + rel?: never; + download?: never; +}; + // @public export const CardDefinition: { readonly styles: { @@ -585,10 +618,17 @@ export const CardDefinition: { }; readonly classNames: { readonly root: 'bui-Card'; + readonly overlay: 'bui-CardOverlay'; }; readonly propDefs: { readonly children: {}; readonly className: {}; + readonly onPress: {}; + readonly href: {}; + readonly label: {}; + readonly target: {}; + readonly rel: {}; + readonly download: {}; }; }; @@ -653,15 +693,42 @@ export interface CardHeaderProps React.HTMLAttributes {} // @public (undocumented) -export type CardOwnProps = { - children?: ReactNode; - className?: string; +export type CardLinkVariant = { + href: string; + onPress?: never; + label: string; + target?: string; + rel?: string; + download?: boolean | string; }; // @public -export interface CardProps - extends CardOwnProps, - React.HTMLAttributes {} +export type CardOwnProps = Pick< + CardBaseProps & (CardButtonVariant | CardLinkVariant | CardStaticVariant), + | 'children' + | 'className' + | 'onPress' + | 'href' + | 'label' + | 'target' + | 'rel' + | 'download' +>; + +// @public +export type CardProps = CardBaseProps & + Omit, 'onPress'> & + (CardButtonVariant | CardLinkVariant | CardStaticVariant); + +// @public (undocumented) +export type CardStaticVariant = { + onPress?: never; + href?: never; + label?: never; + target?: never; + rel?: never; + download?: never; +}; // @public (undocumented) export const Cell: { @@ -669,25 +736,33 @@ export const Cell: { displayName: string; }; +// @public (undocumented) +export type CellOwnProps = { + className?: string; +}; + // @public (undocumented) export const CellProfile: (props: CellProfileProps) => JSX_2.Element; // @public (undocumented) -export interface CellProfileProps extends CellProps_2 { - // (undocumented) - color?: TextColors; - // (undocumented) - description?: string; - // (undocumented) - href?: string; - // (undocumented) - name?: string; - // (undocumented) +export type CellProfileOwnProps = { src?: string; -} + name?: string; + href?: string; + description?: string; + color?: TextColors; + className?: string; +}; // @public (undocumented) -export interface CellProps extends CellProps_2 {} +export interface CellProfileProps + extends CellProfileOwnProps, + Omit {} + +// @public (undocumented) +export interface CellProps + extends CellOwnProps, + Omit {} // @public (undocumented) export const CellText: { @@ -696,18 +771,19 @@ export const CellText: { }; // @public (undocumented) -export interface CellTextProps extends CellProps_2 { - // (undocumented) - color?: TextColors; - // (undocumented) - description?: string; - // (undocumented) - href?: string; - // (undocumented) - leadingIcon?: React.ReactNode | null; - // (undocumented) +export type CellTextOwnProps = { title: string; -} + description?: string; + color?: TextColors; + leadingIcon?: React.ReactNode | null; + href?: string; + className?: string; +}; + +// @public (undocumented) +export interface CellTextProps + extends CellTextOwnProps, + Omit {} // @public (undocumented) export const Checkbox: ForwardRefExoticComponent< @@ -716,24 +792,37 @@ export const Checkbox: ForwardRefExoticComponent< // @public export const CheckboxDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Checkbox'; readonly indicator: 'bui-CheckboxIndicator'; }; - readonly dataAttributes: { - readonly selected: readonly [true, false]; - readonly indeterminate: readonly [true, false]; + readonly propDefs: { + readonly selected: { + readonly dataAttribute: true; + }; + readonly indeterminate: { + readonly dataAttribute: true; + }; + readonly children: {}; + readonly className: {}; }; }; // @public (undocumented) -export interface CheckboxProps extends CheckboxProps_2 { - // (undocumented) +export type CheckboxOwnProps = { + selected?: boolean; + indeterminate?: boolean; children: React.ReactNode; -} + className?: string; +}; -// @public -export type ClassNamesMap = Record; +// @public (undocumented) +export interface CheckboxProps + extends Omit, + CheckboxOwnProps {} // @public (undocumented) export const Column: (props: ColumnProps) => JSX_2.Element; @@ -765,10 +854,15 @@ export interface ColumnConfig { } // @public (undocumented) -export interface ColumnProps extends Omit { - // (undocumented) +export type ColumnOwnProps = { children?: React.ReactNode; -} + className?: string; +}; + +// @public (undocumented) +export interface ColumnProps + extends ColumnOwnProps, + Omit {} // @public (undocumented) export type Columns = @@ -786,16 +880,6 @@ export type Columns = | '12' | 'auto'; -// @public -export interface ComponentDefinition { - // (undocumented) - classNames: ClassNamesMap; - // (undocumented) - dataAttributes?: DataAttributesMap; - // (undocumented) - utilityProps?: string[]; -} - // @public (undocumented) export const Container: ForwardRefExoticComponent< ContainerProps & RefAttributes @@ -812,18 +896,39 @@ export type ContainerBg = // @public export const ContainerDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Container'; }; - readonly utilityProps: ['my', 'mt', 'mb', 'py', 'pt', 'pb', 'display']; + readonly propDefs: { + readonly children: {}; + readonly className: {}; + readonly style: {}; + }; + readonly utilityProps: readonly [ + 'my', + 'mt', + 'mb', + 'py', + 'pt', + 'pb', + 'display', + ]; }; // @public (undocumented) -export interface ContainerProps { - // (undocumented) +export type ContainerOwnProps = { children?: React.ReactNode; - // (undocumented) className?: string; + style?: React.CSSProperties; +}; + +// @public (undocumented) +export interface ContainerProps + extends ContainerOwnProps, + Omit, keyof ContainerOwnProps> { // (undocumented) mb?: SpaceProps['mb']; // (undocumented) @@ -836,8 +941,6 @@ export interface ContainerProps { pt?: SpaceProps['pt']; // (undocumented) py?: SpaceProps['py']; - // (undocumented) - style?: React.CSSProperties; } // @public (undocumented) @@ -868,12 +971,6 @@ export interface CursorResponse { totalCount?: number; } -// @public -export type DataAttributesMap = Record; - -// @public -export type DataAttributeValues = readonly (string | number | boolean)[]; - // @public (undocumented) export const Dialog: ForwardRefExoticComponent< DialogProps & RefAttributes @@ -885,58 +982,123 @@ export const DialogBody: ForwardRefExoticComponent< >; // @public -export interface DialogBodyProps { - // (undocumented) - children?: React.ReactNode; - // (undocumented) +export const DialogBodyDefinition: { + readonly styles: { + readonly [key: string]: string; + }; + readonly classNames: { + readonly root: 'bui-DialogBody'; + }; + readonly propDefs: { + readonly children: {}; + readonly className: {}; + }; +}; + +// @public (undocumented) +export type DialogBodyOwnProps = { + children?: ReactNode; className?: string; -} +}; + +// @public +export interface DialogBodyProps + extends DialogBodyOwnProps, + Omit, keyof DialogBodyOwnProps> {} // @public export const DialogDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { - readonly overlay: 'bui-DialogOverlay'; + readonly root: 'bui-DialogOverlay'; readonly dialog: 'bui-Dialog'; - readonly header: 'bui-DialogHeader'; - readonly headerTitle: 'bui-DialogHeaderTitle'; - readonly body: 'bui-DialogBody'; - readonly footer: 'bui-DialogFooter'; + readonly content: 'bui-DialogContent'; + }; + readonly propDefs: { + readonly children: {}; + readonly className: {}; + readonly width: {}; + readonly height: {}; + readonly style: {}; }; }; // @public (undocumented) export const DialogFooter: ForwardRefExoticComponent< - Omit< - DetailedHTMLProps, HTMLDivElement>, - 'ref' - > & - RefAttributes + DialogFooterProps & RefAttributes >; +// @public +export const DialogFooterDefinition: { + readonly styles: { + readonly [key: string]: string; + }; + readonly classNames: { + readonly root: 'bui-DialogFooter'; + }; + readonly propDefs: { + readonly children: {}; + readonly className: {}; + }; +}; + +// @public (undocumented) +export type DialogFooterOwnProps = { + children?: ReactNode; + className?: string; +}; + +// @public +export interface DialogFooterProps + extends DialogFooterOwnProps, + Omit, keyof DialogFooterOwnProps> {} + // @public (undocumented) export const DialogHeader: ForwardRefExoticComponent< DialogHeaderProps & RefAttributes >; // @public -export interface DialogHeaderProps extends HeadingProps { - // (undocumented) - children?: React.ReactNode; - // (undocumented) +export const DialogHeaderDefinition: { + readonly styles: { + readonly [key: string]: string; + }; + readonly classNames: { + readonly root: 'bui-DialogHeader'; + readonly title: 'bui-DialogHeaderTitle'; + }; + readonly propDefs: { + readonly children: {}; + readonly className: {}; + }; +}; + +// @public (undocumented) +export type DialogHeaderOwnProps = { + children?: ReactNode; className?: string; -} +}; // @public -export interface DialogProps extends ModalOverlayProps { - // (undocumented) - children?: React.ReactNode; - // (undocumented) +export interface DialogHeaderProps + extends DialogHeaderOwnProps, + Omit {} + +// @public (undocumented) +export type DialogOwnProps = { + children?: ReactNode; className?: string; - // (undocumented) - height?: number | string; - // (undocumented) width?: number | string; -} + height?: number | string; + style?: React.CSSProperties; +}; + +// @public +export interface DialogProps + extends DialogOwnProps, + Omit {} // @public (undocumented) export const DialogTrigger: (props: DialogTriggerProps) => JSX_2.Element; @@ -954,23 +1116,39 @@ export const FieldLabel: ForwardRefExoticComponent< // @public export const FieldLabelDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-FieldLabelWrapper'; readonly label: 'bui-FieldLabel'; readonly secondaryLabel: 'bui-FieldSecondaryLabel'; readonly description: 'bui-FieldDescription'; }; + readonly propDefs: { + readonly label: {}; + readonly secondaryLabel: {}; + readonly description: {}; + readonly htmlFor: {}; + readonly id: {}; + readonly className: {}; + }; +}; + +// @public (undocumented) +export type FieldLabelOwnProps = { + label?: string | null; + secondaryLabel?: string | null; + description?: string | null; + htmlFor?: string; + id?: string; + className?: string; }; // @public (undocumented) export interface FieldLabelProps - extends Pick, 'className'> { - description?: string | null; - htmlFor?: string; - id?: string; - label?: string | null; - secondaryLabel?: string | null; -} + extends FieldLabelOwnProps, + Omit, keyof FieldLabelOwnProps> {} // @public (undocumented) export interface FilterState { @@ -987,10 +1165,22 @@ export const Flex: ForwardRefExoticComponent< // @public export const FlexDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Flex'; }; - readonly utilityProps: [ + readonly bg: 'provider'; + readonly propDefs: { + readonly bg: { + readonly dataAttribute: true; + }; + readonly children: {}; + readonly className: {}; + readonly style: {}; + }; + readonly utilityProps: readonly [ 'm', 'mb', 'ml', @@ -1010,39 +1200,29 @@ export const FlexDefinition: { 'justify', 'direction', ]; - readonly dataAttributes: { - readonly bg: readonly [ - 'neutral-1', - 'neutral-2', - 'neutral-3', - 'danger', - 'warning', - 'success', - ]; - }; }; // @public (undocumented) export type FlexDirection = 'row' | 'column'; // @public (undocumented) -export interface FlexProps extends SpaceProps { +export type FlexOwnProps = { + children: ReactNode; + className?: string; + style?: CSSProperties; + bg?: Responsive; +}; + +// @public (undocumented) +export interface FlexProps extends SpaceProps, FlexOwnProps { // (undocumented) align?: Responsive<'start' | 'center' | 'end' | 'baseline' | 'stretch'>; // (undocumented) - bg?: Responsive; - // (undocumented) - children?: React.ReactNode; - // (undocumented) - className?: string; - // (undocumented) direction?: Responsive<'row' | 'column' | 'row-reverse' | 'column-reverse'>; // (undocumented) gap?: Responsive; // (undocumented) justify?: Responsive<'start' | 'center' | 'end' | 'between'>; - // (undocumented) - style?: React.CSSProperties; } // @public (undocumented) @@ -1055,13 +1235,26 @@ export const FullPage: ForwardRefExoticComponent< // @public export const FullPageDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-FullPage'; }; + readonly propDefs: { + readonly className: {}; + }; +}; + +// @public (undocumented) +export type FullPageOwnProps = { + className?: string; }; // @public -export interface FullPageProps extends React.ComponentPropsWithoutRef<'main'> {} +export interface FullPageProps + extends Omit, 'className'>, + FullPageOwnProps {} // @public (undocumented) export const Grid: { @@ -1073,10 +1266,22 @@ export const Grid: { // @public export const GridDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Grid'; }; - readonly utilityProps: [ + readonly bg: 'provider'; + readonly propDefs: { + readonly bg: { + readonly dataAttribute: true; + }; + readonly children: {}; + readonly className: {}; + readonly style: {}; + }; + readonly utilityProps: readonly [ 'columns', 'gap', 'm', @@ -1094,44 +1299,38 @@ export const GridDefinition: { 'px', 'py', ]; - readonly dataAttributes: { - readonly bg: readonly [ - 'neutral-1', - 'neutral-2', - 'neutral-3', - 'danger', - 'warning', - 'success', - ]; - }; }; // @public export const GridItemDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-GridItem'; }; - readonly utilityProps: ['colSpan', 'colEnd', 'colStart', 'rowSpan']; - readonly dataAttributes: { - readonly bg: readonly [ - 'neutral-1', - 'neutral-2', - 'neutral-3', - 'danger', - 'warning', - 'success', - ]; + readonly bg: 'provider'; + readonly propDefs: { + readonly bg: { + readonly dataAttribute: true; + }; + readonly children: {}; + readonly className: {}; + readonly style: {}; }; + readonly utilityProps: readonly ['colSpan', 'colEnd', 'colStart', 'rowSpan']; }; // @public (undocumented) -export interface GridItemProps { - // (undocumented) - bg?: Responsive; - // (undocumented) - children?: React.ReactNode; - // (undocumented) +export type GridItemOwnProps = { + children: ReactNode; className?: string; + style?: CSSProperties; + bg?: Responsive; +}; + +// @public (undocumented) +export interface GridItemProps extends GridItemOwnProps { // (undocumented) colEnd?: Responsive; // (undocumented) @@ -1140,24 +1339,22 @@ export interface GridItemProps { colStart?: Responsive; // (undocumented) rowSpan?: Responsive; - // (undocumented) - style?: React.CSSProperties; } // @public (undocumented) -export interface GridProps extends SpaceProps { - // (undocumented) - bg?: Responsive; - // (undocumented) - children?: React.ReactNode; - // (undocumented) +export type GridOwnProps = { + children: ReactNode; className?: string; + style?: CSSProperties; + bg?: Responsive; +}; + +// @public (undocumented) +export interface GridProps extends SpaceProps, GridOwnProps { // (undocumented) columns?: Responsive; // (undocumented) gap?: Responsive; - // (undocumented) - style?: React.CSSProperties; } // @public @@ -1173,6 +1370,9 @@ export interface HeaderPageBreadcrumb { // @public export const HeaderPageDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-HeaderPage'; readonly content: 'bui-HeaderPageContent'; @@ -1180,10 +1380,17 @@ export const HeaderPageDefinition: { readonly tabsWrapper: 'bui-HeaderPageTabsWrapper'; readonly controls: 'bui-HeaderPageControls'; }; + readonly propDefs: { + readonly title: {}; + readonly customActions: {}; + readonly tabs: {}; + readonly breadcrumbs: {}; + readonly className: {}; + }; }; // @public -export interface HeaderPageProps { +export interface HeaderPageOwnProps { // (undocumented) breadcrumbs?: HeaderPageBreadcrumb[]; // (undocumented) @@ -1196,6 +1403,9 @@ export interface HeaderPageProps { title?: string; } +// @public +export interface HeaderPageProps extends HeaderPageOwnProps {} + // @public export interface HeaderTab { // (undocumented) @@ -1223,45 +1433,56 @@ export const Link: ForwardRefExoticComponent< // @public export const LinkDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Link'; }; - readonly dataAttributes: { - readonly variant: readonly ['subtitle', 'body', 'caption', 'label']; - readonly weight: readonly ['regular', 'bold']; - readonly color: readonly [ - 'primary', - 'secondary', - 'danger', - 'warning', - 'success', - 'info', - ]; - readonly truncate: readonly [true, false]; - readonly standalone: readonly [true, false]; + readonly propDefs: { + readonly variant: { + readonly dataAttribute: true; + readonly default: 'body-medium'; + }; + readonly weight: { + readonly dataAttribute: true; + readonly default: 'regular'; + }; + readonly color: { + readonly dataAttribute: true; + readonly default: 'primary'; + }; + readonly truncate: { + readonly dataAttribute: true; + }; + readonly standalone: { + readonly dataAttribute: true; + }; + readonly title: {}; + readonly children: {}; + readonly className: {}; }; }; // @public (undocumented) -export interface LinkProps extends LinkProps_2 { - // (undocumented) - children?: ReactNode; - // (undocumented) +export type LinkOwnProps = { + variant?: TextVariants | Partial>; + weight?: TextWeights | Partial>; color?: | TextColors | TextColorStatus | Partial>; - // (undocumented) - standalone?: boolean; - // (undocumented) - title?: string; - // (undocumented) truncate?: boolean; - // (undocumented) - variant?: TextVariants | Partial>; - // (undocumented) - weight?: TextWeights | Partial>; -} + standalone?: boolean; + title?: string; + children?: ReactNode; + className?: string; +}; + +// @public (undocumented) +export interface LinkProps + extends Omit, + LinkOwnProps {} // @public (undocumented) export interface MarginProps { @@ -1295,56 +1516,47 @@ export const MenuAutocompleteListbox: ( ) => JSX_2.Element; // @public (undocumented) -export interface MenuAutocompleteListBoxProps - extends ListBoxProps, - Omit, 'children'> { - // (undocumented) - maxHeight?: string; - // (undocumented) - maxWidth?: string; - // (undocumented) +export type MenuAutocompleteListBoxOwnProps = MenuPopoverOwnProps & { placeholder?: string; - // (undocumented) - placement?: PopoverProps_2['placement']; - // (undocumented) - virtualized?: boolean; -} + selectionMode?: ListBoxProps['selectionMode']; +}; + +// @public (undocumented) +export interface MenuAutocompleteListBoxProps + extends MenuAutocompleteListBoxOwnProps, + Omit, keyof MenuAutocompleteListBoxOwnProps> {} + +// @public (undocumented) +export type MenuAutocompleteOwnProps = MenuPopoverOwnProps & { + placeholder?: string; +}; // @public (undocumented) export interface MenuAutocompleteProps - extends MenuProps_2, - Omit, 'children'> { - // (undocumented) - maxHeight?: string; - // (undocumented) - maxWidth?: string; - // (undocumented) - placeholder?: string; - // (undocumented) - placement?: PopoverProps_2['placement']; - // (undocumented) - virtualized?: boolean; -} + extends MenuAutocompleteOwnProps, + Omit, keyof MenuAutocompleteOwnProps> {} // @public export const MenuDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { - readonly root: 'bui-Menu'; - readonly popover: 'bui-MenuPopover'; + readonly root: 'bui-MenuPopover'; + readonly inner: 'bui-MenuInner'; readonly content: 'bui-MenuContent'; - readonly section: 'bui-MenuSection'; - readonly sectionHeader: 'bui-MenuSectionHeader'; - readonly item: 'bui-MenuItem'; - readonly itemListBox: 'bui-MenuItemListBox'; - readonly itemListBoxCheck: 'bui-MenuItemListBoxCheck'; - readonly itemWrapper: 'bui-MenuItemWrapper'; - readonly itemContent: 'bui-MenuItemContent'; - readonly itemArrow: 'bui-MenuItemArrow'; - readonly separator: 'bui-MenuSeparator'; - readonly searchField: 'bui-MenuSearchField'; - readonly searchFieldInput: 'bui-MenuSearchFieldInput'; - readonly searchFieldClear: 'bui-MenuSearchFieldClear'; - readonly emptyState: 'bui-MenuEmptyState'; + }; + readonly propDefs: { + readonly placement: { + readonly default: 'bottom start'; + }; + readonly virtualized: { + readonly default: false; + }; + readonly maxWidth: {}; + readonly maxHeight: {}; + readonly style: {}; + readonly className: {}; }; }; @@ -1352,16 +1564,18 @@ export const MenuDefinition: { export const MenuItem: (props: MenuItemProps) => JSX_2.Element; // @public (undocumented) -export interface MenuItemProps - extends MenuItemProps_2, - Omit { - // (undocumented) - children: React.ReactNode; - // (undocumented) - color?: 'primary' | 'danger'; - // (undocumented) +export type MenuItemOwnProps = { iconStart?: React.ReactNode; -} + children: React.ReactNode; + color?: 'primary' | 'danger'; + href?: MenuItemProps_2['href']; + className?: string; +}; + +// @public (undocumented) +export interface MenuItemProps + extends MenuItemOwnProps, + Omit {} // @public (undocumented) export const MenuListBox: (props: MenuListBoxProps) => JSX_2.Element; @@ -1370,59 +1584,71 @@ export const MenuListBox: (props: MenuListBoxProps) => JSX_2.Element; export const MenuListBoxItem: (props: MenuListBoxItemProps) => JSX_2.Element; // @public (undocumented) -export interface MenuListBoxItemProps - extends ListBoxItemProps, - Omit { - // (undocumented) +export type MenuListBoxItemOwnProps = { children: React.ReactNode; -} + className?: string; +}; + +// @public (undocumented) +export interface MenuListBoxItemProps + extends MenuListBoxItemOwnProps, + Omit {} + +// @public (undocumented) +export type MenuListBoxOwnProps = MenuPopoverOwnProps & { + selectionMode?: ListBoxProps['selectionMode']; +}; // @public (undocumented) export interface MenuListBoxProps - extends ListBoxProps, - Omit, 'children'> { - // (undocumented) - maxHeight?: string; - // (undocumented) - maxWidth?: string; - // (undocumented) + extends MenuListBoxOwnProps, + Omit, keyof MenuListBoxOwnProps> {} + +// @public (undocumented) +export type MenuOwnProps = MenuPopoverOwnProps; + +// @public +export type MenuPopoverOwnProps = { placement?: PopoverProps_2['placement']; - // (undocumented) virtualized?: boolean; -} + maxWidth?: string; + maxHeight?: string; + style?: React.CSSProperties; + className?: string; +}; // @public (undocumented) export interface MenuProps - extends MenuProps_2, - Omit, 'children'> { - // (undocumented) - maxHeight?: string; - // (undocumented) - maxWidth?: string; - // (undocumented) - placement?: PopoverProps_2['placement']; - // (undocumented) - virtualized?: boolean; -} + extends MenuOwnProps, + Omit, keyof MenuOwnProps> {} // @public (undocumented) export const MenuSection: (props: MenuSectionProps) => JSX_2.Element; // @public (undocumented) -export interface MenuSectionProps - extends MenuSectionProps_2, - Omit, 'children'> { - // (undocumented) - children: React.ReactNode; - // (undocumented) +export type MenuSectionOwnProps = { title: string; -} + children: React.ReactNode; + className?: string; +}; + +// @public (undocumented) +export interface MenuSectionProps + extends MenuSectionOwnProps, + Omit, keyof MenuSectionOwnProps> {} // @public (undocumented) export const MenuSeparator: (props: MenuSeparatorProps) => JSX_2.Element; // @public (undocumented) -export interface MenuSeparatorProps extends SeparatorProps {} +export type MenuSeparatorOwnProps = { + className?: string; +}; + +// @public (undocumented) +export interface MenuSeparatorProps + extends MenuSeparatorOwnProps, + Omit {} // @public (undocumented) export const MenuTrigger: (props: MenuTriggerProps) => JSX_2.Element; @@ -1525,6 +1751,9 @@ export const PasswordField: ForwardRefExoticComponent< // @public export const PasswordFieldDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-PasswordField'; readonly inputWrapper: 'bui-PasswordFieldInputWrapper'; @@ -1532,25 +1761,44 @@ export const PasswordFieldDefinition: { readonly inputIcon: 'bui-PasswordFieldIcon'; readonly inputVisibility: 'bui-PasswordFieldVisibility'; }; - readonly dataAttributes: { - readonly size: readonly ['small', 'medium']; + readonly propDefs: { + readonly size: { + readonly dataAttribute: true; + readonly default: 'small'; + }; + readonly className: {}; + readonly icon: {}; + readonly placeholder: {}; + readonly label: {}; + readonly description: {}; + readonly secondaryLabel: {}; }; }; // @public (undocumented) -export interface PasswordFieldProps - extends TextFieldProps_2, - Omit { +export type PasswordFieldOwnProps = { + size?: 'small' | 'medium' | Partial>; + className?: string; icon?: ReactNode; placeholder?: string; - size?: 'small' | 'medium' | Partial>; -} + label?: FieldLabelProps['label']; + description?: FieldLabelProps['description']; + secondaryLabel?: FieldLabelProps['secondaryLabel']; +}; + +// @public (undocumented) +export interface PasswordFieldProps + extends Omit, + PasswordFieldOwnProps {} // @public export const PluginHeader: (props: PluginHeaderProps) => JSX_2.Element; // @public export const PluginHeaderDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-PluginHeader'; readonly toolbar: 'bui-PluginHeaderToolbar'; @@ -1559,12 +1807,21 @@ export const PluginHeaderDefinition: { readonly toolbarControls: 'bui-PluginHeaderToolbarControls'; readonly toolbarIcon: 'bui-PluginHeaderToolbarIcon'; readonly toolbarName: 'bui-PluginHeaderToolbarName'; - readonly tabsWrapper: 'bui-PluginHeaderTabsWrapper'; + readonly tabs: 'bui-PluginHeaderTabsWrapper'; + }; + readonly propDefs: { + readonly icon: {}; + readonly title: {}; + readonly titleLink: {}; + readonly customActions: {}; + readonly tabs: {}; + readonly onTabSelectionChange: {}; + readonly className: {}; }; }; // @public -export interface PluginHeaderProps { +export interface PluginHeaderOwnProps { // (undocumented) className?: string; // (undocumented) @@ -1581,6 +1838,9 @@ export interface PluginHeaderProps { titleLink?: string; } +// @public +export interface PluginHeaderProps extends PluginHeaderOwnProps {} + // @public export const Popover: ForwardRefExoticComponent< PopoverProps & RefAttributes @@ -1588,21 +1848,35 @@ export const Popover: ForwardRefExoticComponent< // @public export const PopoverDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Popover'; readonly arrow: 'bui-PopoverArrow'; readonly content: 'bui-PopoverContent'; }; + readonly propDefs: { + readonly children: {}; + readonly hideArrow: {}; + readonly className: {}; + }; +}; + +// @public (undocumented) +export type PopoverOwnProps = { + children: React.ReactNode; + hideArrow?: boolean; + className?: string; }; // @public -export interface PopoverProps extends Omit { - children: React.ReactNode; - hideArrow?: boolean; -} +export interface PopoverProps + extends Omit, + PopoverOwnProps {} // @public -export type ProviderBg = ContainerBg | 'neutral-auto'; +export type ProviderBg = 'neutral' | 'danger' | 'warning' | 'success'; // @public (undocumented) export interface QueryOptions { @@ -1631,6 +1905,19 @@ export const Radio: ForwardRefExoticComponent< RadioProps & RefAttributes >; +// @public +export const RadioDefinition: { + readonly styles: { + readonly [key: string]: string; + }; + readonly classNames: { + readonly root: 'bui-Radio'; + }; + readonly propDefs: { + readonly className: {}; + }; +}; + // @public (undocumented) export const RadioGroup: ForwardRefExoticComponent< RadioGroupProps & RefAttributes @@ -1638,23 +1925,47 @@ export const RadioGroup: ForwardRefExoticComponent< // @public export const RadioGroupDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-RadioGroup'; readonly content: 'bui-RadioGroupContent'; - readonly radio: 'bui-Radio'; + }; + readonly propDefs: { + readonly children: {}; + readonly className: {}; + readonly label: {}; + readonly secondaryLabel: {}; + readonly description: {}; + readonly isRequired: {}; }; }; // @public (undocumented) -export interface RadioGroupProps - extends Omit, - Omit { - // (undocumented) +export type RadioGroupOwnProps = { children?: ReactNode; -} + className?: string; + label?: FieldLabelProps['label']; + secondaryLabel?: FieldLabelProps['secondaryLabel']; + description?: FieldLabelProps['description']; + isRequired?: RadioGroupProps_2['isRequired']; +}; // @public (undocumented) -export interface RadioProps extends RadioProps_2 {} +export interface RadioGroupProps + extends RadioGroupOwnProps, + Omit {} + +// @public (undocumented) +export type RadioOwnProps = { + className?: string; +}; + +// @public (undocumented) +export interface RadioProps + extends RadioOwnProps, + Omit {} // @public (undocumented) export type Responsive = T | Partial>; @@ -1672,6 +1983,18 @@ export interface RowConfig { onClick?: (item: T) => void; } +// @public (undocumented) +export type RowOwnProps = { + columns?: RowProps_2['columns']; + children?: RowProps_2['children']; + href?: string; +}; + +// @public (undocumented) +export interface RowProps + extends RowOwnProps, + Omit, keyof RowOwnProps> {} + // @public (undocumented) export type RowRenderFn = (params: { item: T; @@ -1685,6 +2008,9 @@ export const SearchField: ForwardRefExoticComponent< // @public export const SearchFieldDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-SearchField'; readonly clear: 'bui-SearchFieldClear'; @@ -1692,21 +2018,43 @@ export const SearchFieldDefinition: { readonly input: 'bui-SearchFieldInput'; readonly inputIcon: 'bui-SearchFieldInputIcon'; }; - readonly dataAttributes: { - readonly startCollapsed: readonly [true, false]; - readonly size: readonly ['small', 'medium']; + readonly bg: 'consumer'; + readonly propDefs: { + readonly startCollapsed: { + readonly dataAttribute: true; + readonly default: false; + }; + readonly size: { + readonly dataAttribute: true; + readonly default: 'small'; + }; + readonly className: {}; + readonly icon: {}; + readonly placeholder: { + readonly default: 'Search'; + }; + readonly label: {}; + readonly description: {}; + readonly secondaryLabel: {}; }; }; // @public (undocumented) -export interface SearchFieldProps - extends SearchFieldProps_2, - Omit { +export type SearchFieldOwnProps = { icon?: ReactNode | false; - placeholder?: string; size?: 'small' | 'medium' | Partial>; + placeholder?: string; startCollapsed?: boolean; -} + className?: string; + label?: FieldLabelProps['label']; + description?: FieldLabelProps['description']; + secondaryLabel?: FieldLabelProps['secondaryLabel']; +}; + +// @public (undocumented) +export interface SearchFieldProps + extends Omit, + SearchFieldOwnProps {} // @public (undocumented) export interface SearchState { @@ -1723,36 +2071,49 @@ export const Select: ForwardRefExoticComponent< // @public export const SelectDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Select'; readonly popover: 'bui-SelectPopover'; - readonly trigger: 'bui-SelectTrigger'; - readonly chevron: 'bui-SelectTriggerChevron'; - readonly value: 'bui-SelectValue'; - readonly list: 'bui-SelectList'; - readonly item: 'bui-SelectItem'; - readonly itemIndicator: 'bui-SelectItemIndicator'; - readonly itemLabel: 'bui-SelectItemLabel'; - readonly searchWrapper: 'bui-SelectSearchWrapper'; - readonly search: 'bui-SelectSearch'; - readonly searchClear: 'bui-SelectSearchClear'; - readonly noResults: 'bui-SelectNoResults'; }; - readonly dataAttributes: { - readonly size: readonly ['small', 'medium']; + readonly propDefs: { + readonly icon: {}; + readonly size: { + readonly dataAttribute: true; + readonly default: 'small'; + }; + readonly options: {}; + readonly searchable: {}; + readonly searchPlaceholder: {}; + readonly label: {}; + readonly secondaryLabel: {}; + readonly description: {}; + readonly isRequired: {}; + readonly className: {}; }; }; // @public (undocumented) -export interface SelectProps - extends SelectProps_2, - Omit { +export type SelectOwnProps = { icon?: ReactNode; + size?: 'small' | 'medium' | Partial>; options?: Array; searchable?: boolean; searchPlaceholder?: string; + label?: FieldLabelProps['label']; + secondaryLabel?: FieldLabelProps['secondaryLabel']; + description?: FieldLabelProps['description']; + isRequired?: boolean; + className?: string; +}; + +// @public (undocumented) +export interface SelectProps + extends SelectOwnProps, + Omit, keyof SelectOwnProps> { selectionMode?: T; - size?: 'small' | 'medium' | Partial>; } // @public (undocumented) @@ -1760,20 +2121,41 @@ export const Skeleton: (props: SkeletonProps) => JSX_2.Element; // @public export const SkeletonDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Skeleton'; }; + readonly propDefs: { + readonly width: { + readonly default: 80; + }; + readonly height: { + readonly default: 24; + }; + readonly rounded: { + readonly dataAttribute: true; + readonly default: false; + }; + readonly className: {}; + readonly style: {}; + }; }; // @public (undocumented) -export interface SkeletonProps extends ComponentProps<'div'> { - // (undocumented) - height?: number | string; - // (undocumented) - rounded?: boolean; - // (undocumented) +export type SkeletonOwnProps = { width?: number | string; -} + height?: number | string; + rounded?: boolean; + className?: string; + style?: React.CSSProperties; +}; + +// @public (undocumented) +export interface SkeletonProps + extends Omit, 'children' | 'className' | 'style'>, + SkeletonOwnProps {} // @public (undocumented) export type SortDescriptor = SortDescriptor_2; @@ -1822,16 +2204,29 @@ export const Switch: ForwardRefExoticComponent< // @public export const SwitchDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Switch'; readonly indicator: 'bui-SwitchIndicator'; }; + readonly propDefs: { + readonly label: {}; + readonly className: {}; + }; }; // @public (undocumented) -export interface SwitchProps extends SwitchProps_2 { +export type SwitchOwnProps = { label?: string; -} + className?: string; +}; + +// @public (undocumented) +export interface SwitchProps + extends Omit, + SwitchOwnProps {} // @public export const Tab: (props: TabProps) => JSX_2.Element; @@ -1844,31 +2239,26 @@ export const TableBody: ( props: TableBodyProps, ) => JSX_2.Element; +// @public (undocumented) +export type TableBodyOwnProps = {}; + +// @public (undocumented) +export interface TableBodyProps + extends TableBodyOwnProps, + Omit, keyof TableBodyOwnProps> {} + // @public export const TableDefinition: { - readonly classNames: { - readonly table: 'bui-Table'; - readonly header: 'bui-TableHeader'; - readonly body: 'bui-TableBody'; - readonly row: 'bui-TableRow'; - readonly head: 'bui-TableHead'; - readonly headContent: 'bui-TableHeadContent'; - readonly headSortButton: 'bui-TableHeadSortButton'; - readonly caption: 'bui-TableCaption'; - readonly cell: 'bui-TableCell'; - readonly cellContentWrapper: 'bui-TableCellContentWrapper'; - readonly cellContent: 'bui-TableCellContent'; - readonly cellIcon: 'bui-TableCellIcon'; - readonly cellProfileAvatar: 'bui-TableCellProfileAvatar'; - readonly cellProfileAvatarImage: 'bui-TableCellProfileAvatarImage'; - readonly cellProfileAvatarFallback: 'bui-TableCellProfileAvatarFallback'; - readonly cellProfileName: 'bui-TableCellProfileName'; - readonly cellProfileLink: 'bui-TableCellProfileLink'; - readonly headSelection: 'bui-TableHeadSelection'; - readonly cellSelection: 'bui-TableCellSelection'; + readonly styles: { + readonly [key: string]: string; }; - readonly dataAttributes: { - readonly stale: readonly [true, false]; + readonly classNames: { + readonly root: 'bui-Table'; + }; + readonly propDefs: { + readonly stale: { + readonly dataAttribute: true; + }; }; }; @@ -1877,6 +2267,17 @@ export const TableHeader: ( props: TableHeaderProps, ) => JSX_2.Element; +// @public (undocumented) +export type TableHeaderOwnProps = { + columns?: TableHeaderProps_2['columns']; + children?: TableHeaderProps_2['children']; +}; + +// @public (undocumented) +export interface TableHeaderProps + extends TableHeaderOwnProps, + Omit, keyof TableHeaderOwnProps> {} + // @public (undocumented) export interface TableItem { // (undocumented) @@ -1884,47 +2285,59 @@ export interface TableItem { } // @public -export function TablePagination(input: TablePaginationProps): JSX_2.Element; +export function TablePagination(props: TablePaginationProps): JSX_2.Element; // @public export const TablePaginationDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-TablePagination'; readonly left: 'bui-TablePaginationLeft'; readonly right: 'bui-TablePaginationRight'; readonly select: 'bui-TablePaginationSelect'; }; + readonly propDefs: { + readonly pageSize: {}; + readonly pageSizeOptions: { + readonly default: PageSizeOption[]; + }; + readonly offset: {}; + readonly totalCount: {}; + readonly hasNextPage: {}; + readonly hasPreviousPage: {}; + readonly onNextPage: {}; + readonly onPreviousPage: {}; + readonly onPageSizeChange: {}; + readonly showPageSizeOptions: { + readonly default: true; + }; + readonly getLabel: {}; + }; }; // @public (undocumented) -export interface TablePaginationProps { - // (undocumented) +export type TablePaginationOwnProps = { + pageSize: number; + pageSizeOptions?: number[] | PageSizeOption[]; + offset?: number; + totalCount?: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + onNextPage: () => void; + onPreviousPage: () => void; + onPageSizeChange?: (size: number) => void; + showPageSizeOptions?: boolean; getLabel?: (params: { pageSize: number; offset?: number; totalCount?: number; }) => string; - // (undocumented) - hasNextPage: boolean; - // (undocumented) - hasPreviousPage: boolean; - // (undocumented) - offset?: number; - // (undocumented) - onNextPage: () => void; - // (undocumented) - onPageSizeChange?: (size: number) => void; - // (undocumented) - onPreviousPage: () => void; - // (undocumented) - pageSize: number; - // (undocumented) - pageSizeOptions?: number[] | PageSizeOption[]; - // (undocumented) - showPageSizeOptions?: boolean; - // (undocumented) - totalCount?: number; -} +}; + +// @public (undocumented) +export interface TablePaginationProps extends TablePaginationOwnProps {} // @public (undocumented) export type TablePaginationType = NoPagination | PagePagination; @@ -1961,10 +2374,14 @@ export interface TableProps { export const TableRoot: (props: TableRootProps) => JSX_2.Element; // @public (undocumented) -export interface TableRootProps extends TableProps_2 { - // (undocumented) +export type TableRootOwnProps = { stale?: boolean; -} +}; + +// @public (undocumented) +export interface TableRootProps + extends TableRootOwnProps, + Omit {} // @public (undocumented) export interface TableSelection { @@ -1982,40 +2399,72 @@ export interface TableSelection { export const TabList: (props: TabListProps) => JSX_2.Element; // @public -export interface TabListProps extends Omit, 'items'> {} +export type TabListOwnProps = { + className?: string; + children?: TabListProps_2['children']; +}; + +// @public +export interface TabListProps + extends TabListOwnProps, + Omit, 'items' | keyof TabListOwnProps> {} // @public export type TabMatchStrategy = 'exact' | 'prefix'; +// @public +export type TabOwnProps = { + className?: string; + matchStrategy?: TabMatchStrategy; + href?: TabProps_2['href']; + id?: TabProps_2['id']; +}; + // @public export const TabPanel: (props: TabPanelProps) => JSX_2.Element; // @public -export interface TabPanelProps extends TabPanelProps_2 {} +export type TabPanelOwnProps = { + className?: string; +}; // @public -export interface TabProps extends TabProps_2 { - matchStrategy?: 'exact' | 'prefix'; -} +export interface TabPanelProps + extends TabPanelOwnProps, + Omit {} + +// @public +export interface TabProps + extends TabOwnProps, + Omit {} // @public export const Tabs: (props: TabsProps) => JSX_2.Element | null; // @public export const TabsDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { - readonly tabs: 'bui-Tabs'; - readonly tabList: 'bui-TabList'; - readonly tabListWrapper: 'bui-TabListWrapper'; - readonly tab: 'bui-Tab'; - readonly tabActive: 'bui-TabActive'; - readonly tabHovered: 'bui-TabHovered'; - readonly panel: 'bui-TabPanel'; + readonly root: 'bui-Tabs'; + }; + readonly propDefs: { + readonly className: {}; + readonly children: {}; }; }; // @public -export interface TabsProps extends TabsProps_2 {} +export type TabsOwnProps = { + className?: string; + children?: TabsProps_2['children']; +}; + +// @public +export interface TabsProps + extends TabsOwnProps, + Omit {} // @public export const Tag: ForwardRefExoticComponent< @@ -2029,25 +2478,47 @@ export const TagGroup: ( // @public export const TagGroupDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { - readonly group: 'bui-TagGroup'; + readonly root: 'bui-TagGroup'; readonly list: 'bui-TagList'; - readonly tag: 'bui-Tag'; - readonly tagIcon: 'bui-TagIcon'; - readonly tagRemoveButton: 'bui-TagRemoveButton'; + }; + readonly propDefs: { + readonly items: {}; + readonly children: {}; + readonly renderEmptyState: {}; + readonly className: {}; }; }; // @public -export interface TagGroupProps - extends Omit, - Pick, 'items' | 'children' | 'renderEmptyState'> {} +export type TagGroupOwnProps = { + items?: TagListProps['items']; + children?: TagListProps['children']; + renderEmptyState?: TagListProps['renderEmptyState']; + className?: string; +}; // @public -export interface TagProps extends TagProps_2 { +export interface TagGroupProps + extends TagGroupOwnProps, + Omit {} + +// @public +export type TagOwnProps = { icon?: React.ReactNode; size?: 'small' | 'medium'; -} + href?: TagProps_2['href']; + children?: TagProps_2['children']; + className?: string; +}; + +// @public +export interface TagProps + extends TagOwnProps, + Omit {} // @public (undocumented) const Text_2: { @@ -2068,21 +2539,32 @@ export type TextColorStatus = 'danger' | 'warning' | 'success' | 'info'; // @public export const TextDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-Text'; }; - readonly dataAttributes: { - readonly variant: readonly ['subtitle', 'body', 'caption', 'label']; - readonly weight: readonly ['regular', 'bold']; - readonly color: readonly [ - 'primary', - 'secondary', - 'danger', - 'warning', - 'success', - 'info', - ]; - readonly truncate: readonly [true, false]; + readonly propDefs: { + readonly as: { + readonly default: 'span'; + }; + readonly variant: { + readonly dataAttribute: true; + readonly default: 'body-medium'; + }; + readonly weight: { + readonly dataAttribute: true; + readonly default: 'regular'; + }; + readonly color: { + readonly dataAttribute: true; + readonly default: 'primary'; + }; + readonly truncate: { + readonly dataAttribute: true; + }; + readonly className: {}; }; }; @@ -2093,6 +2575,9 @@ export const TextField: ForwardRefExoticComponent< // @public export const TextFieldDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-TextField'; readonly inputWrapper: 'bui-InputWrapper'; @@ -2100,20 +2585,36 @@ export const TextFieldDefinition: { readonly inputIcon: 'bui-InputIcon'; readonly inputAction: 'bui-InputAction'; }; - readonly dataAttributes: { - readonly invalid: readonly [true, false]; - readonly disabled: readonly [true, false]; - readonly size: readonly ['small', 'medium']; + readonly bg: 'consumer'; + readonly propDefs: { + readonly size: { + readonly dataAttribute: true; + readonly default: 'small'; + }; + readonly className: {}; + readonly icon: {}; + readonly placeholder: {}; + readonly label: {}; + readonly description: {}; + readonly secondaryLabel: {}; }; }; // @public (undocumented) -export interface TextFieldProps - extends TextFieldProps_2, - Omit { +export type TextFieldOwnProps = { + size?: 'small' | 'medium' | Partial>; + className?: string; icon?: ReactNode; placeholder?: string; - size?: 'small' | 'medium' | Partial>; + label?: FieldLabelProps['label']; + description?: FieldLabelProps['description']; + secondaryLabel?: FieldLabelProps['secondaryLabel']; +}; + +// @public (undocumented) +export interface TextFieldProps + extends Omit, + TextFieldOwnProps { type?: 'text' | 'email' | 'tel' | 'url'; } @@ -2141,6 +2642,7 @@ export type TextOwnProps = { | TextColorStatus | Partial>; truncate?: boolean; + className?: string; }; // @public (undocumented) @@ -2168,12 +2670,22 @@ export const ToggleButton: ForwardRefExoticComponent< // @public export const ToggleButtonDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-ToggleButton'; readonly content: 'bui-ToggleButtonContent'; }; - readonly dataAttributes: { - readonly size: readonly ['small', 'medium']; + readonly propDefs: { + readonly size: { + readonly dataAttribute: true; + readonly default: 'small'; + }; + readonly iconStart: {}; + readonly iconEnd: {}; + readonly children: {}; + readonly className: {}; }; }; @@ -2184,30 +2696,42 @@ export const ToggleButtonGroup: ForwardRefExoticComponent< // @public export const ToggleButtonGroupDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-ToggleButtonGroup'; }; - readonly dataAttributes: { - readonly orientation: readonly ['horizontal', 'vertical']; + readonly propDefs: { + readonly className: {}; + readonly children: {}; }; }; +// @public (undocumented) +export type ToggleButtonGroupOwnProps = { + className?: string; + children?: ReactNode; +}; + // @public (undocumented) export interface ToggleButtonGroupProps - extends Omit { - // (undocumented) - orientation?: NonNullable; -} + extends Omit, + ToggleButtonGroupOwnProps {} + +// @public (undocumented) +export type ToggleButtonOwnProps = { + size?: 'small' | 'medium' | Partial>; + iconStart?: ReactElement; + iconEnd?: ReactElement; + children?: ToggleButtonProps_2['children']; + className?: string; +}; // @public -export interface ToggleButtonProps extends ToggleButtonProps_2 { - // (undocumented) - iconEnd?: ReactElement; - // (undocumented) - iconStart?: ReactElement; - // (undocumented) - size?: 'small' | 'medium' | Partial>; -} +export interface ToggleButtonProps + extends Omit, + ToggleButtonOwnProps {} // @public (undocumented) export const Tooltip: ForwardRefExoticComponent< @@ -2216,17 +2740,30 @@ export const Tooltip: ForwardRefExoticComponent< // @public export const TooltipDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly tooltip: 'bui-Tooltip'; + readonly content: 'bui-TooltipContent'; readonly arrow: 'bui-TooltipArrow'; }; + readonly propDefs: { + readonly children: {}; + readonly className: {}; + }; }; // @public (undocumented) -export interface TooltipProps extends Omit { - // (undocumented) +export type TooltipOwnProps = { children: React.ReactNode; -} + className?: string; +}; + +// @public (undocumented) +export interface TooltipProps + extends Omit, + TooltipOwnProps {} // @public (undocumented) export const TooltipTrigger: ( @@ -2350,14 +2887,24 @@ export const VisuallyHidden: (props: VisuallyHiddenProps) => JSX_2.Element; // @public export const VisuallyHiddenDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { readonly root: 'bui-VisuallyHidden'; }; + readonly propDefs: { + readonly className: {}; + }; +}; + +// @public (undocumented) +export type VisuallyHiddenOwnProps = { + className?: string; }; // @public -export interface VisuallyHiddenProps extends ComponentProps<'div'> { - // (undocumented) - children?: React.ReactNode; -} +export interface VisuallyHiddenProps + extends Omit, 'className'>, + VisuallyHiddenOwnProps {} ``` diff --git a/packages/ui/src/components/Accordion/Accordion.stories.tsx b/packages/ui/src/components/Accordion/Accordion.stories.tsx index b88b547e08..5def4f57d3 100644 --- a/packages/ui/src/components/Accordion/Accordion.stories.tsx +++ b/packages/ui/src/components/Accordion/Accordion.stories.tsx @@ -195,7 +195,7 @@ export const AutoBg = meta.story({ - + Neutral 1 container @@ -210,35 +210,41 @@ export const AutoBg = meta.story({ - - Neutral 2 container - - - - - - - - - - - - + + + Neutral 2 container + + + + + + + + + + + + + - - Neutral 3 container - - - - - - - - - - - - + + + + Neutral 3 container + + + + + + + + + + + + + + ), diff --git a/packages/ui/src/components/Accordion/definition.ts b/packages/ui/src/components/Accordion/definition.ts index 69ef9be3a7..2e76f0cd77 100644 --- a/packages/ui/src/components/Accordion/definition.ts +++ b/packages/ui/src/components/Accordion/definition.ts @@ -34,7 +34,7 @@ export const AccordionDefinition = defineComponent()({ }, bg: 'provider', propDefs: { - bg: { dataAttribute: true, default: 'neutral-auto' }, + bg: { dataAttribute: true, default: 'neutral' }, children: {}, className: {}, }, diff --git a/packages/ui/src/components/Alert/Alert.stories.tsx b/packages/ui/src/components/Alert/Alert.stories.tsx index 28239becb9..589d55b7dd 100644 --- a/packages/ui/src/components/Alert/Alert.stories.tsx +++ b/packages/ui/src/components/Alert/Alert.stories.tsx @@ -297,7 +297,7 @@ export const OnDifferentBackgrounds = meta.story({ On Neutral 1 - + @@ -305,18 +305,24 @@ export const OnDifferentBackgrounds = meta.story({ On Neutral 2 - - - - + + + + + + On Neutral 3 - - - - + + + + + + + + ), @@ -337,7 +343,7 @@ export const Responsive = meta.story({ export const WithUtilityProps = meta.story({ render: () => ( - + , + keyof AlertOwnProps | keyof MarginProps + > {} diff --git a/packages/ui/src/components/Avatar/Avatar.tsx b/packages/ui/src/components/Avatar/Avatar.tsx index 920a9b127f..e1694d738a 100644 --- a/packages/ui/src/components/Avatar/Avatar.tsx +++ b/packages/ui/src/components/Avatar/Avatar.tsx @@ -15,24 +15,18 @@ */ import { forwardRef, useState, useEffect } from 'react'; -import clsx from 'clsx'; import { AvatarProps } from './types'; -import { useStyles } from '../../hooks/useStyles'; +import { useDefinition } from '../../hooks/useDefinition'; import { AvatarDefinition } from './definition'; -import styles from './Avatar.module.css'; /** @public */ export const Avatar = forwardRef((props, ref) => { - const { classNames, dataAttributes, cleanedProps } = useStyles( + const { ownProps, restProps, dataAttributes } = useDefinition( AvatarDefinition, - { - size: 'medium', - purpose: 'informative', - ...props, - }, + props, ); - const { className, src, name, purpose, ...rest } = cleanedProps; + const { classes, size, src, name, purpose } = ownProps; const [imageStatus, setImageStatus] = useState< 'loading' | 'loaded' | 'error' @@ -51,9 +45,7 @@ export const Avatar = forwardRef((props, ref) => { }; }, [src]); - const initialsCount = ['x-small', 'small'].includes(cleanedProps.size) - ? 1 - : 2; + const initialsCount = ['x-small', 'small'].includes(size) ? 1 : 2; const initials = name .split(' ') @@ -68,21 +60,14 @@ export const Avatar = forwardRef((props, ref) => { role="img" aria-label={purpose === 'informative' ? name : undefined} aria-hidden={purpose === 'decoration' ? true : undefined} - className={clsx(classNames.root, styles[classNames.root], className)} + className={classes.root} {...dataAttributes} - {...rest} + {...restProps} > {imageStatus === 'loaded' ? ( - + ) : ( - - + Neutral 1 container - - Neutral 2 container - - - - + + + Neutral 2 container + + + + + - - Neutral 3 container - - - - + + + + Neutral 3 container + + + + + + ), diff --git a/packages/ui/src/components/Button/definition.ts b/packages/ui/src/components/Button/definition.ts index d15ceabd77..21d3c7017b 100644 --- a/packages/ui/src/components/Button/definition.ts +++ b/packages/ui/src/components/Button/definition.ts @@ -39,6 +39,5 @@ export const ButtonDefinition = defineComponent()({ iconEnd: {}, children: {}, className: {}, - style: {}, }, }); diff --git a/packages/ui/src/components/Button/types.ts b/packages/ui/src/components/Button/types.ts index a18d9e9164..2bf36d808e 100644 --- a/packages/ui/src/components/Button/types.ts +++ b/packages/ui/src/components/Button/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { ReactElement, ReactNode, CSSProperties } from 'react'; +import type { ReactElement, ReactNode } from 'react'; import type { ButtonProps as RAButtonProps } from 'react-aria-components'; import type { Responsive } from '../../types'; @@ -28,7 +28,6 @@ export type ButtonOwnProps = { loading?: boolean; children?: ReactNode; className?: string; - style?: CSSProperties; }; /** @@ -37,5 +36,5 @@ export type ButtonOwnProps = { * @public */ export interface ButtonProps - extends Omit, + extends Omit, ButtonOwnProps {} diff --git a/packages/ui/src/components/ButtonIcon/definition.ts b/packages/ui/src/components/ButtonIcon/definition.ts index 76f6be68ce..0027227094 100644 --- a/packages/ui/src/components/ButtonIcon/definition.ts +++ b/packages/ui/src/components/ButtonIcon/definition.ts @@ -36,6 +36,5 @@ export const ButtonIconDefinition = defineComponent()({ loading: { dataAttribute: true }, icon: {}, className: {}, - style: {}, }, }); diff --git a/packages/ui/src/components/ButtonIcon/types.ts b/packages/ui/src/components/ButtonIcon/types.ts index 92618d5ff6..f30f01ff99 100644 --- a/packages/ui/src/components/ButtonIcon/types.ts +++ b/packages/ui/src/components/ButtonIcon/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { ReactElement, CSSProperties } from 'react'; +import type { ReactElement } from 'react'; import type { ButtonProps as RAButtonProps } from 'react-aria-components'; import type { Responsive } from '../../types'; @@ -25,7 +25,6 @@ export type ButtonIconOwnProps = { icon?: ReactElement; loading?: boolean; className?: string; - style?: CSSProperties; }; /** @@ -34,5 +33,5 @@ export type ButtonIconOwnProps = { * @public */ export interface ButtonIconProps - extends Omit, + extends Omit, ButtonIconOwnProps {} diff --git a/packages/ui/src/components/ButtonLink/definition.ts b/packages/ui/src/components/ButtonLink/definition.ts index c86cb026b5..ea271a966e 100644 --- a/packages/ui/src/components/ButtonLink/definition.ts +++ b/packages/ui/src/components/ButtonLink/definition.ts @@ -36,6 +36,5 @@ export const ButtonLinkDefinition = defineComponent()({ iconEnd: {}, children: {}, className: {}, - style: {}, }, }); diff --git a/packages/ui/src/components/ButtonLink/types.ts b/packages/ui/src/components/ButtonLink/types.ts index 0f01c3cf40..f2753eafe5 100644 --- a/packages/ui/src/components/ButtonLink/types.ts +++ b/packages/ui/src/components/ButtonLink/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { ReactElement, ReactNode, CSSProperties } from 'react'; +import type { ReactElement, ReactNode } from 'react'; import type { LinkProps as RALinkProps } from 'react-aria-components'; import type { Responsive } from '../../types'; @@ -26,7 +26,6 @@ export type ButtonLinkOwnProps = { iconEnd?: ReactElement; children?: ReactNode; className?: string; - style?: CSSProperties; }; /** @@ -35,5 +34,5 @@ export type ButtonLinkOwnProps = { * @public */ export interface ButtonLinkProps - extends Omit, + extends Omit, ButtonLinkOwnProps {} diff --git a/packages/ui/src/components/Card/Card.module.css b/packages/ui/src/components/Card/Card.module.css index 445873f6ba..f0aaa6831f 100644 --- a/packages/ui/src/components/Card/Card.module.css +++ b/packages/ui/src/components/Card/Card.module.css @@ -20,13 +20,127 @@ .bui-Card { display: flex; flex-direction: column; - gap: var(--bui-space-3); border-radius: var(--bui-radius-3); - padding-block: var(--bui-space-3); color: var(--bui-fg-primary); - overflow: hidden; + overflow: auto; min-height: 0; width: 100%; + position: relative; + padding: var(--bui-space-3); + } + + .bui-Card[data-bg='neutral-1'] { + --bui-card-bg: var(--bui-bg-neutral-1); + } + + .bui-Card[data-bg='neutral-2'] { + --bui-card-bg: var(--bui-bg-neutral-2); + } + + .bui-Card[data-bg='neutral-3'] { + --bui-card-bg: var(--bui-bg-neutral-3); + } + + .bui-Card:has(.bui-CardHeader, .bui-CardBody, .bui-CardFooter) { + padding: 0; + } + + /* + * Cursor and hover tint are applied at the card level so they cover the + * entire surface. The overlay inherits the cursor via cursor: inherit. + */ + .bui-Card[data-interactive] { + cursor: pointer; + overflow: hidden; + + &::after { + content: ''; + position: absolute; + inset: 0; + background: color-mix(in srgb, currentColor 5%, transparent); + border-radius: inherit; + pointer-events: none; + z-index: 3; + opacity: 0; + transition: opacity 200ms ease-in-out; + } + + &:hover::after { + opacity: 1; + } + } + + .bui-CardOverlay { + position: absolute; + inset: 0; + z-index: 1; + background: transparent; + border-radius: inherit; + border: none; + padding: 0; + appearance: none; + display: block; + width: 100%; + cursor: inherit; + + &:focus-visible { + outline: 2px solid var(--bui-ring); + outline-offset: -2px; + } + + /* + * Keep focus tint for keyboard navigation (hover tint has moved to the + * card container above). + */ + &[data-focused]::after { + content: ''; + position: absolute; + inset: 0; + background: color-mix(in srgb, currentColor 5%, transparent); + border-radius: inherit; + pointer-events: none; + } + } + + /* + * Nested interactive elements must sit above the overlay (z-index: 1) so + * that buttons, links, and inputs remain independently clickable. + * CardBody is intentionally excluded: it sits beneath the overlay so that + * card-surface clicks route through the overlay natively (preserving link + * semantics such as target and rel). Scroll is not supported for interactive + * cards as a result. + */ + .bui-Card[data-interactive] + :is( + button, + a[href], + [role='button'], + [role='link'], + input, + select, + textarea, + .bui-Button + ):not(.bui-CardOverlay) { + position: relative; + z-index: 2; + } + + /* + * The bottom scroll-shadow pseudo-element uses a reversed scroll-driven + * animation whose fill-before state is opacity: 1. When the card has no + * height constraint (nothing to scroll), the animation is permanently stuck + * in that fill-before state, making the shadow visible even though there is + * no overflow. Interactive cards never scroll, so we suppress it entirely. + * The selector mirrors .bui-Card:has(.bui-CardFooter) .bui-CardBody::after + * with an added attribute to win the specificity race. + */ + .bui-Card[data-interactive]:has(.bui-CardFooter) .bui-CardBody::after { + display: none; + } + + .bui-CardHeader { + padding-inline: var(--bui-space-3); + padding-block: var(--bui-space-3); } .bui-CardBody { @@ -34,13 +148,65 @@ min-height: 0; overflow: auto; padding-inline: var(--bui-space-3); + padding-block: var(--bui-space-3); } - .bui-CardHeader { - padding-inline: var(--bui-space-3); + @keyframes bui-card-body-shadow { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + .bui-Card:has(.bui-CardHeader) .bui-CardBody { + padding-block-start: 0; + + &::before { + content: ''; + position: sticky; + top: 0; + display: block; + height: 1.5rem; + margin-bottom: -1.5rem; + background: linear-gradient( + to bottom, + var(--bui-card-bg), + rgb(from var(--bui-card-bg) r g b / 0) + ); + pointer-events: none; + opacity: 0; + animation: bui-card-body-shadow linear both; + animation-timeline: scroll(); + animation-range: 0px 2.5rem; + } + } + + .bui-Card:has(.bui-CardFooter) .bui-CardBody { + padding-block-end: 0; + + &::after { + content: ''; + position: sticky; + bottom: 0; + display: block; + height: 1.5rem; + margin-top: -1.5rem; + background: linear-gradient( + to top, + var(--bui-card-bg), + rgb(from var(--bui-card-bg) r g b / 0) + ); + pointer-events: none; + animation: bui-card-body-shadow linear both reverse; + animation-timeline: scroll(); + animation-range: calc(100% - 2.5rem) 100%; + } } .bui-CardFooter { padding-inline: var(--bui-space-3); + padding-block: var(--bui-space-3); } } diff --git a/packages/ui/src/components/Card/Card.stories.tsx b/packages/ui/src/components/Card/Card.stories.tsx index 3957b7beff..117859a1bb 100644 --- a/packages/ui/src/components/Card/Card.stories.tsx +++ b/packages/ui/src/components/Card/Card.stories.tsx @@ -27,49 +27,64 @@ const meta = preview.meta({ }); export const Default = meta.story({ + render: args => Hello world, +}); + +export const DefaultWithHeader = meta.story({ render: args => ( Header Body - Footer ), }); -export const CustomSize = Default.extend({ - args: { - style: { - width: '300px', - height: '200px', - }, - }, +const content = ( + <> + + This is the first paragraph of a long body text that demonstrates how the + Card component handles extensive content. The card should adjust + accordingly to display all the text properly while maintaining its + structure. + + + Here's a second paragraph that adds more content to our card body. Having + multiple paragraphs helps to visualize how spacing works within the card + component. + + + This third paragraph continues to add more text to ensure we have a proper + demonstration of a card with significant content. This makes it easier to + test scrolling behavior and overall layout when content exceeds the + initial view. + + +); + +export const LongBody = meta.story({ + render: () => ( + {content} + ), }); -export const WithLongBody = meta.story({ +export const LongBodyHeader = meta.story({ render: () => ( Header - - - This is the first paragraph of a long body text that demonstrates how - the Card component handles extensive content. The card should adjust - accordingly to display all the text properly while maintaining its - structure. - - - Here's a second paragraph that adds more content to our card body. - Having multiple paragraphs helps to visualize how spacing works within - the card component. - - - This third paragraph continues to add more text to ensure we have a - proper demonstration of a card with significant content. This makes it - easier to test scrolling behavior and overall layout when content - exceeds the initial view. - - + {content} + + ), +}); + +export const LongBodyHeaderFooter = meta.story({ + render: () => ( + + + Header + + {content} Footer @@ -77,7 +92,7 @@ export const WithLongBody = meta.story({ ), }); -const ListRow = ({ children }: { children: React.ReactNode }) => { +const ListRowComponent = ({ children }: { children: React.ReactNode }) => { return (
    { paddingInline: 'var(--bui-space-3)', borderRadius: 'var(--bui-radius-2)', fontSize: 'var(--bui-font-size-3)', - marginBottom: 'var(--bui-space-1)', }} > {children} @@ -97,29 +111,76 @@ const ListRow = ({ children }: { children: React.ReactNode }) => { ); }; -export const WithListRow = meta.story({ +const listRowContent = ( + + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + +); + +export const ListRow = meta.story({ + render: () => ( + + {listRowContent} + + ), +}); + +export const ListRowHeader = meta.story({ + render: () => ( + + + Header + + {listRowContent} + + ), +}); + +export const ListRowFooter = meta.story({ + render: () => ( + + {listRowContent} + + Footer + + + ), +}); + +export const ListRowHeaderFooter = meta.story({ render: () => ( Header - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world - Hello world + + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Hello world + Footer @@ -135,23 +196,29 @@ export const Backgrounds = meta.story({ No parent Defaults to neutral-1 - + On neutral-1 Auto-increments to neutral-2 - - - On neutral-2 - Auto-increments to neutral-3 - + + + + On neutral-2 + Auto-increments to neutral-3 + + - - - On neutral-3 - Steps up to neutral-4 - + + + + + On neutral-3 + Steps up to neutral-4 + + + ), @@ -197,28 +264,119 @@ export const BgOnProviders = meta.story({ No provider Card defaults to neutral-1 - + On neutral-1 Card auto-increments to neutral-2 - - - On neutral-2 - Card auto-increments to neutral-3 - + + + + On neutral-2 + Card auto-increments to neutral-3 + + - - - On neutral-3 - Card visually at neutral-4 - + + + + + On neutral-3 + Card visually at neutral-4 + + + ), }); +export const Interactive = meta.story({ + render: () => ( + alert('Card pressed')} + label="View component details" + > + + Interactive Card + + + + Click anywhere on this card to trigger the press handler. The entire + card surface is interactive. + + + + + Click to interact + + + + ), +}); + +export const InteractiveAsLink = meta.story({ + render: () => ( + + Link Card + + + This card navigates to a URL when clicked. The entire card surface + acts as a link. + + + Opens backstage.io + + ), +}); + +export const InteractiveWithNestedButtons = meta.story({ + render: () => ( + alert('Card pressed')} + label="View plugin details" + > + + Card with Actions + + + + Clicking the card background triggers the card press handler. The + buttons below remain independently interactive. + + + + + + + + + + ), +}); + export const CustomCardWithBox = meta.story({ render: () => ( @@ -226,14 +384,8 @@ export const CustomCardWithBox = meta.story({ A custom card built with Box. Use Box with an explicit bg prop to create a card-like container that participates in the bg system as a provider. - - + + Header diff --git a/packages/ui/src/components/Card/Card.tsx b/packages/ui/src/components/Card/Card.tsx index e3e4781eac..2527a3a46b 100644 --- a/packages/ui/src/components/Card/Card.tsx +++ b/packages/ui/src/components/Card/Card.tsx @@ -15,6 +15,7 @@ */ import { forwardRef } from 'react'; +import { Button as RAButton, Link as RALink } from 'react-aria-components'; import { useDefinition } from '../../hooks/useDefinition'; import { CardDefinition, @@ -40,16 +41,36 @@ export const Card = forwardRef((props, ref) => { CardDefinition, props, ); - const { classes, children } = ownProps; + const { classes, children, onPress, href, label, target, rel, download } = + ownProps; + const isInteractive = !!(onPress || href); return ( + {href && ( + + )} + {onPress && !href && ( + + )} {children} ); diff --git a/packages/ui/src/components/Card/definition.ts b/packages/ui/src/components/Card/definition.ts index 6493d01839..3408828a4d 100644 --- a/packages/ui/src/components/Card/definition.ts +++ b/packages/ui/src/components/Card/definition.ts @@ -31,10 +31,17 @@ export const CardDefinition = defineComponent()({ styles, classNames: { root: 'bui-Card', + overlay: 'bui-CardOverlay', }, propDefs: { children: {}, className: {}, + onPress: {}, + href: {}, + label: {}, + target: {}, + rel: {}, + download: {}, }, }); diff --git a/packages/ui/src/components/Card/types.ts b/packages/ui/src/components/Card/types.ts index e31b2cfda2..f1e91055e9 100644 --- a/packages/ui/src/components/Card/types.ts +++ b/packages/ui/src/components/Card/types.ts @@ -15,11 +15,46 @@ */ import type { ReactNode } from 'react'; +import type { ButtonProps as RAButtonProps } from 'react-aria-components'; /** @public */ -export type CardOwnProps = { - children?: ReactNode; - className?: string; +export type CardBaseProps = { children?: ReactNode; className?: string }; + +/** @public */ +export type CardButtonVariant = { + /** Handler called when the card is pressed. Makes the card interactive as a button. */ + onPress: NonNullable; + href?: never; + /** Accessible label announced by screen readers for the interactive card. */ + label: string; + target?: never; + rel?: never; + download?: never; +}; + +/** @public */ +export type CardLinkVariant = { + /** URL to navigate to. Makes the card interactive as a link. */ + href: string; + onPress?: never; + /** Accessible label announced by screen readers for the interactive card. */ + label: string; + /** Specifies where to open the linked URL (e.g. `_blank` for a new tab). */ + target?: string; + /** Relationship between the current document and the linked URL (e.g. `noopener`). */ + rel?: string; + /** Prompts the user to save the linked URL. Pass `true` for default filename or a string for a custom filename. */ + download?: boolean | string; +}; + +/** @public */ +export type CardStaticVariant = { + onPress?: never; + href?: never; + label?: never; + target?: never; + rel?: never; + download?: never; }; /** @@ -27,9 +62,26 @@ export type CardOwnProps = { * * @public */ -export interface CardProps - extends CardOwnProps, - React.HTMLAttributes {} +export type CardProps = CardBaseProps & + Omit, 'onPress'> & + (CardButtonVariant | CardLinkVariant | CardStaticVariant); + +/** + * Flat own-props shape used by the component definition system. + * Derived from the Card variant types so it automatically stays in sync with CardProps. + * @public + */ +export type CardOwnProps = Pick< + CardBaseProps & (CardButtonVariant | CardLinkVariant | CardStaticVariant), + | 'children' + | 'className' + | 'onPress' + | 'href' + | 'label' + | 'target' + | 'rel' + | 'download' +>; /** @public */ export type CardHeaderOwnProps = { diff --git a/packages/ui/src/components/Checkbox/Checkbox.tsx b/packages/ui/src/components/Checkbox/Checkbox.tsx index 6030fbc06b..a825ccfb8a 100644 --- a/packages/ui/src/components/Checkbox/Checkbox.tsx +++ b/packages/ui/src/components/Checkbox/Checkbox.tsx @@ -17,32 +17,29 @@ import { forwardRef } from 'react'; import { Checkbox as RACheckbox } from 'react-aria-components'; import type { CheckboxProps } from './types'; -import { useStyles } from '../../hooks/useStyles'; +import { useDefinition } from '../../hooks/useDefinition'; import { CheckboxDefinition } from './definition'; -import clsx from 'clsx'; -import styles from './Checkbox.module.css'; import { RiCheckLine, RiSubtractLine } from '@remixicon/react'; /** @public */ export const Checkbox = forwardRef( (props, ref) => { - const { classNames } = useStyles(CheckboxDefinition); - const { className, children, ...rest } = props; + const { ownProps, restProps, dataAttributes } = useDefinition( + CheckboxDefinition, + props, + ); + const { classes, children } = ownProps; return ( {({ isIndeterminate }) => ( <> -
    +
    {isIndeterminate ? ( ) : ( diff --git a/packages/ui/src/components/Checkbox/definition.ts b/packages/ui/src/components/Checkbox/definition.ts index c7b5b16276..5aa2473eac 100644 --- a/packages/ui/src/components/Checkbox/definition.ts +++ b/packages/ui/src/components/Checkbox/definition.ts @@ -14,19 +14,24 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { CheckboxOwnProps } from './types'; +import styles from './Checkbox.module.css'; /** * Component definition for Checkbox * @public */ -export const CheckboxDefinition = { +export const CheckboxDefinition = defineComponent()({ + styles, classNames: { root: 'bui-Checkbox', indicator: 'bui-CheckboxIndicator', }, - dataAttributes: { - selected: [true, false] as const, - indeterminate: [true, false] as const, + propDefs: { + selected: { dataAttribute: true }, + indeterminate: { dataAttribute: true }, + children: {}, + className: {}, }, -} as const satisfies ComponentDefinition; +}); diff --git a/packages/ui/src/components/Checkbox/index.ts b/packages/ui/src/components/Checkbox/index.ts index 7c0cb5b414..bd20736c41 100644 --- a/packages/ui/src/components/Checkbox/index.ts +++ b/packages/ui/src/components/Checkbox/index.ts @@ -15,4 +15,4 @@ */ export { Checkbox } from './Checkbox'; export { CheckboxDefinition } from './definition'; -export type { CheckboxProps } from './types'; +export type { CheckboxOwnProps, CheckboxProps } from './types'; diff --git a/packages/ui/src/components/Checkbox/types.ts b/packages/ui/src/components/Checkbox/types.ts index 86e4b620b8..8507df1e5b 100644 --- a/packages/ui/src/components/Checkbox/types.ts +++ b/packages/ui/src/components/Checkbox/types.ts @@ -13,9 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { CheckboxProps as RACheckboxProps } from 'react-aria-components'; +import type { CheckboxProps as RACheckboxProps } from 'react-aria-components'; /** @public */ -export interface CheckboxProps extends RACheckboxProps { +export type CheckboxOwnProps = { + selected?: boolean; + indeterminate?: boolean; children: React.ReactNode; -} + className?: string; +}; + +/** @public */ +export interface CheckboxProps + extends Omit, + CheckboxOwnProps {} diff --git a/packages/ui/src/components/Container/Container.module.css b/packages/ui/src/components/Container/Container.module.css index a0dec1e581..b93665525f 100644 --- a/packages/ui/src/components/Container/Container.module.css +++ b/packages/ui/src/components/Container/Container.module.css @@ -21,7 +21,7 @@ max-width: 120rem; padding-inline: var(--bui-space-4); margin-inline: auto; - transition: padding 0.2s ease-in-out; + padding-bottom: var(--bui-space-8); } @media (min-width: 640px) { diff --git a/packages/ui/src/components/Container/Container.tsx b/packages/ui/src/components/Container/Container.tsx index 7e3627f0b9..743704b00e 100644 --- a/packages/ui/src/components/Container/Container.tsx +++ b/packages/ui/src/components/Container/Container.tsx @@ -15,34 +15,30 @@ */ import { forwardRef } from 'react'; -import { ContainerProps } from './types'; -import clsx from 'clsx'; -import { useStyles } from '../../hooks/useStyles'; +import type { ContainerProps } from './types'; +import { useDefinition } from '../../hooks/useDefinition'; import { ContainerDefinition } from './definition'; -import styles from './Container.module.css'; /** @public */ export const Container = forwardRef( (props, ref) => { - const { classNames, utilityClasses, style, cleanedProps } = useStyles( + const { ownProps, restProps, utilityStyle } = useDefinition( ContainerDefinition, props, ); - - const { className, ...rest } = cleanedProps; + const { classes, children, style } = ownProps; return (
    + className={classes.root} + style={{ ...utilityStyle, ...style }} + {...restProps} + > + {children} +
    ); }, ); + +Container.displayName = 'Container'; diff --git a/packages/ui/src/components/Container/definition.ts b/packages/ui/src/components/Container/definition.ts index 330e6adf2f..e9d7ce3238 100644 --- a/packages/ui/src/components/Container/definition.ts +++ b/packages/ui/src/components/Container/definition.ts @@ -14,15 +14,23 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { ContainerOwnProps } from './types'; +import styles from './Container.module.css'; /** * Component definition for Container * @public */ -export const ContainerDefinition = { +export const ContainerDefinition = defineComponent()({ + styles, classNames: { root: 'bui-Container', }, + propDefs: { + children: {}, + className: {}, + style: {}, + }, utilityProps: ['my', 'mt', 'mb', 'py', 'pt', 'pb', 'display'], -} as const satisfies ComponentDefinition; +}); diff --git a/packages/ui/src/components/Container/index.tsx b/packages/ui/src/components/Container/index.tsx index 73d1a0ab6e..d7e8eed01e 100644 --- a/packages/ui/src/components/Container/index.tsx +++ b/packages/ui/src/components/Container/index.tsx @@ -15,4 +15,4 @@ */ export { Container } from './Container'; export { ContainerDefinition } from './definition'; -export type { ContainerProps } from './types'; +export type { ContainerOwnProps, ContainerProps } from './types'; diff --git a/packages/ui/src/components/Container/types.ts b/packages/ui/src/components/Container/types.ts index 41de725b32..375f3a4a38 100644 --- a/packages/ui/src/components/Container/types.ts +++ b/packages/ui/src/components/Container/types.ts @@ -13,17 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { SpaceProps } from '../../types'; +import type { SpaceProps } from '../../types'; /** @public */ -export interface ContainerProps { +export type ContainerOwnProps = { children?: React.ReactNode; className?: string; + style?: React.CSSProperties; +}; + +/** @public */ +export interface ContainerProps + extends ContainerOwnProps, + Omit, keyof ContainerOwnProps> { my?: SpaceProps['my']; mb?: SpaceProps['mb']; mt?: SpaceProps['mt']; py?: SpaceProps['py']; pb?: SpaceProps['pb']; pt?: SpaceProps['pt']; - style?: React.CSSProperties; } diff --git a/packages/ui/src/components/Dialog/Dialog.module.css b/packages/ui/src/components/Dialog/Dialog.module.css index f5968820a2..da82509134 100644 --- a/packages/ui/src/components/Dialog/Dialog.module.css +++ b/packages/ui/src/components/Dialog/Dialog.module.css @@ -43,8 +43,10 @@ } .bui-Dialog { - background: var(--bui-bg-popover); - border-radius: 0.5rem; + --dialog-border-radius: 0.5rem; + background: var(--bui-bg-app); + box-shadow: var(--bui-shadow); + border-radius: var(--dialog-border-radius); border: 1px solid var(--bui-border-1); color: var(--bui-fg-primary); position: relative; @@ -52,9 +54,14 @@ max-width: calc(100vw - 3rem); height: min(var(--bui-dialog-min-height, auto), calc(100vh - 3rem)); max-height: calc(100vh - 3rem); + outline: none; + } + + .bui-DialogContent { display: flex; flex-direction: column; - outline: none; + border-radius: var(--dialog-border-radius); + height: 100%; } /* Dialog entering animation */ diff --git a/packages/ui/src/components/Dialog/Dialog.stories.tsx b/packages/ui/src/components/Dialog/Dialog.stories.tsx index 0392959a59..9084bfa7e4 100644 --- a/packages/ui/src/components/Dialog/Dialog.stories.tsx +++ b/packages/ui/src/components/Dialog/Dialog.stories.tsx @@ -63,6 +63,24 @@ export const Default = meta.story({ }); export const Open = Default.extend({ + parameters: { layout: 'fullscreen' }, + decorators: [ + Story => ( +
    + +
    + ), + ], args: { defaultOpen: true, }, diff --git a/packages/ui/src/components/Dialog/Dialog.tsx b/packages/ui/src/components/Dialog/Dialog.tsx index 1c33588306..fad5e10ea2 100644 --- a/packages/ui/src/components/Dialog/Dialog.tsx +++ b/packages/ui/src/components/Dialog/Dialog.tsx @@ -21,19 +21,25 @@ import { Modal, Heading, } from 'react-aria-components'; -import clsx from 'clsx'; import type { DialogTriggerProps, DialogHeaderProps, DialogProps, DialogBodyProps, + DialogFooterProps, } from './types'; import { RiCloseLine } from '@remixicon/react'; import { Button } from '../Button'; -import { useStyles } from '../../hooks/useStyles'; -import { DialogDefinition } from './definition'; +import { useDefinition } from '../../hooks/useDefinition'; +import { + DialogDefinition, + DialogHeaderDefinition, + DialogBodyDefinition, + DialogFooterDefinition, +} from './definition'; +import { Box } from '../Box'; +import { BgReset } from '../../hooks/useBg'; import { Flex } from '../Flex'; -import styles from './Dialog.module.css'; /** @public */ export const DialogTrigger = (props: DialogTriggerProps) => { @@ -43,23 +49,21 @@ export const DialogTrigger = (props: DialogTriggerProps) => { /** @public */ export const Dialog = forwardRef, DialogProps>( (props, ref) => { - const { classNames, cleanedProps } = useStyles(DialogDefinition, props); - const { className, children, width, height, style, ...rest } = cleanedProps; + const { ownProps, restProps } = useDefinition(DialogDefinition, props, { + classNameTarget: 'dialog', + }); + const { classes, children, width, height, style } = ownProps; return ( , DialogProps>( ...style, }} > - {children} + + + {children} + + ); @@ -85,19 +93,12 @@ export const DialogHeader = forwardRef< React.ElementRef<'div'>, DialogHeaderProps >((props, ref) => { - const { classNames, cleanedProps } = useStyles(DialogDefinition, props); - const { className, children, ...rest } = cleanedProps; + const { ownProps, restProps } = useDefinition(DialogHeaderDefinition, props); + const { classes, children } = ownProps; return ( - - + + {children}