Merge branch 'master' into bui-pluginheader
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Fixed CIMD redirect URI matching to allow any port for localhost addresses per RFC 8252 Section 7.3. Native CLI clients use ephemeral ports for OAuth callbacks, which are now accepted when the registered redirect URI uses a localhost address.
|
||||
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"mode": "pre",
|
||||
"tag": "next",
|
||||
"initialVersions": {
|
||||
"example-app": "0.0.33",
|
||||
"@backstage/app-defaults": "1.7.6",
|
||||
"app-example-plugin": "0.0.33",
|
||||
"example-app-legacy": "0.2.119",
|
||||
"example-backend": "0.0.48",
|
||||
"@backstage/backend-app-api": "1.6.0",
|
||||
"@backstage/backend-defaults": "0.16.0",
|
||||
"@backstage/backend-dev-utils": "0.1.7",
|
||||
"@backstage/backend-dynamic-feature-service": "0.8.0",
|
||||
"@backstage/backend-openapi-utils": "0.6.7",
|
||||
"@backstage/backend-plugin-api": "1.8.0",
|
||||
"@backstage/backend-test-utils": "1.11.1",
|
||||
"@backstage/catalog-client": "1.14.0",
|
||||
"@backstage/catalog-model": "1.7.7",
|
||||
"@backstage/cli": "0.36.0",
|
||||
"@backstage/cli-common": "0.2.0",
|
||||
"@backstage/cli-defaults": "0.1.0",
|
||||
"@internal/cli": "0.0.2",
|
||||
"@backstage/cli-module-actions": "0.0.1",
|
||||
"@backstage/cli-module-auth": "0.1.0",
|
||||
"@backstage/cli-module-build": "0.1.0",
|
||||
"@backstage/cli-module-config": "0.1.0",
|
||||
"@backstage/cli-module-github": "0.1.0",
|
||||
"@backstage/cli-module-info": "0.1.0",
|
||||
"@backstage/cli-module-lint": "0.1.0",
|
||||
"@backstage/cli-module-maintenance": "0.1.0",
|
||||
"@backstage/cli-module-migrate": "0.1.0",
|
||||
"@backstage/cli-module-new": "0.1.0",
|
||||
"@backstage/cli-module-test-jest": "0.1.0",
|
||||
"@backstage/cli-module-translations": "0.1.0",
|
||||
"@backstage/cli-node": "0.3.0",
|
||||
"@backstage/codemods": "0.1.55",
|
||||
"@backstage/config": "1.3.6",
|
||||
"@backstage/config-loader": "1.10.9",
|
||||
"@backstage/core-app-api": "1.19.6",
|
||||
"@backstage/core-compat-api": "0.5.9",
|
||||
"@backstage/core-components": "0.18.8",
|
||||
"@backstage/core-plugin-api": "1.12.4",
|
||||
"@backstage/create-app": "0.8.0",
|
||||
"@backstage/dev-utils": "1.1.21",
|
||||
"e2e-test": "0.2.38",
|
||||
"@backstage/e2e-test-utils": "0.1.2",
|
||||
"@backstage/errors": "1.2.7",
|
||||
"@backstage/eslint-plugin": "0.2.2",
|
||||
"@backstage/filter-predicates": "0.1.1",
|
||||
"@backstage/frontend-app-api": "0.16.0",
|
||||
"@backstage/frontend-defaults": "0.5.0",
|
||||
"@backstage/frontend-dev-utils": "0.1.0",
|
||||
"@backstage/frontend-dynamic-feature-loader": "0.1.10",
|
||||
"@internal/frontend": "0.0.18",
|
||||
"@backstage/frontend-plugin-api": "0.15.0",
|
||||
"@backstage/frontend-test-utils": "0.5.1",
|
||||
"@backstage/integration": "2.0.0",
|
||||
"@backstage/integration-aws-node": "0.1.20",
|
||||
"@backstage/integration-react": "1.2.16",
|
||||
"@backstage/module-federation-common": "0.1.2",
|
||||
"@internal/opaque": "0.0.1",
|
||||
"@backstage/release-manifests": "0.0.13",
|
||||
"@backstage/repo-tools": "0.17.0",
|
||||
"@internal/scaffolder": "0.0.19",
|
||||
"@techdocs/cli": "1.10.6",
|
||||
"techdocs-cli-embedded-app": "0.2.118",
|
||||
"@backstage/test-utils": "1.7.16",
|
||||
"@backstage/theme": "0.7.2",
|
||||
"@backstage/types": "1.2.2",
|
||||
"@backstage/ui": "0.13.0",
|
||||
"@backstage/version-bridge": "1.0.12",
|
||||
"yarn-plugin-backstage": "0.0.10",
|
||||
"@backstage/plugin-api-docs": "0.13.5",
|
||||
"@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.11",
|
||||
"@backstage/plugin-app": "0.4.1",
|
||||
"@backstage/plugin-app-backend": "0.5.12",
|
||||
"@backstage/plugin-app-node": "0.1.43",
|
||||
"@backstage/plugin-app-react": "0.2.1",
|
||||
"@backstage/plugin-app-visualizer": "0.2.1",
|
||||
"@backstage/plugin-auth": "0.1.6",
|
||||
"@backstage/plugin-auth-backend": "0.27.2",
|
||||
"@backstage/plugin-auth-backend-module-atlassian-provider": "0.4.13",
|
||||
"@backstage/plugin-auth-backend-module-auth0-provider": "0.3.1",
|
||||
"@backstage/plugin-auth-backend-module-aws-alb-provider": "0.4.14",
|
||||
"@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.2.18",
|
||||
"@backstage/plugin-auth-backend-module-bitbucket-provider": "0.3.13",
|
||||
"@backstage/plugin-auth-backend-module-bitbucket-server-provider": "0.2.13",
|
||||
"@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.4.13",
|
||||
"@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.4.13",
|
||||
"@backstage/plugin-auth-backend-module-github-provider": "0.5.1",
|
||||
"@backstage/plugin-auth-backend-module-gitlab-provider": "0.4.1",
|
||||
"@backstage/plugin-auth-backend-module-google-provider": "0.3.13",
|
||||
"@backstage/plugin-auth-backend-module-guest-provider": "0.2.17",
|
||||
"@backstage/plugin-auth-backend-module-microsoft-provider": "0.3.13",
|
||||
"@backstage/plugin-auth-backend-module-oauth2-provider": "0.4.13",
|
||||
"@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.2.18",
|
||||
"@backstage/plugin-auth-backend-module-oidc-provider": "0.4.14",
|
||||
"@backstage/plugin-auth-backend-module-okta-provider": "0.2.13",
|
||||
"@backstage/plugin-auth-backend-module-onelogin-provider": "0.3.13",
|
||||
"@backstage/plugin-auth-backend-module-openshift-provider": "0.1.5",
|
||||
"@backstage/plugin-auth-backend-module-pinniped-provider": "0.3.12",
|
||||
"@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.5.12",
|
||||
"@backstage/plugin-auth-node": "0.6.14",
|
||||
"@backstage/plugin-auth-react": "0.1.25",
|
||||
"@backstage/plugin-bitbucket-cloud-common": "0.3.8",
|
||||
"@backstage/plugin-catalog": "2.0.0",
|
||||
"@backstage/plugin-catalog-backend": "3.5.0",
|
||||
"@backstage/plugin-catalog-backend-module-aws": "0.4.21",
|
||||
"@backstage/plugin-catalog-backend-module-azure": "0.3.15",
|
||||
"@backstage/plugin-catalog-backend-module-backstage-openapi": "0.5.12",
|
||||
"@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.5.9",
|
||||
"@backstage/plugin-catalog-backend-module-bitbucket-server": "0.5.9",
|
||||
"@backstage/plugin-catalog-backend-module-gcp": "0.3.17",
|
||||
"@backstage/plugin-catalog-backend-module-gerrit": "0.3.12",
|
||||
"@backstage/plugin-catalog-backend-module-gitea": "0.1.10",
|
||||
"@backstage/plugin-catalog-backend-module-github": "0.13.0",
|
||||
"@backstage/plugin-catalog-backend-module-github-org": "0.3.20",
|
||||
"@backstage/plugin-catalog-backend-module-gitlab": "0.8.1",
|
||||
"@backstage/plugin-catalog-backend-module-gitlab-org": "0.2.19",
|
||||
"@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.7.10",
|
||||
"@backstage/plugin-catalog-backend-module-ldap": "0.12.3",
|
||||
"@backstage/plugin-catalog-backend-module-logs": "0.1.20",
|
||||
"@backstage/plugin-catalog-backend-module-msgraph": "0.9.1",
|
||||
"@backstage/plugin-catalog-backend-module-openapi": "0.2.20",
|
||||
"@backstage/plugin-catalog-backend-module-puppetdb": "0.2.20",
|
||||
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.2.18",
|
||||
"@backstage/plugin-catalog-backend-module-unprocessed": "0.6.9",
|
||||
"@backstage/plugin-catalog-common": "1.1.8",
|
||||
"@backstage/plugin-catalog-graph": "0.6.0",
|
||||
"@backstage/plugin-catalog-import": "0.13.11",
|
||||
"@backstage/plugin-catalog-node": "2.1.0",
|
||||
"@backstage/plugin-catalog-react": "2.1.0",
|
||||
"@backstage/plugin-catalog-unprocessed-entities": "0.2.27",
|
||||
"@backstage/plugin-catalog-unprocessed-entities-common": "0.0.13",
|
||||
"@backstage/plugin-config-schema": "0.1.78",
|
||||
"@backstage/plugin-devtools": "0.1.37",
|
||||
"@backstage/plugin-devtools-backend": "0.5.15",
|
||||
"@backstage/plugin-devtools-common": "0.1.23",
|
||||
"@backstage/plugin-devtools-react": "0.2.0",
|
||||
"@backstage/plugin-events-backend": "0.6.0",
|
||||
"@backstage/plugin-events-backend-module-aws-sqs": "0.4.20",
|
||||
"@backstage/plugin-events-backend-module-azure": "0.2.29",
|
||||
"@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.29",
|
||||
"@backstage/plugin-events-backend-module-bitbucket-server": "0.1.10",
|
||||
"@backstage/plugin-events-backend-module-gerrit": "0.2.29",
|
||||
"@backstage/plugin-events-backend-module-github": "0.4.10",
|
||||
"@backstage/plugin-events-backend-module-gitlab": "0.3.10",
|
||||
"@backstage/plugin-events-backend-module-google-pubsub": "0.2.1",
|
||||
"@backstage/plugin-events-backend-module-kafka": "0.3.2",
|
||||
"@backstage/plugin-events-backend-test-utils": "0.1.53",
|
||||
"@backstage/plugin-events-node": "0.4.20",
|
||||
"@internal/plugin-todo-list": "1.0.49",
|
||||
"@internal/plugin-todo-list-backend": "1.0.48",
|
||||
"@internal/plugin-todo-list-common": "1.0.29",
|
||||
"@backstage/plugin-gateway-backend": "1.1.3",
|
||||
"@backstage/plugin-home": "0.9.3",
|
||||
"@backstage/plugin-home-react": "0.1.36",
|
||||
"@backstage/plugin-kubernetes": "0.12.17",
|
||||
"@backstage/plugin-kubernetes-backend": "0.21.2",
|
||||
"@backstage/plugin-kubernetes-cluster": "0.0.35",
|
||||
"@backstage/plugin-kubernetes-common": "0.9.10",
|
||||
"@backstage/plugin-kubernetes-node": "0.4.2",
|
||||
"@backstage/plugin-kubernetes-react": "0.5.17",
|
||||
"@backstage/plugin-mcp-actions-backend": "0.1.10",
|
||||
"@backstage/plugin-mui-to-bui": "0.2.5",
|
||||
"@backstage/plugin-notifications": "0.5.15",
|
||||
"@backstage/plugin-notifications-backend": "0.6.3",
|
||||
"@backstage/plugin-notifications-backend-module-email": "0.3.19",
|
||||
"@backstage/plugin-notifications-backend-module-slack": "0.4.0",
|
||||
"@backstage/plugin-notifications-common": "0.2.1",
|
||||
"@backstage/plugin-notifications-node": "0.2.24",
|
||||
"@backstage/plugin-org": "0.7.0",
|
||||
"@backstage/plugin-org-react": "0.1.48",
|
||||
"@backstage/plugin-permission-backend": "0.7.10",
|
||||
"@backstage/plugin-permission-backend-module-allow-all-policy": "0.2.17",
|
||||
"@backstage/plugin-permission-common": "0.9.7",
|
||||
"@backstage/plugin-permission-node": "0.10.11",
|
||||
"@backstage/plugin-permission-react": "0.4.41",
|
||||
"@backstage/plugin-proxy-backend": "0.6.11",
|
||||
"@backstage/plugin-proxy-node": "0.1.13",
|
||||
"@backstage/plugin-scaffolder": "1.36.0",
|
||||
"@backstage/plugin-scaffolder-backend": "3.2.0",
|
||||
"@backstage/plugin-scaffolder-backend-module-azure": "0.2.19",
|
||||
"@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.3.4",
|
||||
"@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.2.19",
|
||||
"@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.3.19",
|
||||
"@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.3.21",
|
||||
"@backstage/plugin-scaffolder-backend-module-gcp": "0.2.19",
|
||||
"@backstage/plugin-scaffolder-backend-module-gerrit": "0.2.19",
|
||||
"@backstage/plugin-scaffolder-backend-module-gitea": "0.2.19",
|
||||
"@backstage/plugin-scaffolder-backend-module-github": "0.9.7",
|
||||
"@backstage/plugin-scaffolder-backend-module-gitlab": "0.11.4",
|
||||
"@backstage/plugin-scaffolder-backend-module-notifications": "0.1.20",
|
||||
"@backstage/plugin-scaffolder-backend-module-rails": "0.5.19",
|
||||
"@backstage/plugin-scaffolder-backend-module-sentry": "0.3.2",
|
||||
"@backstage/plugin-scaffolder-backend-module-yeoman": "0.4.20",
|
||||
"@backstage/plugin-scaffolder-common": "2.0.0",
|
||||
"@backstage/plugin-scaffolder-node": "0.13.0",
|
||||
"@backstage/plugin-scaffolder-node-test-utils": "0.3.9",
|
||||
"@backstage/plugin-scaffolder-react": "1.20.0",
|
||||
"@backstage/plugin-search": "1.7.0",
|
||||
"@backstage/plugin-search-backend": "2.1.0",
|
||||
"@backstage/plugin-search-backend-module-catalog": "0.3.13",
|
||||
"@backstage/plugin-search-backend-module-elasticsearch": "1.8.1",
|
||||
"@backstage/plugin-search-backend-module-explore": "0.3.12",
|
||||
"@backstage/plugin-search-backend-module-pg": "0.5.53",
|
||||
"@backstage/plugin-search-backend-module-stack-overflow-collator": "0.3.18",
|
||||
"@backstage/plugin-search-backend-module-techdocs": "0.4.12",
|
||||
"@backstage/plugin-search-backend-node": "1.4.2",
|
||||
"@backstage/plugin-search-common": "1.2.22",
|
||||
"@backstage/plugin-search-react": "1.11.0",
|
||||
"@backstage/plugin-signals": "0.0.29",
|
||||
"@backstage/plugin-signals-backend": "0.3.13",
|
||||
"@backstage/plugin-signals-node": "0.1.29",
|
||||
"@backstage/plugin-signals-react": "0.0.20",
|
||||
"@backstage/plugin-techdocs": "1.17.1",
|
||||
"@backstage/plugin-techdocs-addons-test-utils": "2.0.3",
|
||||
"@backstage/plugin-techdocs-backend": "2.1.6",
|
||||
"@backstage/plugin-techdocs-common": "0.1.1",
|
||||
"@backstage/plugin-techdocs-module-addons-contrib": "1.1.34",
|
||||
"@backstage/plugin-techdocs-node": "1.14.4",
|
||||
"@backstage/plugin-techdocs-react": "1.3.9",
|
||||
"@backstage/plugin-user-settings": "0.9.1",
|
||||
"@backstage/plugin-user-settings-backend": "0.4.1",
|
||||
"@backstage/plugin-user-settings-common": "0.1.0"
|
||||
},
|
||||
"changesets": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Fixed the ButtonIcon's loading spinner animation
|
||||
|
||||
**Affected components:** ButtonIcon
|
||||
@@ -1 +0,0 @@
|
||||
Added titleLink prop to PageLayoutProps so the plugin header title can link back to the plugin root
|
||||
@@ -1 +0,0 @@
|
||||
Removed the unnecessary `@backstage/cli-module-new` dependency from the default create-app template.
|
||||
@@ -1 +0,0 @@
|
||||
Fixed broken API reference links in documentation.
|
||||
@@ -1 +0,0 @@
|
||||
Migrates TechDocs alpha plugin pages to BUI header system, fixing double scrollbar issue with the new plugin header.
|
||||
@@ -1 +0,0 @@
|
||||
Integrate unprocessed entities as a DevTools tab by default
|
||||
@@ -1 +0,0 @@
|
||||
Add `apis` to `BootstrapSpecializedApp` and `FinalizedSpecializedApp` types
|
||||
@@ -1 +0,0 @@
|
||||
Disable page layout header for the catalog entity page in the new frontend system
|
||||
@@ -1 +0,0 @@
|
||||
Various fixes and improvements for the `@backstage/create-app` template.
|
||||
@@ -1 +0,0 @@
|
||||
Fix scaffolder plugin page layout in the new frontend system
|
||||
@@ -0,0 +1 @@
|
||||
Fixed CIMD redirect URI matching to allow any port for loopback addresses.
|
||||
@@ -0,0 +1 @@
|
||||
Fixed CIMD redirect URI matching to allow any port for loopback addresses.
|
||||
+2
-1
@@ -289,4 +289,5 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/
|
||||
| [Swiss Mobiliar Insurance Company Ltd.](https://www.mobiliar.ch/) | [Patrick Wyler](mailto:patrick.wyler@mobiliar.ch) and [Beat Winistörfer](mailto:beat.winistoerfer@mobiliar.ch) | The portal provides a unified interface for accessing all relevant DevOps information previously scattered across various locations, enhancing accessibility and clarity for all IT employees. It relies on an internal graph database that enhances the Backstage software catalog with many additional elements. Significant effort has been invested in the visual representation of information through graphs and diagrams, facilitating analysis and improving the understanding of dependencies. |
|
||||
| [DB Systel GmbH](https://www.dbsystel.de/dbsystel-en/) | [DB Systel GmbH](https://github.com/dbsystel) | Deutsche Bahn's Internal Developer Portal leverages Backstage and Crossplane.io to deliver a fully GitOps-driven onboarding experience for engineering teams across the company. Our Scaffolder ecosystem accelerates platform setup (Artifactory, GitLab, OpenShift), service bootstrapping, automated testing, AI-ready backends, and modern frontend development. We also make heavy use of custom catalog modules and frontend plugins that support developers in checking their provisioned resources or security and compliance of their code. |
|
||||
| [Danske StatsBaner (DSB)](https://www.dsb.dk/om-dsb/job-og-karriere/) | [Isaac Livingston](mailto:isjl@dsb.dk), [Paula-Catalina Curca](mailto:PACU@DSB.DK), [Muhammad Salman Khan](mailto:MUSK@DSB.DK) | DSB's portal is a central catalog of all our microservices, APIs, and systems. Developers can easily create new components with templates that run in our CNCF based developer platform |
|
||||
| [NN Group](https://www.nn-group.com/) | [Flavia Naezer (Product Owner)](mailto:flavia.naezer@nn-group.com), [Vijai Ramcharan (Principal Engineer)](mailto:vijai.ramcharan@nn-group.com) | Our Backstage powered IDP supports engineers and other makers and shapers in our large financial enterprise with the Software Catalog, a Store for infrastructure, TechDocs and Portal Platform with mini App-Store.|
|
||||
| [NN Group](https://www.nn-group.com/) | [Flavia Naezer (Product Owner)](mailto:flavia.naezer@nn-group.com), [Vijai Ramcharan (Principal Engineer)](mailto:vijai.ramcharan@nn-group.com) | Our Backstage powered IDP supports engineers and other makers and shapers in our large financial enterprise with the Software Catalog, a Store for infrastructure, TechDocs and Portal Platform with mini App-Store.|
|
||||
| [Puzzle ITC](https://www.puzzle.ch/) | [Christoph Raaflaub](https://github.com/chrira) | Portal to our developer platform. |
|
||||
@@ -0,0 +1,236 @@
|
||||
---
|
||||
title: Backstage AI Skills Publishing and Discovery
|
||||
status: implementable
|
||||
authors:
|
||||
- '@drodil'
|
||||
owners:
|
||||
- '@backstage/maintainers'
|
||||
project-areas:
|
||||
- core
|
||||
creation-date: 2026-03-06
|
||||
---
|
||||
|
||||
# BEP: AI skills for Backstage development
|
||||
|
||||
- [Summary](#summary)
|
||||
- [Motivation](#motivation)
|
||||
- [Goals](#goals)
|
||||
- [Non-Goals](#non-goals)
|
||||
- [Proposal](#proposal)
|
||||
- [Design Details](#design-details)
|
||||
- [Release Plan](#release-plan)
|
||||
- [Dependencies](#dependencies)
|
||||
- [Alternatives](#alternatives)
|
||||
|
||||
## Summary
|
||||
|
||||
This BEP proposes that curated Backstage AI skills are authored in the main Backstage
|
||||
repository and published separately to a well-known endpoint on `backstage.io`. Instead
|
||||
of creating a new Backstage-owned repository and custom discovery protocol, Backstage
|
||||
will publish skills using the well-known layout already supported by [`skills.sh`](https://skills.sh/).
|
||||
|
||||
The published output can be released on its own publishing cadence even though the source
|
||||
content lives in the main monorepo.
|
||||
|
||||
The primary consumer workflow becomes:
|
||||
|
||||
- `npx skills add https://backstage.io` to discover and install all published Backstage skills.
|
||||
- `npx skills add https://backstage.io/.well-known/skills/<skill-name>` to install a specific skill.
|
||||
|
||||
This keeps Backstage aligned with an existing ecosystem convention, reduces maintenance
|
||||
surface area, and still gives adopters a clear, official source for reusable AI guidance.
|
||||
Prompt templates, instructions, and supporting files can still be shipped, but they are
|
||||
packaged within skill directories instead of through a separate Backstage-specific asset system.
|
||||
|
||||
## Motivation
|
||||
|
||||
Backstage adopters are increasingly using AI-assisted workflows for plugin development,
|
||||
migration, maintenance, and documentation. Today, prompt and skill quality varies
|
||||
significantly across adopters, and there is no common, well-known channel for sharing proven
|
||||
guidance. A lot of adopters already have assets that help AI-assisted development and
|
||||
could be shared with the community.
|
||||
|
||||
As a result:
|
||||
|
||||
- Teams duplicate effort by recreating similar prompts and instructions.
|
||||
- Effective practices are hard to discover and spread.
|
||||
- Shared guidance is often published in tool-specific or organization-specific layouts.
|
||||
- Security and governance concerns are often handled ad hoc.
|
||||
|
||||
Backstage should solve this in the simplest interoperable way. Creating a dedicated asset
|
||||
repository, a Backstage-specific discovery index, and custom install/update tooling would
|
||||
increase maintenance cost while duplicating functionality that `skills.sh` already provides.
|
||||
|
||||
By publishing official Backstage skills through a well-known endpoint on `backstage.io`,
|
||||
Backstage can provide a reliable baseline for AI-enabled development while preserving local
|
||||
extension and customization and staying aligned with an existing ecosystem workflow.
|
||||
|
||||
### Goals
|
||||
|
||||
- Author official Backstage AI skills in the main Backstage repository.
|
||||
- Publish those skills separately to a well-known endpoint on `backstage.io`.
|
||||
- Reuse `skills.sh` discovery and installation behavior instead of inventing a Backstage-specific protocol.
|
||||
- Define a stable source layout and review process for Backstage-authored skills.
|
||||
- Allow skill directories to include prompts, instructions, and supporting files needed by each skill.
|
||||
- Publish contributor and review guidelines so assets remain high quality and safe.
|
||||
- Provide clear docs for adopters on how to install and customize the published skills.
|
||||
|
||||
### Non-Goals
|
||||
|
||||
- Building or hosting a new LLM inference platform.
|
||||
- Standardizing model provider APIs across all Backstage AI integrations.
|
||||
- Automatically executing AI-generated code in adopter repositories.
|
||||
- Enforcing centralized telemetry collection from adopter environments.
|
||||
- Replacing existing organization-specific prompt repositories. Local assets remain supported.
|
||||
- Solving all possible AI governance policy differences between organizations.
|
||||
- Creating a separate Backstage-owned repository just for AI skills.
|
||||
- Building a Backstage-specific discovery index, manifest, or asset registry.
|
||||
- Supporting bundles, lockfiles, version pinning, or custom update flows in this first iteration.
|
||||
|
||||
## Proposal
|
||||
|
||||
Introduce three deliverables:
|
||||
|
||||
1. A Backstage monorepo location that serves as the source of truth for Backstage-authored AI skills.
|
||||
1. A publishing workflow that exposes approved skills at `https://backstage.io/.well-known/skills/`.
|
||||
1. Documentation for consuming those skills through `skills.sh`
|
||||
|
||||
The Backstage source content will contain:
|
||||
|
||||
- Reusable skills for common Backstage engineering tasks.
|
||||
- Skill definitions, instructions, prompt fragments, and supporting files packaged per skill.
|
||||
- Lightweight metadata needed for publication and review.
|
||||
- Validation and quality checks executed in CI.
|
||||
|
||||
The primary consumer workflow will use `skills.sh` directly:
|
||||
|
||||
- `npx skills add https://backstage.io`
|
||||
- `npx skills add https://backstage.io/.well-known/skills/<skill-name>`
|
||||
|
||||
If Backstage later adds CLI support, it should remain intentionally thin:
|
||||
|
||||
- It should default to the Backstage well-known endpoint.
|
||||
- It should delegate to `skills.sh` semantics rather than defining its own registry or manifest.
|
||||
- It should not introduce separate bundle, version, or update concepts.
|
||||
|
||||
This approach keeps discovery and installation interoperable with the broader ecosystem,
|
||||
while still letting Backstage publish curated guidance from an official source.
|
||||
|
||||
## Design Details
|
||||
|
||||
### Source and publishing design
|
||||
|
||||
The source of truth for Backstage skills will live in the Backstage monorepo. The exact
|
||||
directory can be finalized during implementation, but it should provide a predictable layout
|
||||
for authoring, review, and publication.
|
||||
|
||||
Each skill directory should be self-contained and may include:
|
||||
|
||||
- `SKILL.md` as the primary skill entry point.
|
||||
- Supporting prompt or instruction files referenced by the skill.
|
||||
- Optional metadata files used by Backstage publishing workflows.
|
||||
|
||||
Published output on `backstage.io` will follow the `skills.sh` well-known layout:
|
||||
|
||||
- `https://backstage.io/.well-known/skills/index.json`
|
||||
- `https://backstage.io/.well-known/skills/<skill-name>/SKILL.md`
|
||||
- Additional files under `https://backstage.io/.well-known/skills/<skill-name>/...`
|
||||
|
||||
The published index should contain entries with the current `skills.sh` well-known shape:
|
||||
|
||||
- `name`: skill identifier, matching the published directory name.
|
||||
- `description`: short description of the skill.
|
||||
- `files`: all published files in the skill directory.
|
||||
|
||||
Example published layout:
|
||||
|
||||
```text
|
||||
.well-known/skills/index.json
|
||||
.well-known/skills/plugin-authoring/SKILL.md
|
||||
.well-known/skills/plugin-authoring/prompts/review.md
|
||||
```
|
||||
|
||||
Example index structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"skills": [
|
||||
{
|
||||
"name": "plugin-authoring",
|
||||
"description": "Guidance for creating and reviewing Backstage plugins.",
|
||||
"files": ["SKILL.md", "prompts/review.md"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Backstage CI should validate the source skill layout, ensure published file references are
|
||||
correct, and generate the well-known endpoint contents as part of the publishing workflow.
|
||||
|
||||
### Consumer workflow
|
||||
|
||||
The main adoption path is documentation, not new tooling:
|
||||
|
||||
- Users install Backstage skills with `npx skills add https://backstage.io`.
|
||||
- Users can install a specific published skill by pointing at its well-known skill URL.
|
||||
- Any install behavior, target path decisions, or local merge semantics remain the responsibility of `skills.sh`.
|
||||
|
||||
### Extensibility
|
||||
|
||||
Adopters can:
|
||||
|
||||
- Keep local-only skills alongside installed Backstage skills.
|
||||
- Customize installed files after adding them to their repository.
|
||||
- Publish their own well-known skills endpoints independently of Backstage.
|
||||
|
||||
Backstage should not block these local workflows by requiring a proprietary asset format.
|
||||
|
||||
## Release Plan
|
||||
|
||||
Phase 1: Repository bootstrap
|
||||
|
||||
- Define the Backstage monorepo location and review guidelines for skills.
|
||||
- Seed the source tree with a small set of high-value Backstage skills.
|
||||
- Validate contributor workflow and CI checks for source content.
|
||||
|
||||
Phase 2: Publishing rollout
|
||||
|
||||
- Implement a publishing workflow that generates `/.well-known/skills/` content for `backstage.io`.
|
||||
- Publish `index.json` and the initial skill directories.
|
||||
- Validate the published endpoint against `skills.sh` consumption behavior.
|
||||
|
||||
Phase 3: General availability
|
||||
|
||||
- Publish adoption documentation centered on `npx skills add https://backstage.io`.
|
||||
- Define support and ownership model for ongoing skill curation and publishing.
|
||||
- Evaluate whether an optional thin `backstage-cli` wrapper adds enough value to justify maintenance.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `skills.sh` continuing to support the current well-known endpoint format.
|
||||
- A Backstage publishing workflow that can emit static files to `backstage.io/.well-known/skills/`.
|
||||
- Agreement on long-term ownership and governance model for Backstage-authored skills.
|
||||
- Documentation updates describing authoring, publishing, and adopter usage patterns.
|
||||
|
||||
## Alternatives
|
||||
|
||||
1. Keep AI assets fully decentralized in individual adopter repositories.
|
||||
|
||||
This preserves autonomy but does not solve discovery, reuse, or consistency. It also increases duplicated maintenance
|
||||
effort across adopters.
|
||||
|
||||
2. Create a separate Backstage-owned repository for AI assets.
|
||||
|
||||
This was the original direction, but it adds repository sprawl and still requires a distribution mechanism. Keeping the
|
||||
source in the main monorepo simplifies governance and keeps the content closer to the Backstage code and docs it
|
||||
describes.
|
||||
|
||||
3. Build a Backstage-specific discovery and installation protocol.
|
||||
|
||||
This would duplicate functionality already available through `skills.sh` and create a long-term maintenance burden for
|
||||
limited short-term benefit.
|
||||
|
||||
4. Publish assets only as documentation in the Backstage docs site.
|
||||
|
||||
This improves discoverability, but without the well-known `skills.sh` layout it does not provide a direct installation
|
||||
path for users.
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"skills": [
|
||||
{
|
||||
"name": "mui-to-bui-migration",
|
||||
"description": "Migrate Backstage plugins from Material-UI (MUI) to Backstage UI (BUI). Use this skill when migrating components, updating imports, replacing styling patterns, or converting MUI components to their BUI equivalents.",
|
||||
"files": ["SKILL.md"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,781 @@
|
||||
---
|
||||
name: mui-to-bui-migration
|
||||
description: Migrate Backstage plugins from Material-UI (MUI) to Backstage UI (BUI). Use this skill when migrating components, updating imports, replacing styling patterns, or converting MUI components to their BUI equivalents.
|
||||
---
|
||||
|
||||
# MUI to BUI Migration Skill
|
||||
|
||||
This skill helps migrate Backstage plugins from Material-UI (@material-ui/core, @material-ui/icons) to Backstage UI (
|
||||
@backstage/ui).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting migration:
|
||||
|
||||
1. Install the BUI package:
|
||||
|
||||
```bash
|
||||
yarn add @backstage/ui
|
||||
```
|
||||
|
||||
2. Add the CSS import to your root file (typically `src/index.ts` or app entry point):
|
||||
```typescript
|
||||
import '@backstage/ui/css/styles.css';
|
||||
```
|
||||
|
||||
## Available BUI Components
|
||||
|
||||
### Layout Components
|
||||
|
||||
- `Box` - Basic layout container with CSS properties
|
||||
- `Container` - Centered content container with max-width
|
||||
- `Grid` - CSS Grid-based layout (`Grid.Root`, `Grid.Item`)
|
||||
- `Flex` - `Flexbox` layout component
|
||||
|
||||
### UI Components
|
||||
|
||||
- `Accordion` - Collapsible content panels
|
||||
- `Avatar` - User/entity avatars
|
||||
- `Button` - Primary action buttons (`variant="primary"`, `variant="secondary"`, `isDisabled`)
|
||||
- `ButtonIcon` - Icon-only buttons (`icon`, `onPress`, `variant`)
|
||||
- `ButtonLink` - Link styled as button
|
||||
- `Card` - Content cards (`Card`, `CardHeader`, `CardBody`, `CardFooter`)
|
||||
- `Checkbox` - Checkbox input
|
||||
- `Dialog` - Modal dialogs (`DialogTrigger`, `Dialog`, `DialogHeader`, `DialogBody`, `DialogFooter`)
|
||||
- `Header` - Page headers
|
||||
- `HeaderPage` - Full page header component
|
||||
- `Link` - Navigation links
|
||||
- `Menu` - Dropdown menus (`MenuTrigger`, `Menu`, `MenuItem`)
|
||||
- `PasswordField` - Password input field
|
||||
- `Popover` - Popover overlays
|
||||
- `RadioGroup` - Radio button groups
|
||||
- `SearchField` - Search input
|
||||
- `Select` - Dropdown select
|
||||
- `Skeleton` - Loading skeleton
|
||||
- `Switch` - Toggle switch
|
||||
- `Table` - Data tables
|
||||
- `Tabs` - Tab navigation (`Tabs`, `TabList`, `Tab`, `TabPanel`)
|
||||
- `Tag` - Tag/chip component (replaces MUI Chip)
|
||||
- `TagGroup` - Tag/chip groups
|
||||
- `Text` - Typography component (`variant`, `color`)
|
||||
- `TextField` - Text input (`isRequired`, `onChange` receives string directly)
|
||||
- `ToggleButton` - Toggle buttons
|
||||
- `ToggleButtonGroup` - Grouped toggle buttons
|
||||
- `Tooltip` - Tooltip overlays (used with TooltipTrigger from react-aria-components)
|
||||
- `VisuallyHidden` - Accessibility helper
|
||||
|
||||
### Hooks
|
||||
|
||||
- `useBreakpoint` - Responsive breakpoint hook
|
||||
|
||||
## Migration Patterns
|
||||
|
||||
### 1. Import Changes
|
||||
|
||||
**Remove MUI imports:**
|
||||
|
||||
```typescript
|
||||
// REMOVE these imports
|
||||
import { Box, Typography, Tooltip, Paper } from '@material-ui/core';
|
||||
import { makeStyles, Theme } from '@material-ui/core/styles';
|
||||
import SomeIcon from '@material-ui/icons/SomeIcon';
|
||||
```
|
||||
|
||||
**Add BUI imports:**
|
||||
|
||||
```typescript
|
||||
// ADD these imports
|
||||
import { Box, Flex, Text, Tooltip, Card } from '@backstage/ui';
|
||||
import { RiSomeIcon } from '@remixicon/react';
|
||||
import styles from './MyComponent.module.css';
|
||||
```
|
||||
|
||||
### 2. Styling: `makeStyles` to CSS Modules
|
||||
|
||||
Create a `.module.css` file alongside your component using BUI CSS variables.
|
||||
|
||||
**Before (MUI `makeStyles`):**
|
||||
|
||||
```typescript
|
||||
// MyComponent.tsx
|
||||
import {makeStyles, Theme} from '@material-ui/core/styles';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
container: {
|
||||
padding: theme.spacing(2),
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
title: {
|
||||
marginBottom: theme.spacing(1),
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
listItem: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
minWidth: 56,
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}));
|
||||
|
||||
function MyComponent() {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<div className = {classes.container} >
|
||||
<Typography className = {classes.title} > Title < /Typography>
|
||||
< div
|
||||
className = {classes.listItem} >
|
||||
<div className = {classes.icon} >
|
||||
<SomeIcon / >
|
||||
</div>
|
||||
< span > Content < /span>
|
||||
< /div>
|
||||
< /div>
|
||||
)
|
||||
;
|
||||
}
|
||||
```
|
||||
|
||||
**After (CSS Modules with BUI variables):**
|
||||
|
||||
```css
|
||||
/* MyComponent.module.css */
|
||||
@layer components {
|
||||
.container {
|
||||
padding: var(--bui-space-4);
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
border-radius: var(--bui-radius-2);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: var(--bui-space-2);
|
||||
color: var(--bui-fg-primary);
|
||||
}
|
||||
|
||||
.listItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--bui-space-2) 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
min-width: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--bui-fg-secondary);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// MyComponent.tsx
|
||||
import {Box, Text} from '@backstage/ui';
|
||||
import {RiSomeIcon} from '@remixicon/react';
|
||||
import styles from './MyComponent.module.css';
|
||||
|
||||
function MyComponent() {
|
||||
return (
|
||||
<Box className = {styles.container} >
|
||||
<Text className = {styles.title} > Title < /Text>
|
||||
< div
|
||||
className = {styles.listItem} >
|
||||
<div className = {styles.icon} >
|
||||
<RiSomeIcon size = {24}
|
||||
/>
|
||||
< /div>
|
||||
< span > Content < /span>
|
||||
< /div>
|
||||
< /Box>
|
||||
)
|
||||
;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Layout: Box with display to `Flex`
|
||||
|
||||
**Before (MUI Box with display prop):**
|
||||
|
||||
```typescript
|
||||
<Box
|
||||
display = "flex"
|
||||
flexDirection = "column"
|
||||
alignItems = "center"
|
||||
justifyContent = "space-between"
|
||||
>
|
||||
<Box display = "flex"
|
||||
flexDirection = "row"
|
||||
gap = {2} >
|
||||
{children}
|
||||
< /Box>
|
||||
< /Box>
|
||||
```
|
||||
|
||||
**After (BUI `Flex` component):**
|
||||
|
||||
```typescript
|
||||
<Flex direction = "column"
|
||||
align = "center"
|
||||
justify = "between" >
|
||||
<Flex direction = "row"
|
||||
style = {
|
||||
{
|
||||
gap: 'var(--bui-space-4)'
|
||||
}
|
||||
}>
|
||||
{
|
||||
children
|
||||
}
|
||||
</Flex>
|
||||
< /Flex>
|
||||
```
|
||||
|
||||
Note: BUI `Flex` uses `justify="between"` not `justify="space-between"`.
|
||||
|
||||
### 4. Grid Layout
|
||||
|
||||
**Before (MUI Grid):**
|
||||
|
||||
```typescript
|
||||
<Grid container
|
||||
spacing = {3} >
|
||||
<Grid item
|
||||
xs = {12}
|
||||
md = {6} >
|
||||
{content}
|
||||
< /Grid>
|
||||
< /Grid>
|
||||
```
|
||||
|
||||
**After (BUI Grid):**
|
||||
|
||||
```typescript
|
||||
<Grid.Root columns = {
|
||||
{
|
||||
sm: '12'
|
||||
}
|
||||
}
|
||||
gap = "6" >
|
||||
<Grid.Item colSpan = {
|
||||
{
|
||||
sm: '12', md
|
||||
:
|
||||
'6'
|
||||
}
|
||||
}>
|
||||
{
|
||||
content
|
||||
}
|
||||
</Grid.Item>
|
||||
< /Grid.Root>
|
||||
```
|
||||
|
||||
### 5. Typography to Text
|
||||
|
||||
**Before (MUI Typography):**
|
||||
|
||||
```typescript
|
||||
<Typography variant = "h1" > Heading < /Typography>
|
||||
< Typography
|
||||
variant = "h6" > Subheading < /Typography>
|
||||
< Typography
|
||||
variant = "body1" > Body
|
||||
text < /Typography>
|
||||
< Typography
|
||||
variant = "body2"
|
||||
color = "textSecondary" > Secondary
|
||||
text < /Typography>
|
||||
```
|
||||
|
||||
**After (BUI Text):**
|
||||
|
||||
```typescript
|
||||
<Text variant = "title-large" > Heading < /Text>
|
||||
< Text
|
||||
variant = "title-small" > Subheading < /Text>
|
||||
< Text
|
||||
variant = "body-medium" > Body
|
||||
text < /Text>
|
||||
< Text
|
||||
variant = "body-small"
|
||||
color = "secondary" > Secondary
|
||||
text < /Text>
|
||||
```
|
||||
|
||||
Valid Text variants: `title-large`, `title-medium`, `title-small`, `title-x-small`, `body-large`, `body-medium`,
|
||||
`body-small`, `body-x-small`
|
||||
|
||||
### 6. Tooltip Pattern
|
||||
|
||||
**Before (MUI Tooltip):**
|
||||
|
||||
```typescript
|
||||
import {Tooltip, Typography} from '@material-ui/core';
|
||||
|
||||
<Tooltip title = { < Typography > Tooltip
|
||||
content < /Typography>}>
|
||||
< span > Hover
|
||||
me < /span>
|
||||
< /Tooltip>;
|
||||
```
|
||||
|
||||
**After (BUI TooltipTrigger pattern):**
|
||||
|
||||
```typescript
|
||||
import {Tooltip, Text} from '@backstage/ui';
|
||||
import {TooltipTrigger} from 'react-aria-components';
|
||||
|
||||
<TooltipTrigger>
|
||||
<Text>Hover
|
||||
me < /Text>
|
||||
< Tooltip > Tooltip
|
||||
content < /Tooltip>
|
||||
< /TooltipTrigger>;
|
||||
```
|
||||
|
||||
Note: Add `react-aria-components` to your dependencies.
|
||||
|
||||
### 7. Dialog Pattern
|
||||
|
||||
**Before (MUI Dialog):**
|
||||
|
||||
```typescript
|
||||
import {Dialog, DialogTitle, DialogActions, Button} from '@material-ui/core';
|
||||
|
||||
<Dialog open = {isOpen}
|
||||
onClose = {onClose} >
|
||||
<DialogTitle>Title < /DialogTitle>
|
||||
< DialogActions >
|
||||
<Button onClick = {onClose} > Cancel < /Button>
|
||||
< Button
|
||||
onClick = {onConfirm}
|
||||
color = "primary" >
|
||||
Confirm
|
||||
< /Button>
|
||||
< /DialogActions>
|
||||
< /Dialog>;
|
||||
```
|
||||
|
||||
**After (BUI Dialog):**
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
Button,
|
||||
} from '@backstage/ui';
|
||||
|
||||
<DialogTrigger>
|
||||
<Dialog
|
||||
isOpen = {isOpen}
|
||||
isDismissable
|
||||
onOpenChange = {open
|
||||
=>
|
||||
{
|
||||
if (!open) onClose();
|
||||
}
|
||||
}
|
||||
>
|
||||
<DialogHeader>Title < /DialogHeader>
|
||||
< DialogFooter >
|
||||
<Button onClick = {onConfirm}
|
||||
variant = "primary" >
|
||||
Confirm
|
||||
< /Button>
|
||||
< Button
|
||||
onClick = {onClose}
|
||||
variant = "secondary"
|
||||
slot = "close" >
|
||||
Cancel
|
||||
< /Button>
|
||||
< /DialogFooter>
|
||||
< /Dialog>
|
||||
< /DialogTrigger>;
|
||||
```
|
||||
|
||||
### 8. Button Changes
|
||||
|
||||
**Before (MUI Button):**
|
||||
|
||||
```typescript
|
||||
<Button variant = "contained"
|
||||
color = "primary"
|
||||
disabled = {loading}
|
||||
onClick = {handleClick} >
|
||||
Submit
|
||||
< /Button>
|
||||
< IconButton
|
||||
onClick = {handleDelete}
|
||||
disabled = {!
|
||||
canDelete
|
||||
}>
|
||||
<DeleteIcon / >
|
||||
</IconButton>
|
||||
```
|
||||
|
||||
**After (BUI Button):**
|
||||
|
||||
```typescript
|
||||
<Button variant = "primary"
|
||||
isDisabled = {loading}
|
||||
onClick = {handleClick} >
|
||||
Submit
|
||||
< /Button>
|
||||
< ButtonIcon
|
||||
aria - label = "delete"
|
||||
isDisabled = {!
|
||||
canDelete
|
||||
}
|
||||
onPress = {handleDelete}
|
||||
icon = { < RiDeleteBinLine
|
||||
size = {16}
|
||||
/>}
|
||||
variant = "secondary"
|
||||
/ >
|
||||
```
|
||||
|
||||
### 9. TextField Changes
|
||||
|
||||
**Before (MUI TextField):**
|
||||
|
||||
```typescript
|
||||
<TextField
|
||||
required
|
||||
name="title"
|
||||
label="Title"
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
```
|
||||
|
||||
**After (BUI TextField):**
|
||||
|
||||
```typescript
|
||||
<TextField
|
||||
isRequired
|
||||
id="title"
|
||||
label="Title"
|
||||
value={value}
|
||||
onChange={newValue => setValue(newValue)} // receives string directly!
|
||||
/>
|
||||
```
|
||||
|
||||
Note: BUI TextField `onChange` receives the string value directly, not an event object.
|
||||
|
||||
### 10. Tabs Pattern
|
||||
|
||||
**Before (MUI Tabs):**
|
||||
|
||||
```typescript
|
||||
import {Tab} from '@material-ui/core';
|
||||
import {TabContext, TabList, TabPanel} from '@material-ui/lab';
|
||||
|
||||
<TabContext value = {tab} >
|
||||
<TabList onChange = {handleChange} >
|
||||
<Tab label = "Tab 1"
|
||||
value = "tab1" / >
|
||||
<Tab label = "Tab 2"
|
||||
value = "tab2" / >
|
||||
</TabList>
|
||||
< TabPanel
|
||||
value = "tab1" > Content
|
||||
1 < /TabPanel>
|
||||
< TabPanel
|
||||
value = "tab2" > Content
|
||||
2 < /TabPanel>
|
||||
< /TabContext>;
|
||||
```
|
||||
|
||||
**After (BUI Tabs):**
|
||||
|
||||
```typescript
|
||||
import {Tabs, TabList, Tab, TabPanel} from '@backstage/ui';
|
||||
|
||||
<Tabs defaultSelectedKey = "tab1" >
|
||||
<TabList>
|
||||
<Tab id = "tab1" > Tab
|
||||
1 < /Tab>
|
||||
< Tab
|
||||
id = "tab2" > Tab
|
||||
2 < /Tab>
|
||||
< /TabList>
|
||||
< TabPanel
|
||||
id = "tab1" > Content
|
||||
1 < /TabPanel>
|
||||
< TabPanel
|
||||
id = "tab2" > Content
|
||||
2 < /TabPanel>
|
||||
< /Tabs>;
|
||||
```
|
||||
|
||||
### 11. Menu Pattern
|
||||
|
||||
**Before (MUI Menu):**
|
||||
|
||||
```typescript
|
||||
import {IconButton, Popover, MenuList, MenuItem} from '@material-ui/core';
|
||||
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
||||
|
||||
<IconButton onClick = {handleOpen} > <MoreVertIcon / > </IconButton>
|
||||
< Popover
|
||||
open = {open}
|
||||
anchorEl = {anchorEl}
|
||||
onClose = {handleClose} >
|
||||
<MenuList>
|
||||
<MenuItem onClick = {handleAction} > Action < /MenuItem>
|
||||
< /MenuList>
|
||||
< /Popover>
|
||||
```
|
||||
|
||||
**After (BUI Menu):**
|
||||
|
||||
```typescript
|
||||
import {ButtonIcon, Menu, MenuItem, MenuTrigger} from '@backstage/ui';
|
||||
import {RiMore2Line} from '@remixicon/react';
|
||||
|
||||
<MenuTrigger>
|
||||
<ButtonIcon aria - label = "more"
|
||||
icon = { < RiMore2Line / >
|
||||
}
|
||||
variant = "secondary" / >
|
||||
<Menu>
|
||||
<MenuItem onAction = {handleAction} > Action < /MenuItem>
|
||||
< /Menu>
|
||||
< /MenuTrigger>;
|
||||
```
|
||||
|
||||
### 12. List to HTML with CSS Modules
|
||||
|
||||
**Before (MUI List):**
|
||||
|
||||
```typescript
|
||||
import {List, ListItem, ListItemIcon, ListItemText} from '@material-ui/core';
|
||||
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<SomeIcon / >
|
||||
</ListItemIcon>
|
||||
< ListItemText
|
||||
primary = "Title"
|
||||
secondary = "Description" / >
|
||||
</ListItem>
|
||||
< /List>;
|
||||
```
|
||||
|
||||
**After (HTML list with BUI and CSS Modules):**
|
||||
|
||||
```css
|
||||
/* MyList.module.css */
|
||||
@layer components {
|
||||
.list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: var(--bui-space-2) 0;
|
||||
}
|
||||
|
||||
.listItemIcon {
|
||||
min-width: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--bui-fg-primary);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
import {Flex, Text} from '@backstage/ui';
|
||||
import {RiSomeIcon} from '@remixicon/react';
|
||||
import styles from './MyList.module.css';
|
||||
|
||||
<ul className = {styles.list} >
|
||||
<li className = {styles.listItem} >
|
||||
<div className = {styles.listItemIcon} >
|
||||
<RiSomeIcon size = {20}
|
||||
/>
|
||||
< /div>
|
||||
< Flex
|
||||
direction = "column" >
|
||||
<Text>Title < /Text>
|
||||
< Text
|
||||
variant = "body-small"
|
||||
color = "secondary" >
|
||||
Description
|
||||
< /Text>
|
||||
< /Flex>
|
||||
< /li>
|
||||
< /ul>;
|
||||
```
|
||||
|
||||
### 13. Chip to Tag
|
||||
|
||||
**Before (MUI Chip):**
|
||||
|
||||
```typescript
|
||||
import { Chip } from '@material-ui/core';
|
||||
|
||||
<Chip label="Category" size="small" />;
|
||||
```
|
||||
|
||||
**After (BUI Tag):**
|
||||
|
||||
```typescript
|
||||
import {Tag} from '@backstage/ui';
|
||||
|
||||
<Tag size = "small" > Category < /Tag>;
|
||||
```
|
||||
|
||||
### 14. Icons: MUI Icons to Remix Icons
|
||||
|
||||
**Before (MUI Icons):**
|
||||
|
||||
```typescript
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import SearchIcon from '@material-ui/icons/Search';
|
||||
|
||||
<CloseIcon / >
|
||||
<SearchIcon fontSize = "small" / >
|
||||
```
|
||||
|
||||
**After (Remix Icons):**
|
||||
|
||||
```typescript
|
||||
import {RiCloseLine, RiSearchLine} from '@remixicon/react';
|
||||
|
||||
<RiCloseLine / >
|
||||
<RiSearchLine size = {16}
|
||||
/>
|
||||
```
|
||||
|
||||
Common icon mappings:
|
||||
|
||||
| MUI Icon | Remix Icon |
|
||||
| ----------------- | -------------------- |
|
||||
| `Close` | `RiCloseLine` |
|
||||
| `Search` | `RiSearchLine` |
|
||||
| `Settings` | `RiSettingsLine` |
|
||||
| `Add` | `RiAddLine` |
|
||||
| `Delete` | `RiDeleteBinLine` |
|
||||
| `Edit` | `RiEditLine` |
|
||||
| `Check` | `RiCheckLine` |
|
||||
| `Error` | `RiErrorWarningLine` |
|
||||
| `Warning` | `RiAlertLine` |
|
||||
| `Info` | `RiInformationLine` |
|
||||
| `ExpandMore` | `RiArrowDownSLine` |
|
||||
| `ExpandLess` | `RiArrowUpSLine` |
|
||||
| `ChevronRight` | `RiArrowRightSLine` |
|
||||
| `ChevronLeft` | `RiArrowLeftSLine` |
|
||||
| `Menu` | `RiMenuLine` |
|
||||
| `MoreVert` | `RiMore2Line` |
|
||||
| `Visibility` | `RiEyeLine` |
|
||||
| `VisibilityOff` | `RiEyeOffLine` |
|
||||
| `NewReleases` | `RiMegaphoneLine` |
|
||||
| `RecordVoiceOver` | `RiMegaphoneLine` |
|
||||
| `Description` | `RiFileTextLine` |
|
||||
|
||||
Find more icons at: https://remixicon.com/
|
||||
|
||||
## CSS Variable Reference
|
||||
|
||||
### Spacing
|
||||
|
||||
| MUI theme.spacing() | BUI CSS Variable |
|
||||
| -------------------- | -------------------- |
|
||||
| `theme.spacing(0.5)` | `var(--bui-space-1)` |
|
||||
| `theme.spacing(1)` | `var(--bui-space-2)` |
|
||||
| `theme.spacing(1.5)` | `var(--bui-space-3)` |
|
||||
| `theme.spacing(2)` | `var(--bui-space-4)` |
|
||||
| `theme.spacing(3)` | `var(--bui-space-6)` |
|
||||
| `theme.spacing(4)` | `var(--bui-space-8)` |
|
||||
|
||||
### Colors
|
||||
|
||||
| MUI theme.palette | BUI CSS Variable |
|
||||
| -------------------- | ------------------------------------------ |
|
||||
| `text.primary` | `var(--bui-fg-primary)` |
|
||||
| `text.secondary` | `var(--bui-fg-secondary)` |
|
||||
| `background.paper` | `var(--bui-bg-surface-1)` |
|
||||
| `background.default` | `var(--bui-bg-surface-0)` |
|
||||
| `primary.main` | `var(--bui-bg-solid)` or `var(--bui-ring)` |
|
||||
| `error.main` | `var(--bui-fg-danger)` |
|
||||
| `action.hover` | `var(--bui-bg-hover)` |
|
||||
| `divider` | `var(--bui-border)` |
|
||||
|
||||
### Typography
|
||||
|
||||
| Property | BUI CSS Variable |
|
||||
| ------------------- | -------------------------------- |
|
||||
| Font family | `var(--bui-font-regular)` |
|
||||
| Font size small | `var(--bui-font-size-1)` |
|
||||
| Font size medium | `var(--bui-font-size-2)` |
|
||||
| Font size large | `var(--bui-font-size-3)` |
|
||||
| Font weight regular | `var(--bui-font-weight-regular)` |
|
||||
| Font weight bold | `var(--bui-font-weight-bold)` |
|
||||
|
||||
### Other
|
||||
|
||||
| Property | BUI CSS Variable |
|
||||
| -------------------- | ------------------------ |
|
||||
| Border radius small | `var(--bui-radius-2)` |
|
||||
| Border radius medium | `var(--bui-radius-3)` |
|
||||
| Border radius full | `var(--bui-radius-full)` |
|
||||
| Link color | `var(--bui-fg-link)` |
|
||||
|
||||
## Known Limitations
|
||||
|
||||
Some Backstage APIs still require MUI-compatible icon types:
|
||||
|
||||
- **NavItemBlueprint** (`@backstage/frontend-plugin-api`): The `icon` prop expects MUI `IconComponent` type. Remix icons
|
||||
are not type-compatible.
|
||||
- **Timeline** (`@material-ui/lab`): No BUI equivalent exists.
|
||||
- **Pagination** (`@material-ui/lab`): No BUI equivalent exists.
|
||||
- **Alert** (`@material-ui/lab`): No BUI equivalent exists.
|
||||
|
||||
For these cases, keep using MUI components.
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
When migrating a plugin:
|
||||
|
||||
1. [ ] Add `@backstage/ui` dependency
|
||||
2. [ ] Add `@remixicon/react` dependency (if using icons)
|
||||
3. [ ] Add `react-aria-components` dependency (if using Tooltip)
|
||||
4. [ ] Add CSS import to root file
|
||||
5. [ ] Remove `@material-ui/core` imports (except components with no BUI equivalent)
|
||||
6. [ ] Remove `@material-ui/icons` imports
|
||||
7. [ ] Remove `makeStyles` and related imports
|
||||
8. [ ] Create `.module.css` files for component styles
|
||||
9. [ ] Replace `Typography` with `Text`
|
||||
10. [ ] Replace `Box display="flex"` with `Flex`
|
||||
11. [ ] Replace `Grid container/item` with `Grid.Root/Grid.Item`
|
||||
12. [ ] Replace `Paper` with `Card`
|
||||
13. [ ] Replace MUI `Dialog` with BUI `DialogTrigger` pattern
|
||||
14. [ ] Replace MUI `Tooltip` with BUI `TooltipTrigger` pattern
|
||||
15. [ ] Replace MUI `Tabs` with BUI `Tabs`
|
||||
16. [ ] Replace MUI `Menu` with BUI `MenuTrigger` pattern
|
||||
17. [ ] Replace `Chip` with `Tag`
|
||||
18. [ ] Replace `IconButton` with `ButtonIcon`
|
||||
19. [ ] Update `Button` props (`disabled` → `isDisabled`, `variant="contained"` → `variant="primary"`)
|
||||
20. [ ] Update `TextField` props (`required` → `isRequired`, `onChange` signature)
|
||||
21. [ ] Replace MUI icons with Remix icons
|
||||
22. [ ] Run `yarn tsc` to check for type errors
|
||||
23. [ ] Run `yarn build` to verify build
|
||||
24. [ ] Run `yarn lint` to check for missing dependencies
|
||||
25. [ ] Test component rendering and functionality
|
||||
|
||||
## Reference
|
||||
|
||||
- BUI Documentation: https://ui.backstage.io
|
||||
- Remix Icons: https://remixicon.com/
|
||||
- Example Migration PR: https://github.com/backstage/backstage/pull/31631
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: DORA Metrics
|
||||
author: C2L2C
|
||||
authorUrl: https://github.com/C2L2C
|
||||
category: Monitoring
|
||||
description: Per-entity DORA metrics for catalog services. Computes Deployment Frequency, Lead Time, CFR & MTTR from GitHub PRs — multi-environment, trend history, no extra backend.
|
||||
documentation: https://github.com/C2L2C/backstage-plugin-dora-metrics#readme
|
||||
iconUrl: https://raw.githubusercontent.com/tailwindlabs/heroicons/master/src/24/outline/chart-bar.svg
|
||||
npmPackageName: '@c2l2c/backstage-plugin-dora-metrics'
|
||||
addedDate: '2026-03-18'
|
||||
status: active
|
||||
@@ -19,6 +19,8 @@
|
||||
/** @type{import('prism-react-renderer').PrismTheme} **/
|
||||
// @ts-ignore
|
||||
import { themes } from 'prism-react-renderer';
|
||||
import { cpSync, existsSync } from 'node:fs';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
import type * as Preset from '@docusaurus/preset-classic';
|
||||
import { Config } from '@docusaurus/types';
|
||||
import RedirectPlugin from '@docusaurus/plugin-client-redirects';
|
||||
@@ -28,7 +30,9 @@ import type * as OpenApiPlugin from 'docusaurus-plugin-openapi-docs';
|
||||
const backstageTheme = themes.vsDark;
|
||||
backstageTheme.plain.backgroundColor = '#232323';
|
||||
|
||||
const useVersionedDocs = require('node:fs').existsSync('versions.json');
|
||||
const useVersionedDocs = existsSync('versions.json');
|
||||
const wellKnownDocsPath = resolvePath(__dirname, '../docs/.well-known');
|
||||
const wellKnownPublicPath = '/.well-known';
|
||||
|
||||
// This patches the redirect plugin to ignore the error when it tries to override existing fields.
|
||||
// This lets us add redirects that only apply to the next docs, while the stable docs still contain the source path.
|
||||
@@ -253,6 +257,40 @@ const config: Config = {
|
||||
};
|
||||
},
|
||||
}),
|
||||
() => ({
|
||||
name: 'publish-well-known-docs',
|
||||
getPathsToWatch() {
|
||||
return [wellKnownDocsPath];
|
||||
},
|
||||
configureWebpack() {
|
||||
if (!existsSync(wellKnownDocsPath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
devServer: {
|
||||
static: [
|
||||
{
|
||||
publicPath: wellKnownPublicPath,
|
||||
directory: wellKnownDocsPath,
|
||||
staticOptions: {
|
||||
dotfiles: 'allow',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
async postBuild({ outDir }: { outDir: string }) {
|
||||
if (!existsSync(wellKnownDocsPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
cpSync(wellKnownDocsPath, resolvePath(outDir, '.well-known'), {
|
||||
recursive: true,
|
||||
});
|
||||
},
|
||||
}),
|
||||
ctx =>
|
||||
PatchedRedirectPlugin(ctx, {
|
||||
id: '@docusaurus/plugin-client-redirects',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,3 +27,13 @@ fast-xml-builder@*:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz#a485d7e8381f1db983cf006f849d1066e2935241"
|
||||
integrity sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==
|
||||
|
||||
"knex@npm:3":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/knex/-/knex-3.1.0.tgz#b6ddd5b5ad26a6315234a5b09ec38dc4a370bd8c"
|
||||
integrity sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==
|
||||
|
||||
"knex@npm:^3.0.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/knex/-/knex-3.1.0.tgz#b6ddd5b5ad26a6315234a5b09ec38dc4a370bd8c"
|
||||
integrity sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==
|
||||
|
||||
@@ -219,4 +219,14 @@
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bui-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1090,6 +1090,62 @@ describe('OidcService', () => {
|
||||
).rejects.toThrow('Invalid redirect_uri');
|
||||
});
|
||||
|
||||
it('should accept loopback redirect_uri with a different port per RFC 8252', async () => {
|
||||
mockFetchCimdMetadata.mockResolvedValue({
|
||||
...cimdMetadata,
|
||||
redirectUris: ['http://localhost/callback'],
|
||||
});
|
||||
|
||||
const { service } = await createOidcService({
|
||||
databaseId,
|
||||
config: {
|
||||
auth: {
|
||||
experimentalClientIdMetadataDocuments: { enabled: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authSession = await service.createAuthorizationSession({
|
||||
clientId: cimdClientId,
|
||||
redirectUri: 'http://localhost:60056/callback',
|
||||
responseType: 'code',
|
||||
scope: 'openid',
|
||||
...pkceParams,
|
||||
});
|
||||
|
||||
expect(authSession).toEqual({
|
||||
id: expect.any(String),
|
||||
clientName: 'CIMD Test Client',
|
||||
scope: 'openid',
|
||||
redirectUri: 'http://localhost:60056/callback',
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject non-loopback redirect_uri with a different port', async () => {
|
||||
mockFetchCimdMetadata.mockResolvedValue({
|
||||
...cimdMetadata,
|
||||
redirectUris: ['https://example.com/callback'],
|
||||
});
|
||||
|
||||
const { service } = await createOidcService({
|
||||
databaseId,
|
||||
config: {
|
||||
auth: {
|
||||
experimentalClientIdMetadataDocuments: { enabled: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.createAuthorizationSession({
|
||||
clientId: cimdClientId,
|
||||
redirectUri: 'https://example.com:9999/callback',
|
||||
responseType: 'code',
|
||||
...pkceParams,
|
||||
}),
|
||||
).rejects.toThrow('Redirect URI not registered');
|
||||
});
|
||||
|
||||
it('should reject redirect_uri when CIMD metadata uses wildcard patterns', async () => {
|
||||
mockFetchCimdMetadata.mockResolvedValue({
|
||||
...cimdMetadata,
|
||||
|
||||
@@ -45,6 +45,39 @@ function validateRedirectUri(
|
||||
}
|
||||
}
|
||||
|
||||
const LOOPBACK_HOSTS = ['localhost', '127.0.0.1', '[::1]'];
|
||||
|
||||
/**
|
||||
* RFC 8252 Section 7.3: For loopback redirect URIs, the authorization server
|
||||
* MUST allow any port to be specified at the time of the request. This matches
|
||||
* redirect URIs by scheme, hostname, and path only, ignoring the port.
|
||||
*/
|
||||
function matchesRedirectUri(
|
||||
requestUri: string,
|
||||
registeredUris: string[],
|
||||
): boolean {
|
||||
const requested = new URL(requestUri);
|
||||
|
||||
if (!LOOPBACK_HOSTS.includes(requested.hostname)) {
|
||||
return registeredUris.includes(requestUri);
|
||||
}
|
||||
|
||||
return registeredUris.some(registered => {
|
||||
let reg: URL;
|
||||
try {
|
||||
reg = new URL(registered);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
LOOPBACK_HOSTS.includes(reg.hostname) &&
|
||||
reg.protocol === requested.protocol &&
|
||||
reg.hostname === requested.hostname &&
|
||||
reg.pathname === requested.pathname
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export class OidcService {
|
||||
private readonly auth: AuthService;
|
||||
private readonly tokenIssuer: TokenIssuer;
|
||||
@@ -322,7 +355,7 @@ export class OidcService {
|
||||
if (opts.redirectUri) {
|
||||
validateRedirectUri(opts.redirectUri, cimd.allowedRedirectUriPatterns);
|
||||
|
||||
if (!cimdClient.redirectUris.includes(opts.redirectUri)) {
|
||||
if (!matchesRedirectUri(opts.redirectUri, cimdClient.redirectUris)) {
|
||||
throw new InputError('Redirect URI not registered');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user