` | 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`.
-
+ 
-- 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`.
-
+ 
-- 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
+ 
+
+6. Enter your GitHub user name as the `Owner`.
+7. Enter `tutorial` for the `Repository` and select `REVIEW`.
+
+ 
+
+8. Review the information and select `CREATE`.
+
+ 
+
+If you see an error message, similar to the following,
+
+ 
+
+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.
+
+
+
+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.
+
+
+
+Selecting `Home` in the sidebar, displays the new `tutorial` component in the Catalog.
+
+
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.
+
+
+
+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 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`.
-
+ 
-## 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.
-
+ 
-Hit `Analyze` and review the changes.
+4. Select `ANALYZE`.
+5. If the changes from `ANALYZE` are correct, select `IMPORT`.
-## 3. Import the entity
+ 
-If the changes from `Analyze` are correct, click `Apply`.
+ If your entity was successfully imported, the details will be displayed.
-
+ 
-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.
+ 
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.
+
+ 
+
+4. Select `UNREGISTER LOCATION`. The entity is removed from the Catalog.
+
+ 
+
+## 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.
+
+ 
+
+3. Select the three dots.
+4. Select `Unregister entity` in the dropdown menu.
+
+ 
+
+5. Select `ADVANCED OPTIONS`.
+
+ 
+
+6. Select `DELETE ENTITY`.
+
+ 
+
+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.
+
+ 
+
+ The associated `catalog-info.yaml` file is displayed.
+
+ 
+
+2. Make your changes to the YAML file. In this example, the name of the component is changed to `mytutorial`.
+
+ 
+
+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.
+
+ 
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
+
+
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.
+
+
+
+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`.
+
+
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`.
+
+ 
+
+ 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.
+
+ 
+
+## 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.
+
+ 
+
+ The `Catalog Graph` is displayed.
+
+ 
+
+### 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` = infinite
+
+ 
+
+- `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` = Step Before
+
+ 
+
+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` = On and `Merge Relations` = Off
+
+ 
+
+ - `Simplified` = Off and `Merge Relations` = On
+
+ 
+
+ - `Simplified` = Off and `Merge Relations` = Off
+
+ 
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
+---
+
+
+
+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