+ {partners.map(({ name, url, logo, height }, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/microsite/static/img/incident-io-logo.png b/microsite/static/img/incident-io-logo.png
new file mode 100644
index 0000000000..ac12718808
Binary files /dev/null and b/microsite/static/img/incident-io-logo.png differ
diff --git a/microsite/static/img/partner-logo-frontside-blk.png b/microsite/static/img/partner-logo-frontside-blk.png
index 16b14ac588..87f0fec87c 100644
Binary files a/microsite/static/img/partner-logo-frontside-blk.png and b/microsite/static/img/partner-logo-frontside-blk.png differ
diff --git a/microsite/static/img/partner-logo-frontside.png b/microsite/static/img/partner-logo-frontside.png
index c8cad6bce1..ecce0b5d69 100644
Binary files a/microsite/static/img/partner-logo-frontside.png and b/microsite/static/img/partner-logo-frontside.png differ
diff --git a/microsite/static/img/partner-logo-roadie-blk.png b/microsite/static/img/partner-logo-roadie-blk.png
index a837d3f6f5..2c5ad25be5 100644
Binary files a/microsite/static/img/partner-logo-roadie-blk.png and b/microsite/static/img/partner-logo-roadie-blk.png differ
diff --git a/microsite/static/img/partner-logo-roadie.png b/microsite/static/img/partner-logo-roadie.png
index 41b169f55b..23182e602a 100644
Binary files a/microsite/static/img/partner-logo-roadie.png and b/microsite/static/img/partner-logo-roadie.png differ
diff --git a/microsite/static/img/partner-logo-spotify-blk.png b/microsite/static/img/partner-logo-spotify-blk.png
new file mode 100644
index 0000000000..cfe9107765
Binary files /dev/null and b/microsite/static/img/partner-logo-spotify-blk.png differ
diff --git a/microsite/static/img/partner-logo-spotify.png b/microsite/static/img/partner-logo-spotify.png
new file mode 100644
index 0000000000..826ce233a9
Binary files /dev/null and b/microsite/static/img/partner-logo-spotify.png differ
diff --git a/microsite/static/img/partner-logo-statusneo-blk.png b/microsite/static/img/partner-logo-statusneo-blk.png
index 78fb9bd10e..77eeb704bb 100644
Binary files a/microsite/static/img/partner-logo-statusneo-blk.png and b/microsite/static/img/partner-logo-statusneo-blk.png differ
diff --git a/microsite/static/img/partner-logo-statusneo.png b/microsite/static/img/partner-logo-statusneo.png
index 61a676a220..1502de6e2b 100644
Binary files a/microsite/static/img/partner-logo-statusneo.png and b/microsite/static/img/partner-logo-statusneo.png differ
diff --git a/microsite/static/img/partner-logo-thoughtworks-blk.png b/microsite/static/img/partner-logo-thoughtworks-blk.png
index a9a4894ccf..87cb928c27 100644
Binary files a/microsite/static/img/partner-logo-thoughtworks-blk.png and b/microsite/static/img/partner-logo-thoughtworks-blk.png differ
diff --git a/microsite/static/img/partner-logo-thoughtworks.png b/microsite/static/img/partner-logo-thoughtworks.png
index 53c3bc2a9e..a481f81143 100644
Binary files a/microsite/static/img/partner-logo-thoughtworks.png and b/microsite/static/img/partner-logo-thoughtworks.png differ
diff --git a/microsite/static/img/scaffolder-studio.svg b/microsite/static/img/scaffolder-studio.svg
new file mode 100644
index 0000000000..798e86c678
--- /dev/null
+++ b/microsite/static/img/scaffolder-studio.svg
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/microsite/static/img/testkube-logo.svg b/microsite/static/img/testkube-logo.svg
new file mode 100644
index 0000000000..7416317ed9
--- /dev/null
+++ b/microsite/static/img/testkube-logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/microsite/yarn.lock b/microsite/yarn.lock
index 3f17fdd373..c7bb41bf98 100644
--- a/microsite/yarn.lock
+++ b/microsite/yarn.lock
@@ -5529,12 +5529,12 @@ __metadata:
linkType: hard
"brace-expansion@npm:^1.1.7":
- version: 1.1.12
- resolution: "brace-expansion@npm:1.1.12"
+ version: 1.1.13
+ resolution: "brace-expansion@npm:1.1.13"
dependencies:
balanced-match: "npm:^1.0.0"
concat-map: "npm:0.0.1"
- checksum: 10/12cb6d6310629e3048cadb003e1aca4d8c9bb5c67c3c321bafdd7e7a50155de081f78ea3e0ed92ecc75a9015e784f301efc8132383132f4f7904ad1ac529c562
+ checksum: 10/b5f4329fdbe9d2e25fa250c8f866ebd054ba946179426e99b86dcccddabdb1d481f0e40ee5430032e62a7d0a6c2837605ace6783d015aa1d65d85ca72154d936
languageName: node
linkType: hard
@@ -7614,12 +7614,12 @@ __metadata:
linkType: hard
"follow-redirects@npm:^1.0.0":
- version: 1.15.6
- resolution: "follow-redirects@npm:1.15.6"
+ version: 1.16.0
+ resolution: "follow-redirects@npm:1.16.0"
peerDependenciesMeta:
debug:
optional: true
- checksum: 10/70c7612c4cab18e546e36b991bbf8009a1a41cf85354afe04b113d1117569abf760269409cb3eb842d9f7b03d62826687086b081c566ea7b1e6613cf29030bf7
+ checksum: 10/3fbe3d80b3b544c22705d837aa5d4a0d07a740d913534a2620b0a004c610af4148e3b58723536dd099aaa1c9d3a155964bde9665d6e5cb331460809a1fc572fd
languageName: node
linkType: hard
@@ -12273,13 +12273,13 @@ __metadata:
linkType: hard
"postcss@npm:^8.4.21, postcss@npm:^8.4.24, postcss@npm:^8.4.33, postcss@npm:^8.5.4":
- version: 8.5.6
- resolution: "postcss@npm:8.5.6"
+ version: 8.5.10
+ resolution: "postcss@npm:8.5.10"
dependencies:
nanoid: "npm:^3.3.11"
picocolors: "npm:^1.1.1"
source-map-js: "npm:^1.2.1"
- checksum: 10/9e4fbe97574091e9736d0e82a591e29aa100a0bf60276a926308f8c57249698935f35c5d2f4e80de778d0cbb8dcffab4f383d85fd50c5649aca421c3df729b86
+ checksum: 10/7eac6169e535b63c8412e94d4f6047fc23efa3e9dde804b541940043c831b25f1cd867d83cd2c4371ad2450c8abcb42c208aa25668c1f0f3650d7f72faf711a8
languageName: node
linkType: hard
diff --git a/mkdocs.yml b/mkdocs.yml
index e01ab2a431..5f02cfeaa2 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -62,6 +62,7 @@ nav:
- Extending the model: 'features/software-catalog/extending-the-model.md'
- External integrations: 'features/software-catalog/external-integrations.md'
- Catalog Customization: 'features/software-catalog/catalog-customization.md'
+ - Entity Presentation: 'features/software-catalog/entity-presentation.md'
- API: 'features/software-catalog/api.md'
- FAQ: 'features/software-catalog/faq.md'
- Kubernetes:
@@ -221,7 +222,6 @@ nav:
- React Router 6.0 Migration: 'tutorials/react-router-stable-migration.md'
- Package Role Migration: 'tutorials/package-role-migration.md'
- Migrating away from @backstage/core: 'tutorials/migrating-away-from-core.md'
- - Adding Custom Plugin to Existing Monorepo App: 'tutorials/quickstart-app-plugin.md'
- Manual Rollback using Knex: 'tutorials/manual-knex-rollback.md'
- Switching Backstage from SQLite to PostgreSQL: 'tutorials/switching-sqlite-postgres.md'
- Using the Backstage Proxy from Within a Plugin: 'tutorials/using-backstage-proxy-within-plugin.md'
diff --git a/package.json b/package.json
index 93a6317951..ff247ce01f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "root",
- "version": "1.50.0-next.0",
+ "version": "1.51.0-next.2",
"backstage": {
"cli": {
"new": {
@@ -99,6 +99,7 @@
"rejectFrontendNetworkRequests": true
},
"resolutions": {
+ "@remixicon/react": ">=4.6.0 <4.9.0",
"@changesets/assemble-release-plan@^6.0.0": "patch:@changesets/assemble-release-plan@npm%3A6.0.0#./.yarn/patches/@changesets-assemble-release-plan-npm-6.0.0-f7b3005037.patch",
"@material-ui/pickers@^3.2.10": "patch:@material-ui/pickers@npm%3A3.3.11#./.yarn/patches/@material-ui-pickers-npm-3.3.11-1c8f68ea20.patch",
"@material-ui/pickers@^3.3.10": "patch:@material-ui/pickers@npm%3A3.3.11#./.yarn/patches/@material-ui-pickers-npm-3.3.11-1c8f68ea20.patch",
@@ -132,7 +133,7 @@
"@backstage/eslint-plugin": "workspace:*",
"@backstage/repo-tools": "workspace:*",
"@changesets/cli": "^2.14.0",
- "@jest/environment-jsdom-abstract": "^30",
+ "@jest/environment-jsdom-abstract": "~30.2.0",
"@octokit/rest": "^19.0.3",
"@playwright/test": "^1.32.3",
"@spotify/eslint-plugin": "^15.0.0",
@@ -161,7 +162,7 @@
"eslint-plugin-testing-library": "^7.0.0",
"fs-extra": "^11.2.0",
"husky": "^9.0.0",
- "jest": "^30",
+ "jest": "~30.2.0",
"js-yaml": "^4.1.1",
"jsdom": "^27",
"lint-staged": "^16.0.0",
diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md
index 7940adaf3d..adecadbf90 100644
--- a/packages/app-defaults/CHANGELOG.md
+++ b/packages/app-defaults/CHANGELOG.md
@@ -1,5 +1,49 @@
# @backstage/app-defaults
+## 1.7.8-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.18.10-next.0
+ - @backstage/core-app-api@1.20.1-next.0
+ - @backstage/core-plugin-api@1.12.6-next.0
+ - @backstage/theme@0.7.3
+ - @backstage/plugin-permission-react@0.5.1-next.0
+
+## 1.7.7
+
+### Patch Changes
+
+- 400aa23: Added `FetchMiddlewares.clarifyFailures()` to the default fetch API middleware stack.
+- Updated dependencies
+ - @backstage/theme@0.7.3
+ - @backstage/core-app-api@1.20.0
+ - @backstage/core-components@0.18.9
+ - @backstage/plugin-permission-react@0.5.0
+ - @backstage/core-plugin-api@1.12.5
+
+## 1.7.7-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.7.3-next.0
+ - @backstage/core-app-api@1.20.0-next.2
+ - @backstage/core-components@0.18.9-next.1
+ - @backstage/core-plugin-api@1.12.5-next.2
+ - @backstage/plugin-permission-react@0.4.42-next.1
+
+## 1.7.7-next.1
+
+### Patch Changes
+
+- 400aa23: Added `FetchMiddlewares.clarifyFailures()` to the default fetch API middleware stack.
+- Updated dependencies
+ - @backstage/core-app-api@1.20.0-next.1
+ - @backstage/core-components@0.18.9-next.0
+ - @backstage/core-plugin-api@1.12.5-next.1
+
## 1.7.7-next.0
### Patch Changes
diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json
index c5519ff8b7..04aabbde6f 100644
--- a/packages/app-defaults/package.json
+++ b/packages/app-defaults/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/app-defaults",
- "version": "1.7.7-next.0",
+ "version": "1.7.8-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 91d782ed26..fd881f6380 100644
--- a/packages/app-example-plugin/CHANGELOG.md
+++ b/packages/app-example-plugin/CHANGELOG.md
@@ -1,5 +1,29 @@
# app-example-plugin
+## 0.0.35-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.18.10-next.0
+ - @backstage/frontend-plugin-api@0.17.0-next.0
+
+## 0.0.34
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/frontend-plugin-api@0.16.0
+ - @backstage/core-components@0.18.9
+
+## 0.0.34-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/frontend-plugin-api@0.16.0-next.1
+ - @backstage/core-components@0.18.9-next.0
+
## 0.0.34-next.0
### Patch Changes
diff --git a/packages/app-example-plugin/knip-report.md b/packages/app-example-plugin/knip-report.md
index dfb081c3f9..52915c86db 100644
--- a/packages/app-example-plugin/knip-report.md
+++ b/packages/app-example-plugin/knip-report.md
@@ -2,7 +2,8 @@
## Unused devDependencies (2)
-| Name | Location | Severity |
-| :---------- | :----------- | :------- |
-| cross-fetch | packages/app-example-plugin/package.json | error |
-| msw | packages/app-example-plugin/package.json | error |
+| Name | Location | Severity |
+| :---------- | :---------------- | :------- |
+| cross-fetch | package.json:51:6 | error |
+| msw | package.json:52:6 | error |
+
diff --git a/packages/app-example-plugin/package.json b/packages/app-example-plugin/package.json
index 95aaa962f7..6c80c28f1f 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.34-next.0",
+ "version": "0.0.35-next.0",
"description": "Backstage internal example plugin",
"backstage": {
"role": "frontend-plugin",
diff --git a/packages/app-example-plugin/report.api.md b/packages/app-example-plugin/report.api.md
index 432174da68..0879f318aa 100644
--- a/packages/app-example-plugin/report.api.md
+++ b/packages/app-example-plugin/report.api.md
@@ -27,8 +27,8 @@ const examplePlugin: OverridableFrontendPlugin<
title: string | undefined;
};
configInput: {
- title?: string | undefined;
path?: string | undefined;
+ title?: string | undefined;
};
output:
| ExtensionDataRef
diff --git a/packages/app-legacy/CHANGELOG.md b/packages/app-legacy/CHANGELOG.md
index 21596b4138..312b6d06bf 100644
--- a/packages/app-legacy/CHANGELOG.md
+++ b/packages/app-legacy/CHANGELOG.md
@@ -1,5 +1,171 @@
# example-app-legacy
+## 0.2.121-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.18.10-next.0
+ - @backstage/ui@0.15.0-next.0
+ - @backstage/plugin-notifications@0.5.17-next.0
+ - @backstage/frontend-app-api@0.16.3-next.0
+ - @backstage/plugin-home@0.9.5-next.0
+ - @backstage/plugin-techdocs@1.17.6-next.0
+ - @backstage/plugin-scaffolder@1.36.3-next.0
+ - @backstage/cli@0.36.2-next.0
+ - @backstage/plugin-catalog-react@2.1.5-next.0
+ - @backstage/plugin-catalog@2.0.5-next.0
+ - @backstage/plugin-catalog-graph@0.6.4-next.0
+ - @backstage/plugin-techdocs-react@1.3.11-next.0
+ - @backstage/plugin-catalog-import@0.13.13-next.0
+ - @backstage/plugin-search-react@1.11.4-next.0
+ - @backstage/plugin-search@1.7.4-next.0
+ - @backstage/plugin-org@0.7.4-next.0
+ - @backstage/app-defaults@1.7.8-next.0
+ - @backstage/integration-react@1.2.18-next.0
+ - @backstage/plugin-api-docs@0.14.1-next.0
+ - @backstage/plugin-auth-react@0.1.27-next.0
+ - @backstage/plugin-catalog-unprocessed-entities@0.2.30-next.0
+ - @backstage/plugin-devtools@0.1.39-next.0
+ - @backstage/plugin-home-react@0.1.38-next.0
+ - @backstage/plugin-kubernetes@0.12.19-next.0
+ - @backstage/plugin-kubernetes-cluster@0.0.37-next.0
+ - @backstage/plugin-scaffolder-react@1.20.2-next.0
+ - @backstage/plugin-signals@0.0.31-next.0
+ - @backstage/plugin-techdocs-module-addons-contrib@1.1.36-next.0
+ - @backstage/plugin-user-settings@0.9.3-next.0
+ - @backstage/core-app-api@1.20.1-next.0
+ - @backstage/plugin-mui-to-bui@0.2.7-next.0
+ - @backstage/catalog-model@1.8.1-next.0
+ - @backstage/config@1.3.8-next.0
+ - @backstage/core-plugin-api@1.12.6-next.0
+ - @backstage/theme@0.7.3
+ - @backstage/plugin-catalog-common@1.1.10-next.0
+ - @backstage/plugin-permission-react@0.5.1-next.0
+ - @backstage/plugin-search-common@1.2.24-next.0
+
+## 0.2.120
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/ui@0.14.0
+ - @backstage/app-defaults@1.7.7
+ - @backstage/theme@0.7.3
+ - @backstage/cli@0.36.1
+ - @backstage/plugin-catalog@2.0.2
+ - @backstage/plugin-catalog-graph@0.6.1
+ - @backstage/plugin-catalog-import@0.13.12
+ - @backstage/catalog-model@1.8.0
+ - @backstage/plugin-catalog-react@2.1.2
+ - @backstage/core-app-api@1.20.0
+ - @backstage/plugin-org@0.7.1
+ - @backstage/core-components@0.18.9
+ - @backstage/plugin-catalog-unprocessed-entities@0.2.29
+ - @backstage/plugin-scaffolder-react@1.20.1
+ - @backstage/plugin-scaffolder@1.36.2
+ - @backstage/frontend-app-api@0.16.2
+ - @backstage/plugin-techdocs@1.17.3
+ - @backstage/plugin-kubernetes@0.12.18
+ - @backstage/plugin-notifications@0.5.16
+ - @backstage/plugin-search@1.7.1
+ - @backstage/plugin-api-docs@0.14.0
+ - @backstage/plugin-permission-react@0.5.0
+ - @backstage/plugin-signals@0.0.30
+ - @backstage/plugin-devtools@0.1.38
+ - @backstage/plugin-kubernetes-cluster@0.0.36
+ - @backstage/config@1.3.7
+ - @backstage/core-plugin-api@1.12.5
+ - @backstage/integration-react@1.2.17
+ - @backstage/plugin-auth-react@0.1.26
+ - @backstage/plugin-catalog-common@1.1.9
+ - @backstage/plugin-home@0.9.4
+ - @backstage/plugin-home-react@0.1.37
+ - @backstage/plugin-mui-to-bui@0.2.6
+ - @backstage/plugin-search-common@1.2.23
+ - @backstage/plugin-search-react@1.11.1
+ - @backstage/plugin-techdocs-module-addons-contrib@1.1.35
+ - @backstage/plugin-techdocs-react@1.3.10
+ - @backstage/plugin-user-settings@0.9.2
+
+## 0.2.120-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/ui@0.14.0-next.2
+ - @backstage/theme@0.7.3-next.0
+ - @backstage/plugin-catalog@2.0.2-next.2
+ - @backstage/core-app-api@1.20.0-next.2
+ - @backstage/cli@0.36.1-next.2
+ - @backstage/core-components@0.18.9-next.1
+ - @backstage/plugin-catalog-import@0.13.12-next.2
+ - @backstage/plugin-catalog-react@2.1.2-next.2
+ - @backstage/plugin-catalog-unprocessed-entities@0.2.29-next.2
+ - @backstage/plugin-notifications@0.5.16-next.2
+ - @backstage/plugin-catalog-graph@0.6.1-next.2
+ - @backstage/plugin-scaffolder@1.36.2-next.2
+ - @backstage/plugin-techdocs@1.17.3-next.2
+ - @backstage/catalog-model@1.7.8-next.0
+ - @backstage/config@1.3.7-next.0
+ - @backstage/core-plugin-api@1.12.5-next.2
+ - @backstage/frontend-app-api@0.16.2-next.2
+ - @backstage/plugin-auth-react@0.1.26-next.1
+ - @backstage/plugin-devtools@0.1.38-next.2
+ - @backstage/plugin-search@1.7.1-next.2
+ - @backstage/plugin-user-settings@0.9.2-next.2
+ - @backstage/app-defaults@1.7.7-next.2
+ - @backstage/plugin-home@0.9.4-next.2
+ - @backstage/plugin-mui-to-bui@0.2.6-next.2
+ - @backstage/plugin-scaffolder-react@1.20.1-next.2
+ - @backstage/plugin-search-react@1.11.1-next.2
+ - @backstage/plugin-signals@0.0.30-next.2
+ - @backstage/plugin-techdocs-react@1.3.10-next.2
+ - @backstage/integration-react@1.2.17-next.1
+ - @backstage/plugin-techdocs-module-addons-contrib@1.1.35-next.2
+ - @backstage/plugin-org@0.7.1-next.2
+ - @backstage/plugin-api-docs@0.13.6-next.2
+ - @backstage/plugin-catalog-common@1.1.9-next.0
+ - @backstage/plugin-kubernetes@0.12.18-next.2
+ - @backstage/plugin-kubernetes-cluster@0.0.36-next.1
+ - @backstage/plugin-permission-react@0.4.42-next.1
+ - @backstage/plugin-search-common@1.2.23-next.0
+
+## 0.2.120-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/app-defaults@1.7.7-next.1
+ - @backstage/cli@0.36.1-next.1
+ - @backstage/ui@0.14.0-next.1
+ - @backstage/plugin-catalog-graph@0.6.1-next.1
+ - @backstage/plugin-catalog-import@0.13.12-next.1
+ - @backstage/core-app-api@1.20.0-next.1
+ - @backstage/plugin-catalog-react@2.1.2-next.1
+ - @backstage/plugin-org@0.7.1-next.1
+ - @backstage/frontend-app-api@0.16.2-next.1
+ - @backstage/plugin-kubernetes@0.12.18-next.1
+ - @backstage/plugin-notifications@0.5.16-next.1
+ - @backstage/plugin-scaffolder@1.36.2-next.1
+ - @backstage/plugin-search@1.7.1-next.1
+ - @backstage/plugin-signals@0.0.30-next.1
+ - @backstage/core-components@0.18.9-next.0
+ - @backstage/core-plugin-api@1.12.5-next.1
+ - @backstage/plugin-api-docs@0.13.6-next.1
+ - @backstage/plugin-catalog@2.0.2-next.1
+ - @backstage/plugin-home@0.9.4-next.1
+ - @backstage/plugin-scaffolder-react@1.20.1-next.1
+ - @backstage/plugin-search-react@1.11.1-next.1
+ - @backstage/plugin-techdocs@1.17.3-next.1
+ - @backstage/plugin-user-settings@0.9.2-next.1
+ - @backstage/plugin-catalog-unprocessed-entities@0.2.29-next.1
+ - @backstage/plugin-devtools@0.1.38-next.1
+ - @backstage/plugin-home-react@0.1.37-next.1
+ - @backstage/plugin-mui-to-bui@0.2.6-next.1
+ - @backstage/plugin-techdocs-module-addons-contrib@1.1.35-next.1
+ - @backstage/plugin-techdocs-react@1.3.10-next.1
+
## 0.2.120-next.0
### Patch Changes
diff --git a/packages/app-legacy/knip-report.md b/packages/app-legacy/knip-report.md
index d9c3463946..a04cab24f5 100644
--- a/packages/app-legacy/knip-report.md
+++ b/packages/app-legacy/knip-report.md
@@ -2,27 +2,29 @@
## Unused dependencies (8)
-| Name | Location | Severity |
-| :------------------------------ | :----------- | :------- |
-| @backstage/plugin-search-common | packages/app-legacy/package.json | error |
-| @backstage/plugin-auth-react | packages/app-legacy/package.json | error |
-| @backstage/frontend-app-api | packages/app-legacy/package.json | error |
-| @material-ui/lab | packages/app-legacy/package.json | error |
-| zen-observable | packages/app-legacy/package.json | error |
-| @octokit/rest | packages/app-legacy/package.json | error |
-| react-router | packages/app-legacy/package.json | error |
-| history | packages/app-legacy/package.json | error |
+| Name | Location | Severity |
+| :------------------------------ | :---------------- | :------- |
+| @backstage/plugin-search-common | package.json:67:6 | error |
+| @backstage/plugin-auth-react | package.json:48:6 | error |
+| @backstage/plugin-home-react | package.json:57:6 | error |
+| @backstage/frontend-app-api | package.json:45:6 | error |
+| @material-ui/lab | package.json:78:6 | error |
+| zen-observable | package.json:86:6 | error |
+| @octokit/rest | package.json:79:6 | error |
+| history | package.json:80:6 | error |
-## Unused devDependencies (3)
+## Unused devDependencies (4)
-| Name | Location | Severity |
-| :-------------------------- | :----------- | :------- |
-| @testing-library/user-event | packages/app-legacy/package.json | error |
-| @types/zen-observable | packages/app-legacy/package.json | error |
-| @types/jquery | packages/app-legacy/package.json | error |
+| Name | Location | Severity |
+| :-------------------------- | :---------------- | :------- |
+| @testing-library/user-event | package.json:94:6 | error |
+| @types/zen-observable | package.json:98:6 | error |
+| @playwright/test | package.json:90:6 | error |
+| @types/jquery | package.json:95:6 | error |
## Unlisted dependencies (1)
-| Name | Location | Severity |
-| :---------- | :------------------------------------------------------- | :------- |
-| @rjsf/utils | packages/app/src/components/scaffolder/customScaffolderExtensions.tsx | error |
+| Name | Location | Severity |
+| :---------- | :------------------------------------------------------------- | :------- |
+| @rjsf/utils | src/components/scaffolder/customScaffolderExtensions.tsx:16:39 | error |
+
diff --git a/packages/app-legacy/package.json b/packages/app-legacy/package.json
index 862c14d536..b908be897c 100644
--- a/packages/app-legacy/package.json
+++ b/packages/app-legacy/package.json
@@ -1,6 +1,6 @@
{
"name": "example-app-legacy",
- "version": "0.2.120-next.0",
+ "version": "0.2.121-next.0",
"backstage": {
"role": "frontend"
},
@@ -77,6 +77,7 @@
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"@octokit/rest": "^19.0.3",
+ "@rjsf/utils": "5.24.13",
"history": "^5.0.0",
"react": "^18.0.2",
"react-dom": "^18.0.2",
@@ -96,7 +97,7 @@
"@types/react": "*",
"@types/react-dom": "*",
"@types/zen-observable": "^0.8.0",
- "axios": "^1.13.0",
+ "axios": "^1.15.0",
"cross-env": "^10.0.0",
"msw": "^1.0.0"
},
diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md
index 2ebabb9198..ac9fdf7c77 100644
--- a/packages/app/CHANGELOG.md
+++ b/packages/app/CHANGELOG.md
@@ -1,5 +1,193 @@
# example-app
+## 0.0.35-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.18.10-next.0
+ - @backstage/ui@0.15.0-next.0
+ - @backstage/plugin-notifications@0.5.17-next.0
+ - @backstage/plugin-app@0.4.6-next.0
+ - @backstage/plugin-app-visualizer@0.2.4-next.0
+ - @backstage/frontend-app-api@0.16.3-next.0
+ - @backstage/plugin-home@0.9.5-next.0
+ - @backstage/plugin-techdocs@1.17.6-next.0
+ - @backstage/plugin-scaffolder@1.36.3-next.0
+ - @backstage/cli@0.36.2-next.0
+ - @backstage/core-compat-api@0.5.11-next.0
+ - @backstage/plugin-catalog-react@2.1.5-next.0
+ - @backstage/frontend-plugin-api@0.17.0-next.0
+ - @backstage/plugin-catalog@2.0.5-next.0
+ - @backstage/plugin-catalog-graph@0.6.4-next.0
+ - @backstage/plugin-techdocs-react@1.3.11-next.0
+ - @backstage/plugin-catalog-import@0.13.13-next.0
+ - @backstage/plugin-search-react@1.11.4-next.0
+ - @backstage/plugin-search@1.7.4-next.0
+ - @backstage/plugin-org@0.7.4-next.0
+ - @backstage/app-defaults@1.7.8-next.0
+ - @backstage/frontend-defaults@0.5.2-next.0
+ - @backstage/integration-react@1.2.18-next.0
+ - @backstage/plugin-api-docs@0.14.1-next.0
+ - @backstage/plugin-auth-react@0.1.27-next.0
+ - @backstage/plugin-catalog-unprocessed-entities@0.2.30-next.0
+ - @backstage/plugin-devtools@0.1.39-next.0
+ - @backstage/plugin-home-react@0.1.38-next.0
+ - @backstage/plugin-kubernetes@0.12.19-next.0
+ - @backstage/plugin-kubernetes-cluster@0.0.37-next.0
+ - @backstage/plugin-scaffolder-react@1.20.2-next.0
+ - @backstage/plugin-signals@0.0.31-next.0
+ - @backstage/plugin-techdocs-module-addons-contrib@1.1.36-next.0
+ - @backstage/plugin-user-settings@0.9.3-next.0
+ - @backstage/core-app-api@1.20.1-next.0
+ - @backstage/plugin-auth@0.1.8-next.0
+ - @backstage/catalog-model@1.8.1-next.0
+ - @backstage/config@1.3.8-next.0
+ - @backstage/core-plugin-api@1.12.6-next.0
+ - @backstage/plugin-app-react@0.2.3-next.0
+ - @backstage/theme@0.7.3
+ - @backstage/plugin-catalog-common@1.1.10-next.0
+ - @backstage/plugin-permission-react@0.5.1-next.0
+ - @backstage/plugin-search-common@1.2.24-next.0
+
+## 0.0.34
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/ui@0.14.0
+ - @backstage/plugin-app@0.4.3
+ - @backstage/app-defaults@1.7.7
+ - @backstage/theme@0.7.3
+ - @backstage/cli@0.36.1
+ - @backstage/plugin-catalog@2.0.2
+ - @backstage/plugin-catalog-graph@0.6.1
+ - @backstage/plugin-catalog-import@0.13.12
+ - @backstage/catalog-model@1.8.0
+ - @backstage/plugin-catalog-react@2.1.2
+ - @backstage/core-app-api@1.20.0
+ - @backstage/plugin-org@0.7.1
+ - @backstage/frontend-plugin-api@0.16.0
+ - @backstage/core-components@0.18.9
+ - @backstage/plugin-auth@0.1.7
+ - @backstage/plugin-catalog-unprocessed-entities@0.2.29
+ - @backstage/plugin-scaffolder-react@1.20.1
+ - @backstage/plugin-scaffolder@1.36.2
+ - @backstage/frontend-app-api@0.16.2
+ - @backstage/plugin-techdocs@1.17.3
+ - @backstage/core-compat-api@0.5.10
+ - @backstage/plugin-kubernetes@0.12.18
+ - @backstage/plugin-notifications@0.5.16
+ - @backstage/plugin-search@1.7.1
+ - @backstage/plugin-api-docs@0.14.0
+ - @backstage/plugin-permission-react@0.5.0
+ - @backstage/plugin-signals@0.0.30
+ - @backstage/plugin-app-visualizer@0.2.2
+ - @backstage/frontend-defaults@0.5.1
+ - @backstage/plugin-devtools@0.1.38
+ - @backstage/plugin-kubernetes-cluster@0.0.36
+ - @backstage/config@1.3.7
+ - @backstage/core-plugin-api@1.12.5
+ - @backstage/integration-react@1.2.17
+ - @backstage/plugin-app-react@0.2.2
+ - @backstage/plugin-auth-react@0.1.26
+ - @backstage/plugin-catalog-common@1.1.9
+ - @backstage/plugin-home@0.9.4
+ - @backstage/plugin-home-react@0.1.37
+ - @backstage/plugin-search-common@1.2.23
+ - @backstage/plugin-search-react@1.11.1
+ - @backstage/plugin-techdocs-module-addons-contrib@1.1.35
+ - @backstage/plugin-techdocs-react@1.3.10
+ - @backstage/plugin-user-settings@0.9.2
+
+## 0.0.34-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/ui@0.14.0-next.2
+ - @backstage/theme@0.7.3-next.0
+ - @backstage/plugin-catalog@2.0.2-next.2
+ - @backstage/core-app-api@1.20.0-next.2
+ - @backstage/plugin-app@0.4.3-next.2
+ - @backstage/cli@0.36.1-next.2
+ - @backstage/core-components@0.18.9-next.1
+ - @backstage/plugin-auth@0.1.7-next.2
+ - @backstage/plugin-catalog-import@0.13.12-next.2
+ - @backstage/plugin-catalog-react@2.1.2-next.2
+ - @backstage/plugin-catalog-unprocessed-entities@0.2.29-next.2
+ - @backstage/plugin-notifications@0.5.16-next.2
+ - @backstage/plugin-catalog-graph@0.6.1-next.2
+ - @backstage/plugin-scaffolder@1.36.2-next.2
+ - @backstage/plugin-techdocs@1.17.3-next.2
+ - @backstage/catalog-model@1.7.8-next.0
+ - @backstage/config@1.3.7-next.0
+ - @backstage/core-compat-api@0.5.10-next.2
+ - @backstage/core-plugin-api@1.12.5-next.2
+ - @backstage/frontend-app-api@0.16.2-next.2
+ - @backstage/frontend-defaults@0.5.1-next.2
+ - @backstage/frontend-plugin-api@0.16.0-next.2
+ - @backstage/plugin-auth-react@0.1.26-next.1
+ - @backstage/plugin-devtools@0.1.38-next.2
+ - @backstage/plugin-search@1.7.1-next.2
+ - @backstage/plugin-user-settings@0.9.2-next.2
+ - @backstage/app-defaults@1.7.7-next.2
+ - @backstage/plugin-home@0.9.4-next.2
+ - @backstage/plugin-scaffolder-react@1.20.1-next.2
+ - @backstage/plugin-search-react@1.11.1-next.2
+ - @backstage/plugin-signals@0.0.30-next.2
+ - @backstage/plugin-techdocs-react@1.3.10-next.2
+ - @backstage/integration-react@1.2.17-next.1
+ - @backstage/plugin-techdocs-module-addons-contrib@1.1.35-next.2
+ - @backstage/plugin-org@0.7.1-next.2
+ - @backstage/plugin-api-docs@0.13.6-next.2
+ - @backstage/plugin-catalog-common@1.1.9-next.0
+ - @backstage/plugin-kubernetes@0.12.18-next.2
+ - @backstage/plugin-kubernetes-cluster@0.0.36-next.1
+ - @backstage/plugin-permission-react@0.4.42-next.1
+ - @backstage/plugin-search-common@1.2.23-next.0
+
+## 0.0.34-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/app-defaults@1.7.7-next.1
+ - @backstage/plugin-app@0.4.3-next.1
+ - @backstage/cli@0.36.1-next.1
+ - @backstage/ui@0.14.0-next.1
+ - @backstage/plugin-catalog-graph@0.6.1-next.1
+ - @backstage/plugin-catalog-import@0.13.12-next.1
+ - @backstage/core-app-api@1.20.0-next.1
+ - @backstage/plugin-catalog-react@2.1.2-next.1
+ - @backstage/plugin-org@0.7.1-next.1
+ - @backstage/frontend-plugin-api@0.16.0-next.1
+ - @backstage/frontend-app-api@0.16.2-next.1
+ - @backstage/core-compat-api@0.5.10-next.1
+ - @backstage/plugin-kubernetes@0.12.18-next.1
+ - @backstage/plugin-notifications@0.5.16-next.1
+ - @backstage/plugin-scaffolder@1.36.2-next.1
+ - @backstage/plugin-search@1.7.1-next.1
+ - @backstage/plugin-signals@0.0.30-next.1
+ - @backstage/core-components@0.18.9-next.0
+ - @backstage/core-plugin-api@1.12.5-next.1
+ - @backstage/plugin-api-docs@0.13.6-next.1
+ - @backstage/plugin-catalog@2.0.2-next.1
+ - @backstage/plugin-home@0.9.4-next.1
+ - @backstage/plugin-scaffolder-react@1.20.1-next.1
+ - @backstage/plugin-search-react@1.11.1-next.1
+ - @backstage/plugin-techdocs@1.17.3-next.1
+ - @backstage/plugin-user-settings@0.9.2-next.1
+ - @backstage/frontend-defaults@0.5.1-next.1
+ - @backstage/plugin-app-react@0.2.2-next.1
+ - @backstage/plugin-app-visualizer@0.2.2-next.1
+ - @backstage/plugin-auth@0.1.7-next.1
+ - @backstage/plugin-catalog-unprocessed-entities@0.2.29-next.1
+ - @backstage/plugin-devtools@0.1.38-next.1
+ - @backstage/plugin-home-react@0.1.37-next.1
+ - @backstage/plugin-techdocs-module-addons-contrib@1.1.35-next.1
+ - @backstage/plugin-techdocs-react@1.3.10-next.1
+
## 0.0.34-next.0
### Patch Changes
diff --git a/packages/app/knip-report.md b/packages/app/knip-report.md
index 7ed60089cf..0ef418b6ff 100644
--- a/packages/app/knip-report.md
+++ b/packages/app/knip-report.md
@@ -1,42 +1,44 @@
# Knip report
-## Unused dependencies (26)
+## Unused dependencies (27)
-| Name | Location | Severity |
-| :----------------------------------------------- | :----------- | :------- |
-| @backstage/plugin-techdocs-module-addons-contrib | packages/app/package.json | error |
-| @backstage/plugin-catalog-unprocessed-entities | packages/app/package.json | error |
-| @backstage/plugin-kubernetes-cluster | packages/app/package.json | error |
-| @backstage/plugin-permission-react | packages/app/package.json | error |
-| @backstage/plugin-scaffolder-react | packages/app/package.json | error |
-| @backstage/plugin-catalog-common | packages/app/package.json | error |
-| @backstage/plugin-techdocs-react | packages/app/package.json | error |
-| @backstage/plugin-catalog-graph | packages/app/package.json | error |
-| @backstage/plugin-search-common | packages/app/package.json | error |
-| @backstage/plugin-search-react | packages/app/package.json | error |
-| @backstage/integration-react | packages/app/package.json | error |
-| @backstage/plugin-auth-react | packages/app/package.json | error |
-| @backstage/plugin-scaffolder | packages/app/package.json | error |
-| @backstage/core-plugin-api | packages/app/package.json | error |
-| @backstage/plugin-api-docs | packages/app/package.json | error |
-| @backstage/plugin-catalog | packages/app/package.json | error |
-| @backstage/plugin-signals | packages/app/package.json | error |
-| @backstage/app-defaults | packages/app/package.json | error |
-| @backstage/plugin-app | packages/app/package.json | error |
-| @backstage/plugin-org | packages/app/package.json | error |
-| @backstage/config | packages/app/package.json | error |
-| @material-ui/lab | packages/app/package.json | error |
-| zen-observable | packages/app/package.json | error |
-| @octokit/rest | packages/app/package.json | error |
-| react-use | packages/app/package.json | error |
-| history | packages/app/package.json | error |
+| Name | Location | Severity |
+| :----------------------------------------------- | :---------------- | :------- |
+| @backstage/plugin-techdocs-module-addons-contrib | package.json:77:6 | error |
+| @backstage/plugin-catalog-unprocessed-entities | package.json:61:6 | error |
+| @backstage/plugin-kubernetes-cluster | package.json:66:6 | error |
+| @backstage/plugin-permission-react | package.json:69:6 | error |
+| @backstage/plugin-scaffolder-react | package.json:71:6 | error |
+| @backstage/plugin-catalog-common | package.json:57:6 | error |
+| @backstage/plugin-techdocs-react | package.json:78:6 | error |
+| @backstage/plugin-catalog-graph | package.json:58:6 | error |
+| @backstage/plugin-search-common | package.json:73:6 | error |
+| @backstage/plugin-search-react | package.json:74:6 | error |
+| @backstage/integration-react | package.json:49:6 | error |
+| @backstage/plugin-auth-react | package.json:55:6 | error |
+| @backstage/plugin-scaffolder | package.json:70:6 | error |
+| @backstage/core-plugin-api | package.json:45:6 | error |
+| @backstage/plugin-api-docs | package.json:50:6 | error |
+| @backstage/plugin-devtools | package.json:62:6 | error |
+| @backstage/plugin-signals | package.json:75:6 | error |
+| @backstage/app-defaults | package.json:38:6 | error |
+| @backstage/plugin-auth | package.json:54:6 | error |
+| @backstage/plugin-app | package.json:51:6 | error |
+| @backstage/plugin-org | package.json:68:6 | error |
+| @backstage/config | package.json:41:6 | error |
+| @material-ui/lab | package.json:84:6 | error |
+| zen-observable | package.json:92:6 | error |
+| @octokit/rest | package.json:85:6 | error |
+| react-use | package.json:91:6 | error |
+| history | package.json:86:6 | error |
## Unused devDependencies (5)
-| Name | Location | Severity |
-| :-------------------------- | :----------- | :------- |
-| @testing-library/user-event | packages/app/package.json | error |
-| @types/zen-observable | packages/app/package.json | error |
-| @testing-library/dom | packages/app/package.json | error |
-| @types/jquery | packages/app/package.json | error |
-| cross-env | packages/app/package.json | error |
+| Name | Location | Severity |
+| :-------------------------- | :----------------- | :------- |
+| @testing-library/user-event | package.json:100:6 | error |
+| @types/zen-observable | package.json:104:6 | error |
+| @types/jquery | package.json:101:6 | error |
+| cross-env | package.json:105:6 | error |
+| @testing-library/dom | package.json:97:6 | error |
+
diff --git a/packages/app/package.json b/packages/app/package.json
index e75dc53bcf..7406c051bd 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -1,6 +1,6 @@
{
"name": "example-app",
- "version": "0.0.34-next.0",
+ "version": "0.0.35-next.0",
"backstage": {
"role": "frontend"
},
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 6a6814ae4a..1bd2db5b99 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -50,6 +50,7 @@ import { convertLegacyPageExtension } from '@backstage/core-compat-api';
import { convertLegacyEntityContentExtension } from '@backstage/plugin-catalog-react/alpha';
import { pluginInfoResolver } from './pluginInfoResolver';
import { appModuleNav } from './modules/appModuleNav';
+import { appModuleScaffolder } from './modules/appModuleScaffolder';
import catalogPlugin from '@backstage/plugin-catalog/alpha';
import InfoIcon from '@material-ui/icons/Info';
@@ -140,6 +141,7 @@ const app = createApp({
kubernetesPlugin,
notFoundErrorPageModule,
appModuleNav,
+ appModuleScaffolder,
customHomePageModule,
...collectedLegacyPlugins,
],
diff --git a/packages/app/src/modules/BuiTemplateCard.module.css b/packages/app/src/modules/BuiTemplateCard.module.css
new file mode 100644
index 0000000000..e938bacfc0
--- /dev/null
+++ b/packages/app/src/modules/BuiTemplateCard.module.css
@@ -0,0 +1,28 @@
+.templateCard {
+ height: 212px;
+ display: flex;
+}
+
+.templateName {
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 1;
+ line-clamp: 1;
+}
+
+.templateDescription {
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 3;
+ line-clamp: 3;
+}
+
+.templateBody {
+ display: flex;
+ flex: 1;
+ gap: var(--bui-space-3);
+ flex-direction: column;
+ justify-content: space-between;
+}
diff --git a/packages/app/src/modules/BuiTemplateCard.tsx b/packages/app/src/modules/BuiTemplateCard.tsx
new file mode 100644
index 0000000000..d2d765abd7
--- /dev/null
+++ b/packages/app/src/modules/BuiTemplateCard.tsx
@@ -0,0 +1,116 @@
+/*
+ * 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 { useMemo } from 'react';
+import { RELATION_OWNED_BY } from '@backstage/catalog-model';
+import { useAnalytics } from '@backstage/frontend-plugin-api';
+import {
+ EntityRefLink,
+ getEntityRelations,
+} from '@backstage/plugin-catalog-react';
+import {
+ Box,
+ Button,
+ Card,
+ CardFooter,
+ CardHeader,
+ Flex,
+ Tag,
+ TagGroup,
+ Text,
+} from '@backstage/ui';
+import type { TemplateCardComponentProps } from '@backstage/plugin-scaffolder-react/alpha';
+import styles from './BuiTemplateCard.module.css';
+
+const MAX_TAGS = 4;
+
+export function BuiTemplateCard(props: TemplateCardComponentProps) {
+ const { template, onSelected } = props;
+ const analytics = useAnalytics();
+
+ const {
+ spec: { type },
+ metadata: { tags, description, name, title },
+ } = template;
+
+ const visibleTags = useMemo(
+ () =>
+ Array.from(new Set([type, ...(tags ?? [])].filter(Boolean))).slice(
+ 0,
+ MAX_TAGS,
+ ),
+ [type, tags],
+ );
+
+ const owner = getEntityRelations(template, RELATION_OWNED_BY)[0];
+
+ const handleRun = () => {
+ analytics.captureEvent('click', 'Template has been opened');
+ onSelected?.();
+ };
+
+ return (
+
+
+
+ {title ?? name}
+
+
+
+ {description && (
+
+ {description}
+
+ )}
+ {visibleTags.length > 0 && (
+
+ {visibleTags.map(t => (
+ {t!}
+ ))}
+
+ )}
+
+
+
+
+ Run
+
+ {owner && (
+
+
+ Created by
+
+
+
+
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/app/src/modules/appModuleScaffolder.tsx b/packages/app/src/modules/appModuleScaffolder.tsx
new file mode 100644
index 0000000000..ae9c63cba8
--- /dev/null
+++ b/packages/app/src/modules/appModuleScaffolder.tsx
@@ -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.
+ */
+
+import { createFrontendModule } from '@backstage/frontend-plugin-api';
+import { SwappableComponentBlueprint } from '@backstage/plugin-app-react';
+import { TemplateCard } from '@backstage/plugin-scaffolder-react/alpha';
+
+export const appModuleScaffolder = createFrontendModule({
+ pluginId: 'app',
+ extensions: [
+ SwappableComponentBlueprint.make({
+ name: 'scaffolder-template-card',
+ params: defineParams =>
+ defineParams({
+ component: TemplateCard,
+ loader: () =>
+ import('./BuiTemplateCard').then(m => m.BuiTemplateCard),
+ }),
+ }),
+ ],
+});
diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md
index 74a02da3d9..bcd9e27a03 100644
--- a/packages/backend-app-api/CHANGELOG.md
+++ b/packages/backend-app-api/CHANGELOG.md
@@ -1,5 +1,45 @@
# @backstage/backend-app-api
+## 1.7.0-next.0
+
+### Minor Changes
+
+- 3595c97: Added `ExtensionPointFactoryMiddleware` type and `createExtensionPointFactoryMiddleware` helper to reimplement extension point outputs at backend creation time.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/errors@1.3.1-next.0
+ - @backstage/backend-plugin-api@1.9.1-next.0
+ - @backstage/config@1.3.8-next.0
+
+## 1.6.1
+
+### Patch Changes
+
+- 482ceed: Migrated from `assertError` to `toError` for error handling.
+- Updated dependencies
+ - @backstage/backend-plugin-api@1.9.0
+ - @backstage/errors@1.3.0
+ - @backstage/config@1.3.7
+
+## 1.6.1-next.2
+
+### Patch Changes
+
+- 482ceed: Migrated from `assertError` to `toError` for error handling.
+- Updated dependencies
+ - @backstage/errors@1.3.0-next.0
+ - @backstage/backend-plugin-api@1.9.0-next.2
+ - @backstage/config@1.3.7-next.0
+
+## 1.6.1-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-plugin-api@1.9.0-next.1
+
## 1.6.1-next.0
### Patch Changes
diff --git a/packages/backend-app-api/knip-report.md b/packages/backend-app-api/knip-report.md
index d056972992..0923f6691a 100644
--- a/packages/backend-app-api/knip-report.md
+++ b/packages/backend-app-api/knip-report.md
@@ -2,7 +2,7 @@
## Unused devDependencies (1)
-| Name | Location | Severity |
-| :-------------------------- | :----------- | :------- |
-| @backstage/backend-defaults | packages/backend-app-api/package.json | error |
+| Name | Location | Severity |
+| :-------------------------- | :---------------- | :------- |
+| @backstage/backend-defaults | package.json:54:6 | error |
diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json
index 394d4b894d..9bb7462158 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.6.1-next.0",
+ "version": "1.7.0-next.0",
"description": "Core API used by Backstage backend apps",
"backstage": {
"role": "node-library"
diff --git a/packages/backend-app-api/report.api.md b/packages/backend-app-api/report.api.md
index 62b46b93e6..1fcfc5926e 100644
--- a/packages/backend-app-api/report.api.md
+++ b/packages/backend-app-api/report.api.md
@@ -5,6 +5,7 @@
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { CustomErrorBase } from '@backstage/errors';
+import { ExtensionPoint } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public (undocumented)
@@ -42,6 +43,12 @@ export interface BackendStartupResult {
resultAt: Date;
}
+// @public
+export function createExtensionPointFactoryMiddleware(options: {
+ extensionPoint: ExtensionPoint;
+ middleware: (original: T) => Promise;
+}): ExtensionPointFactoryMiddleware;
+
// @public (undocumented)
export function createSpecializedBackend(
options: CreateSpecializedBackendOptions,
@@ -51,6 +58,14 @@ export function createSpecializedBackend(
export interface CreateSpecializedBackendOptions {
// (undocumented)
defaultServiceFactories: ServiceFactory[];
+ // (undocumented)
+ extensionPointFactoryMiddleware?: ExtensionPointFactoryMiddleware[];
+}
+
+// @public
+export interface ExtensionPointFactoryMiddleware {
+ // (undocumented)
+ $$type: '@backstage/ExtensionPointFactoryMiddleware';
}
// @public
diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts
index f782295e71..fb4da6ca05 100644
--- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts
+++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts
@@ -28,6 +28,7 @@ import {
import { BackendInitializer } from './BackendInitializer';
import { mockServices } from '@backstage/backend-test-utils';
import { BackendStartupError } from './BackendStartupError';
+import { createExtensionPointFactoryMiddleware } from './types';
const baseFactories = [
mockServices.rootLifecycle.factory(),
@@ -2111,4 +2112,218 @@ describe('BackendInitializer', () => {
);
});
});
+
+ describe('extensionPointFactoryMiddleware', () => {
+ it('should apply middleware to matching extension points', async () => {
+ expect.assertions(1);
+
+ const extensionPoint = createExtensionPoint<{ values: string[] }>({
+ id: 'test.ext',
+ });
+
+ const init = new BackendInitializer(baseFactories, [
+ createExtensionPointFactoryMiddleware({
+ extensionPoint,
+ middleware: async original => ({
+ ...original,
+ values: [...original.values, 'from-middleware'],
+ }),
+ }),
+ ]);
+
+ init.add(testPlugin);
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'provider',
+ register(reg) {
+ reg.registerExtensionPoint(extensionPoint, {
+ values: ['original'],
+ });
+ reg.registerInit({ deps: {}, async init() {} });
+ },
+ }),
+ );
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'consumer',
+ register(reg) {
+ reg.registerInit({
+ deps: { ext: extensionPoint },
+ async init({ ext }) {
+ expect(ext.values).toEqual(['original', 'from-middleware']);
+ },
+ });
+ },
+ }),
+ );
+
+ await init.start();
+ });
+
+ it('should not affect non-matching extension points', async () => {
+ expect.assertions(1);
+
+ const extensionPointA = createExtensionPoint<{ values: string[] }>({
+ id: 'test.a',
+ });
+ const extensionPointB = createExtensionPoint<{ values: string[] }>({
+ id: 'test.b',
+ });
+
+ const init = new BackendInitializer(baseFactories, [
+ createExtensionPointFactoryMiddleware({
+ extensionPoint: extensionPointA,
+ middleware: async original => ({
+ ...original,
+ values: [...original.values, 'wrapped'],
+ }),
+ }),
+ ]);
+
+ init.add(testPlugin);
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'provider',
+ register(reg) {
+ reg.registerExtensionPoint(extensionPointB, {
+ values: ['untouched'],
+ });
+ reg.registerInit({ deps: {}, async init() {} });
+ },
+ }),
+ );
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'consumer',
+ register(reg) {
+ reg.registerInit({
+ deps: { ext: extensionPointB },
+ async init({ ext }) {
+ expect(ext.values).toEqual(['untouched']);
+ },
+ });
+ },
+ }),
+ );
+
+ await init.start();
+ });
+
+ it('should chain multiple middlewares for the same extension point', async () => {
+ expect.assertions(1);
+
+ const extensionPoint = createExtensionPoint<{ values: string[] }>({
+ id: 'test.ext',
+ });
+
+ const init = new BackendInitializer(baseFactories, [
+ createExtensionPointFactoryMiddleware({
+ extensionPoint,
+ middleware: async original => ({
+ ...original,
+ values: [...original.values, 'first'],
+ }),
+ }),
+ createExtensionPointFactoryMiddleware({
+ extensionPoint,
+ middleware: async original => ({
+ ...original,
+ values: [...original.values, 'second'],
+ }),
+ }),
+ ]);
+
+ init.add(testPlugin);
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'provider',
+ register(reg) {
+ reg.registerExtensionPoint(extensionPoint, { values: ['base'] });
+ reg.registerInit({ deps: {}, async init() {} });
+ },
+ }),
+ );
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'consumer',
+ register(reg) {
+ reg.registerInit({
+ deps: { ext: extensionPoint },
+ async init({ ext }) {
+ expect(ext.values).toEqual(['base', 'first', 'second']);
+ },
+ });
+ },
+ }),
+ );
+
+ await init.start();
+ });
+
+ it('should not fail when middleware targets an unregistered extension point', async () => {
+ const unregisteredExtensionPoint = createExtensionPoint<{
+ values: string[];
+ }>({
+ id: 'test.unregistered',
+ });
+
+ const init = new BackendInitializer(baseFactories, [
+ createExtensionPointFactoryMiddleware({
+ extensionPoint: unregisteredExtensionPoint,
+ middleware: async original => ({
+ ...original,
+ values: [...original.values, 'never-applied'],
+ }),
+ }),
+ ]);
+
+ init.add(testPlugin);
+ const { result } = await init.start();
+ expect(result.outcome).toBe('success');
+ });
+
+ it('should pass through when no middleware is provided', async () => {
+ expect.assertions(1);
+
+ const extensionPoint = createExtensionPoint<{ values: string[] }>({
+ id: 'test.ext',
+ });
+
+ const init = new BackendInitializer(baseFactories);
+
+ init.add(testPlugin);
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'provider',
+ register(reg) {
+ reg.registerExtensionPoint(extensionPoint, { values: ['orig'] });
+ reg.registerInit({ deps: {}, async init() {} });
+ },
+ }),
+ );
+ init.add(
+ createBackendModule({
+ pluginId: 'test',
+ moduleId: 'consumer',
+ register(reg) {
+ reg.registerInit({
+ deps: { ext: extensionPoint },
+ async init({ ext }) {
+ expect(ext.values).toEqual(['orig']);
+ },
+ });
+ },
+ }),
+ );
+
+ await init.start();
+ });
+ });
});
diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts
index 8ebab723da..94e3e7c960 100644
--- a/packages/backend-app-api/src/wiring/BackendInitializer.ts
+++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts
@@ -25,7 +25,11 @@ import {
createServiceFactory,
ExtensionPointFactoryContext,
} from '@backstage/backend-plugin-api';
-import { ServiceOrExtensionPoint } from './types';
+import {
+ ExtensionPointFactoryMiddleware,
+ ServiceOrExtensionPoint,
+} from './types';
+import { OpaqueExtensionPointFactoryMiddleware } from '@internal/backend';
// Direct internal import to avoid duplication
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import type {
@@ -35,7 +39,7 @@ import type {
} from '../../../backend-plugin-api/src/wiring/types';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import type { InternalServiceFactory } from '../../../backend-plugin-api/src/services/system/types';
-import { ForwardedError, ConflictError, assertError } from '@backstage/errors';
+import { ConflictError, ForwardedError, toError } from '@backstage/errors';
import { DependencyGraph } from '../lib/DependencyGraph';
import { ServiceRegistry } from './ServiceRegistry';
import { createInitializationResultCollector } from './createInitializationResultCollector';
@@ -166,11 +170,17 @@ export class BackendInitializer {
#serviceRegistry: ServiceRegistry;
#registeredFeatures = new Array>();
#registeredFeatureLoaders = new Array();
+ #extensionPointFactoryMiddleware: ExtensionPointFactoryMiddleware[];
#unhandledRejectionHandler?: (reason: Error) => void;
#uncaughtExceptionHandler?: (error: Error) => void;
- constructor(defaultApiFactories: ServiceFactory[]) {
+ constructor(
+ defaultApiFactories: ServiceFactory[],
+ extensionPointFactoryMiddleware?: ExtensionPointFactoryMiddleware[],
+ ) {
this.#serviceRegistry = ServiceRegistry.create([...defaultApiFactories]);
+ this.#extensionPointFactoryMiddleware =
+ extensionPointFactoryMiddleware ?? [];
}
async #getInitDeps(
@@ -195,18 +205,18 @@ export class BackendInitializer {
`Rejected dependency on extension point ${ref.id} from outside of a module`,
);
}
- result.set(
- name,
- ep.factory({
- reportModuleStartupFailure: ({ error }) => {
- resultCollector.amendPluginModuleResult(
- pluginId,
- moduleId,
- error,
- );
- },
- }),
- );
+ let epImpl = ep.factory({
+ reportModuleStartupFailure: ({ error }) => {
+ resultCollector.amendPluginModuleResult(pluginId, moduleId, error);
+ },
+ });
+ for (const mw of this.#extensionPointFactoryMiddleware) {
+ const internal = OpaqueExtensionPointFactoryMiddleware.toInternal(mw);
+ if (internal.extensionPointId === ref.id) {
+ epImpl = await internal.middleware(epImpl);
+ }
+ }
+ result.set(name, epImpl);
} else {
const impl = await this.#serviceRegistry.get(
ref as ServiceRef,
@@ -385,12 +395,8 @@ export class BackendInitializer {
await moduleInit.init.func(moduleDeps);
resultCollector.onPluginModuleResult(pluginId, moduleId);
} catch (error: unknown) {
- assertError(error);
- resultCollector.onPluginModuleResult(
- pluginId,
- moduleId,
- error,
- );
+ const err = toError(error);
+ resultCollector.onPluginModuleResult(pluginId, moduleId, err);
}
},
);
@@ -414,8 +420,8 @@ export class BackendInitializer {
const lifecycleService = await this.#getPluginLifecycleImpl(pluginId);
await lifecycleService.startup();
} catch (error: unknown) {
- assertError(error);
- resultCollector.onPluginResult(pluginId, error);
+ const err = toError(error);
+ resultCollector.onPluginResult(pluginId, err);
}
}),
).catch(error => {
@@ -515,19 +521,19 @@ export class BackendInitializer {
throw new Error(`Invalid registration type '${(r as any).type}'`);
}
} catch (error: unknown) {
- assertError(error);
+ const err = toError(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);
+ resultCollector.onPluginModuleResult(r.pluginId, r.moduleId, err);
} else if ('pluginId' in r) {
pluginInits.delete(r.pluginId);
moduleInits.delete(r.pluginId);
- resultCollector.onPluginResult(r.pluginId, error);
+ resultCollector.onPluginResult(r.pluginId, err);
} else {
- throw error;
+ throw err;
}
}
}
diff --git a/packages/backend-app-api/src/wiring/BackstageBackend.ts b/packages/backend-app-api/src/wiring/BackstageBackend.ts
index 79ee55780f..a38d1991cd 100644
--- a/packages/backend-app-api/src/wiring/BackstageBackend.ts
+++ b/packages/backend-app-api/src/wiring/BackstageBackend.ts
@@ -17,13 +17,23 @@
import { BackendFeature, ServiceFactory } from '@backstage/backend-plugin-api';
import { BackendInitializer } from './BackendInitializer';
import { unwrapFeature } from './helpers';
-import { Backend, BackendStartupResult } from './types';
+import {
+ Backend,
+ BackendStartupResult,
+ ExtensionPointFactoryMiddleware,
+} from './types';
export class BackstageBackend implements Backend {
#initializer: BackendInitializer;
- constructor(defaultServiceFactories: ServiceFactory[]) {
- this.#initializer = new BackendInitializer(defaultServiceFactories);
+ constructor(
+ defaultServiceFactories: ServiceFactory[],
+ extensionPointFactoryMiddleware?: ExtensionPointFactoryMiddleware[],
+ ) {
+ this.#initializer = new BackendInitializer(
+ defaultServiceFactories,
+ extensionPointFactoryMiddleware,
+ );
}
add(feature: BackendFeature | Promise<{ default: BackendFeature }>): void {
diff --git a/packages/backend-app-api/src/wiring/createSpecializedBackend.ts b/packages/backend-app-api/src/wiring/createSpecializedBackend.ts
index fd449be541..d1a88a21e4 100644
--- a/packages/backend-app-api/src/wiring/createSpecializedBackend.ts
+++ b/packages/backend-app-api/src/wiring/createSpecializedBackend.ts
@@ -43,5 +43,8 @@ export function createSpecializedBackend(
);
}
- return new BackstageBackend(options.defaultServiceFactories);
+ return new BackstageBackend(
+ options.defaultServiceFactories,
+ options.extensionPointFactoryMiddleware,
+ );
}
diff --git a/packages/backend-app-api/src/wiring/index.ts b/packages/backend-app-api/src/wiring/index.ts
index cc084e5f34..2ddd70e878 100644
--- a/packages/backend-app-api/src/wiring/index.ts
+++ b/packages/backend-app-api/src/wiring/index.ts
@@ -17,9 +17,11 @@
export type {
Backend,
CreateSpecializedBackendOptions,
+ ExtensionPointFactoryMiddleware,
BackendStartupResult,
PluginStartupResult,
ModuleStartupResult,
} from './types';
+export { createExtensionPointFactoryMiddleware } from './types';
export { createSpecializedBackend } from './createSpecializedBackend';
export { BackendStartupError } from './BackendStartupError';
diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts
index 5782021656..afd0cecbfb 100644
--- a/packages/backend-app-api/src/wiring/types.ts
+++ b/packages/backend-app-api/src/wiring/types.ts
@@ -20,6 +20,34 @@ import {
ServiceRef,
ServiceFactory,
} from '@backstage/backend-plugin-api';
+import { OpaqueExtensionPointFactoryMiddleware } from '@internal/backend';
+
+/**
+ * A middleware entry that reimplements a specific extension point's output.
+ * The framework matches by extension point ID and passes through all
+ * non-matching extension points automatically.
+ *
+ * @public
+ */
+export interface ExtensionPointFactoryMiddleware {
+ $$type: '@backstage/ExtensionPointFactoryMiddleware';
+}
+
+/**
+ * Creates a typed middleware entry that reimplements a specific extension point.
+ * Use this helper to preserve type inference for the middleware callback.
+ *
+ * @public
+ */
+export function createExtensionPointFactoryMiddleware(options: {
+ extensionPoint: ExtensionPoint;
+ middleware: (original: T) => Promise;
+}): ExtensionPointFactoryMiddleware {
+ return OpaqueExtensionPointFactoryMiddleware.createInstance('v1', {
+ extensionPointId: options.extensionPoint.id,
+ middleware: options.middleware as (original: unknown) => Promise,
+ });
+}
/**
* @public
@@ -35,6 +63,7 @@ export interface Backend {
*/
export interface CreateSpecializedBackendOptions {
defaultServiceFactories: ServiceFactory[];
+ extensionPointFactoryMiddleware?: ExtensionPointFactoryMiddleware[];
}
/**
diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md
index d78cbac68a..35a5bc5d2f 100644
--- a/packages/backend-defaults/CHANGELOG.md
+++ b/packages/backend-defaults/CHANGELOG.md
@@ -1,5 +1,105 @@
# @backstage/backend-defaults
+## 0.17.1-next.1
+
+### Patch Changes
+
+- e9b78e9: Removed the `uuid` dependency and replaced usage with the built-in `crypto.randomUUID()`.
+- Updated dependencies
+ - @backstage/cli-node@0.3.2-next.1
+ - @backstage/plugin-auth-node@0.7.1-next.1
+ - @backstage/plugin-permission-common@0.9.9-next.1
+
+## 0.17.1-next.0
+
+### Patch Changes
+
+- 3595c97: Exported `defaultServiceFactories` to allow use with `createSpecializedBackend` for advanced configuration like `extensionPointFactoryMiddleware`.
+- 89d3248: Fixed scheduler `sleep` firing immediately for durations longer than ~24.8 days, caused by Node.js `setTimeout` overflowing its 32-bit millisecond limit.
+- 744fa1f: Removed duplicated entries that appeared in both `dependencies` and `devDependencies`.
+- Updated dependencies
+ - @backstage/errors@1.3.1-next.0
+ - @backstage/backend-app-api@1.7.0-next.0
+ - @backstage/integration@2.0.2-next.0
+ - @backstage/plugin-auth-node@0.7.1-next.0
+ - @backstage/backend-plugin-api@1.9.1-next.0
+ - @backstage/cli-node@0.3.2-next.0
+ - @backstage/config@1.3.8-next.0
+ - @backstage/config-loader@1.10.11-next.0
+ - @backstage/integration-aws-node@0.1.22-next.0
+ - @backstage/plugin-events-node@0.4.22-next.0
+ - @backstage/plugin-permission-common@0.9.9-next.0
+ - @backstage/plugin-permission-node@0.10.13-next.0
+ - @backstage/backend-dev-utils@0.1.7
+ - @backstage/types@1.2.2
+
+## 0.17.0
+
+### Minor Changes
+
+- c69e03c: Added support for AWS RDS IAM authentication for PostgreSQL connections. Set `connection.type: rds` along with `host`, `user`, and `region` to use short-lived IAM tokens instead of a static password. Requires the `@aws-sdk/rds-signer` package and an IAM role with `rds-db:connect` permission.
+
+### Patch Changes
+
+- 4559806: Added support for typed `examples` on actions registered via the actions registry. Action authors can now provide examples with compile-time-checked `input` and `output` values that match their schema definitions.
+- 5cd814f: Refactored auditor severity log level mappings to use `zod/v4` with schema-driven defaults and type inference.
+- 482ceed: Migrated from `assertError` to `toError` for error handling.
+- 6e2aaab: Fixed `AwsS3UrlReader` failing to read files from S3 buckets configured with custom endpoint hosts. When an integration was configured with a specific endpoint like `https://bucket-1.s3.eu-central-1.amazonaws.com`, the URL parser incorrectly fell through to the non-AWS code path, always defaulting the region to `us-east-1` instead of extracting it from the hostname.
+- 308c672: `HostDiscovery` now logs a warning when `backend.baseUrl` is set to a localhost address while `NODE_ENV` is `production`, and when `backend.baseUrl` is not a valid URL.
+- 85c5a46: DefaultActionsRegistryService: add json middleware to /.backstage/actions/ routes only
+- 547258f: Refactored the database creation retry loop to avoid an unnecessary delay after the final failed attempt.
+- 79453c0: Updated dependency `wait-for-expect` to `^4.0.0`.
+- f14df56: Added experimental support for using `embedded-postgres` as the database for local development. Set `backend.database.client` to `embedded-postgres` in your app config to enable this. The `embedded-postgres` package must be installed as an explicit dependency in your project.
+- Updated dependencies
+ - @backstage/backend-plugin-api@1.9.0
+ - @backstage/errors@1.3.0
+ - @backstage/plugin-auth-node@0.7.0
+ - @backstage/backend-app-api@1.6.1
+ - @backstage/cli-node@0.3.1
+ - @backstage/config-loader@1.10.10
+ - @backstage/integration@2.0.1
+ - @backstage/plugin-permission-node@0.10.12
+ - @backstage/config@1.3.7
+ - @backstage/integration-aws-node@0.1.21
+ - @backstage/plugin-events-node@0.4.21
+ - @backstage/plugin-permission-common@0.9.8
+
+## 0.16.1-next.2
+
+### Patch Changes
+
+- 482ceed: Migrated from `assertError` to `toError` for error handling.
+- 308c672: `HostDiscovery` now logs a warning when `backend.baseUrl` is set to a localhost address while `NODE_ENV` is `production`, and when `backend.baseUrl` is not a valid URL.
+- 85c5a46: DefaultActionsRegistryService: add json middleware to /.backstage/actions/ routes only
+- f14df56: Added experimental support for using `embedded-postgres` as the database for local development. Set `backend.database.client` to `embedded-postgres` in your app config to enable this. The `embedded-postgres` package must be installed as an explicit dependency in your project.
+- Updated dependencies
+ - @backstage/errors@1.3.0-next.0
+ - @backstage/plugin-auth-node@0.7.0-next.2
+ - @backstage/backend-app-api@1.6.1-next.2
+ - @backstage/cli-node@0.3.1-next.1
+ - @backstage/config-loader@1.10.10-next.1
+ - @backstage/integration@2.0.1-next.0
+ - @backstage/backend-plugin-api@1.9.0-next.2
+ - @backstage/config@1.3.7-next.0
+ - @backstage/integration-aws-node@0.1.21-next.0
+ - @backstage/plugin-events-node@0.4.21-next.2
+ - @backstage/plugin-permission-common@0.9.8-next.0
+ - @backstage/plugin-permission-node@0.10.12-next.2
+
+## 0.16.1-next.1
+
+### Patch Changes
+
+- 4559806: Added support for typed `examples` on actions registered via the actions registry. Action authors can now provide examples with compile-time-checked `input` and `output` values that match their schema definitions.
+- 5cd814f: Refactored auditor severity log level mappings to use `zod/v4` with schema-driven defaults and type inference.
+- 6e2aaab: Fixed `AwsS3UrlReader` failing to read files from S3 buckets configured with custom endpoint hosts. When an integration was configured with a specific endpoint like `https://bucket-1.s3.eu-central-1.amazonaws.com`, the URL parser incorrectly fell through to the non-AWS code path, always defaulting the region to `us-east-1` instead of extracting it from the hostname.
+- Updated dependencies
+ - @backstage/backend-plugin-api@1.9.0-next.1
+ - @backstage/plugin-auth-node@0.7.0-next.1
+ - @backstage/backend-app-api@1.6.1-next.1
+ - @backstage/plugin-events-node@0.4.21-next.1
+ - @backstage/plugin-permission-node@0.10.12-next.1
+
## 0.16.1-next.0
### Patch Changes
diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts
index 3d09146b0d..d8f2e9b031 100644
--- a/packages/backend-defaults/config.d.ts
+++ b/packages/backend-defaults/config.d.ts
@@ -585,7 +585,7 @@ export interface Config {
/** Database connection configuration, select base database type using the `client` field */
database: {
/** Default database client to use */
- client: 'better-sqlite3' | 'sqlite3' | 'pg';
+ client: 'better-sqlite3' | 'sqlite3' | 'pg' | 'embedded-postgres';
/**
* Base database connection string, or object with individual connection properties
* @visibility secret
@@ -632,6 +632,35 @@ export interface Config {
*/
ipAddressType?: 'PUBLIC' | 'PRIVATE' | 'PSC';
}
+ | {
+ /**
+ * The specific config for AWS RDS connections with IAM authentication.
+ * Requires the `@aws-sdk/rds-signer` package to be installed.
+ * The IAM role or user must have the `rds-db:connect` permission for the database user.
+ */
+ type: 'rds';
+ /**
+ * The hostname of the RDS instance.
+ */
+ host: string;
+ /**
+ * The port number the database is listening on.
+ */
+ port: number;
+ /**
+ * The database user to authenticate as. This user must have the `rds_iam` role granted.
+ */
+ user: string;
+ /**
+ * The AWS region where the RDS instance is located.
+ * Falls back to the AWS_REGION or AWS_DEFAULT_REGION environment variables if not set.
+ */
+ region?: string;
+ /**
+ * Other connection settings
+ */
+ [key: string]: unknown;
+ }
| {
/**
* The rest config for default, regular connections
@@ -1157,6 +1186,52 @@ export interface Config {
};
};
+ /**
+ * Tracing-related backend configuration. Honored by Backstage backend
+ * plugins that emit OpenTelemetry trace spans.
+ */
+ tracing?: {
+ /**
+ * Opt-in capture of attributes that may identify users or contain
+ * sensitive data on backend trace spans.
+ */
+ capture?: {
+ /**
+ * When true, backend plugins emitting trace spans for authenticated
+ * requests SHOULD include the authenticated principal's identity as
+ * `enduser.id` (the user entity ref for a user principal, or the
+ * service subject for a service principal). Defaults to false.
+ */
+ endUser?: boolean;
+ };
+ /**
+ * Plugin-specific tracing configuration. Each plugin can override
+ * tracer instrumentation scope metadata.
+ */
+ plugin?: {
+ [pluginId: string]: {
+ /**
+ * Tracer configuration for this plugin.
+ */
+ tracer?: {
+ /**
+ * Custom tracer name. If not set, defaults to
+ * backstage-plugin-{pluginId}.
+ */
+ name?: string;
+ /**
+ * Version for the tracer.
+ */
+ version?: string;
+ /**
+ * Schema URL for the tracer.
+ */
+ schemaUrl?: string;
+ };
+ };
+ };
+ };
+
/**
* Options to configure the default RootLoggerService.
*/
diff --git a/packages/backend-defaults/knip-report.md b/packages/backend-defaults/knip-report.md
index 8951112aaa..815230b535 100644
--- a/packages/backend-defaults/knip-report.md
+++ b/packages/backend-defaults/knip-report.md
@@ -1,15 +1,15 @@
# Knip report
-## Unused dependencies (2)
+## Unused dependencies (1)
-| Name | Location | Severity |
-| :------------- | :----------- | :------- |
-| better-sqlite3 | packages/backend-defaults/package.json | error |
-| mysql2 | packages/backend-defaults/package.json | error |
+| Name | Location | Severity |
+| :----- | :----------------- | :------- |
+| mysql2 | package.json:182:6 | error |
-## Referenced optional peerDependencies (1)
+## Unused devDependencies (2)
-| Name | Location | Severity |
-| :-------------------------------- | :----------- | :------- |
-| @google-cloud/cloud-sql-connector | packages/backend-defaults/package.json | error |
+| Name | Location | Severity |
+| :------------- | :----------------- | :------- |
+| better-sqlite3 | package.json:218:6 | error |
+| get-port | package.json:219:6 | error |
diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json
index c714f382c6..db6f075aac 100644
--- a/packages/backend-defaults/package.json
+++ b/packages/backend-defaults/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/backend-defaults",
- "version": "0.16.1-next.0",
+ "version": "0.17.1-next.1",
"description": "Backend defaults used by Backstage backend apps",
"backstage": {
"role": "node-library"
@@ -130,6 +130,7 @@
"@aws-sdk/client-codecommit": "^3.350.0",
"@aws-sdk/client-s3": "^3.350.0",
"@aws-sdk/credential-providers": "^3.350.0",
+ "@aws-sdk/rds-signer": "^3.0.0",
"@aws-sdk/types": "^3.347.0",
"@azure/identity": "^4.0.0",
"@azure/storage-blob": "^12.5.0",
@@ -191,7 +192,6 @@
"selfsigned": "^2.0.0",
"tar": "^7.5.6",
"triple-beam": "^1.4.1",
- "uuid": "^11.0.0",
"winston": "^3.2.1",
"winston-transport": "^4.5.0",
"yauzl": "^3.2.1",
@@ -201,7 +201,6 @@
},
"devDependencies": {
"@aws-sdk/util-stream-node": "^3.350.0",
- "@backstage/backend-plugin-api": "workspace:^",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@google-cloud/cloud-sql-connector": "^1.4.0",
diff --git a/packages/backend-defaults/report-alpha.api.md b/packages/backend-defaults/report-alpha.api.md
index 352ec1d688..dae718d3b5 100644
--- a/packages/backend-defaults/report-alpha.api.md
+++ b/packages/backend-defaults/report-alpha.api.md
@@ -8,6 +8,7 @@ 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';
+import { TracingService } from '@backstage/backend-plugin-api/alpha';
// @public (undocumented)
export const actionsRegistryServiceFactory: ServiceFactory<
@@ -37,5 +38,12 @@ export const rootSystemMetadataServiceFactory: ServiceFactory<
'singleton'
>;
+// @alpha
+export const tracingServiceFactory: ServiceFactory<
+ TracingService,
+ 'plugin',
+ 'singleton'
+>;
+
// (No @packageDocumentation comment for this package)
```
diff --git a/packages/backend-defaults/report.api.md b/packages/backend-defaults/report.api.md
index def38beda7..a6702e59f9 100644
--- a/packages/backend-defaults/report.api.md
+++ b/packages/backend-defaults/report.api.md
@@ -5,10 +5,14 @@
```ts
import { Backend } from '@backstage/backend-app-api';
import { BackendFeature } from '@backstage/backend-plugin-api';
+import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public (undocumented)
export function createBackend(): Backend;
+// @public (undocumented)
+export const defaultServiceFactories: ServiceFactory[];
+
// @public
export const discoveryFeatureLoader: BackendFeature;
```
diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts
index 2b4ee5befa..07c5055641 100644
--- a/packages/backend-defaults/src/CreateBackend.ts
+++ b/packages/backend-defaults/src/CreateBackend.ts
@@ -15,6 +15,7 @@
*/
import { Backend, createSpecializedBackend } from '@backstage/backend-app-api';
+import { ServiceFactory } from '@backstage/backend-plugin-api';
import { auditorServiceFactory } from '@backstage/backend-defaults/auditor';
import { authServiceFactory } from '@backstage/backend-defaults/auth';
import { cacheServiceFactory } from '@backstage/backend-defaults/cache';
@@ -39,10 +40,12 @@ import {
actionsRegistryServiceFactory,
actionsServiceFactory,
metricsServiceFactory,
+ tracingServiceFactory,
} from '@backstage/backend-defaults/alpha';
import { instanceMetadataServiceFactory } from './alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory';
-export const defaultServiceFactories = [
+/** @public */
+export const defaultServiceFactories: ServiceFactory[] = [
auditorServiceFactory,
authServiceFactory,
cacheServiceFactory,
@@ -68,6 +71,7 @@ export const defaultServiceFactories = [
actionsRegistryServiceFactory,
actionsServiceFactory,
metricsServiceFactory,
+ tracingServiceFactory,
// 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 1f6f108589..2c14caaeb3 100644
--- a/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts
+++ b/packages/backend-defaults/src/alpha/entrypoints/actionsRegistry/DefaultActionsRegistryService.ts
@@ -90,7 +90,7 @@ export class DefaultActionsRegistryService implements ActionsRegistryService {
createRouter(): Router {
const router = PromiseRouter();
- router.use(json());
+ router.use('/.backstage/actions/', json());
router.get('/.backstage/actions/v1/actions', async (req, res) => {
const credentials = await this.httpAuth.credentials(req);
diff --git a/packages/backend-defaults/src/alpha/entrypoints/tracing/DefaultTracingService.test.ts b/packages/backend-defaults/src/alpha/entrypoints/tracing/DefaultTracingService.test.ts
new file mode 100644
index 0000000000..9675ecd8ef
--- /dev/null
+++ b/packages/backend-defaults/src/alpha/entrypoints/tracing/DefaultTracingService.test.ts
@@ -0,0 +1,270 @@
+/*
+ * 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 { SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
+import { mockCredentials, mockServices } from '@backstage/backend-test-utils';
+import { DefaultTracingService } from './DefaultTracingService';
+
+type MockSpan = {
+ setAttribute: jest.Mock;
+ setStatus: jest.Mock;
+ recordException: jest.Mock;
+ end: jest.Mock;
+};
+
+type MockTracer = {
+ startActiveSpan: jest.Mock;
+};
+
+function createMockTracingPrimitives() {
+ const span: MockSpan = {
+ setAttribute: jest.fn(),
+ setStatus: jest.fn(),
+ recordException: jest.fn(),
+ end: jest.fn(),
+ };
+ const tracer: MockTracer = {
+ startActiveSpan: jest.fn(async (_name, _options, fn) => fn(span)),
+ };
+ const getTracer = jest.fn(() => tracer);
+ return { span, tracer, getTracer };
+}
+
+describe('DefaultTracingService', () => {
+ let mocks: ReturnType;
+ let getTracerProviderSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ mocks = createMockTracingPrimitives();
+ getTracerProviderSpy = jest
+ .spyOn(trace, 'getTracerProvider')
+ .mockReturnValue({ getTracer: mocks.getTracer } as any);
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ function createService(opts?: {
+ captureEndUser?: boolean;
+ name?: string;
+ version?: string;
+ schemaUrl?: string;
+ pluginId?: string;
+ }) {
+ return DefaultTracingService.create({
+ name: opts?.name ?? 'backstage-plugin-test',
+ version: opts?.version,
+ schemaUrl: opts?.schemaUrl,
+ pluginId: opts?.pluginId ?? 'test',
+ captureEndUser: opts?.captureEndUser ?? false,
+ httpAuth: mockServices.httpAuth(),
+ });
+ }
+
+ it('configures the tracer with name, version, and schemaUrl', () => {
+ createService({
+ name: 'tracer-x',
+ version: '1.2.3',
+ schemaUrl: 'https://example.com/schema',
+ });
+ expect(getTracerProviderSpy).toHaveBeenCalled();
+ expect(mocks.getTracer).toHaveBeenCalledWith('tracer-x', '1.2.3', {
+ schemaUrl: 'https://example.com/schema',
+ });
+ });
+
+ it('passes name, kind, and caller attributes through to the tracer', async () => {
+ const service = createService();
+ await service.startActiveSpan('op', async () => undefined, {
+ kind: 'server',
+ attributes: { foo: 'bar' },
+ });
+
+ expect(mocks.tracer.startActiveSpan).toHaveBeenCalledWith(
+ 'op',
+ expect.objectContaining({
+ kind: SpanKind.SERVER,
+ attributes: expect.objectContaining({ foo: 'bar' }),
+ }),
+ expect.any(Function),
+ );
+ });
+
+ it('auto-attaches backstage.plugin.id matching the calling plugin', async () => {
+ const service = createService({ pluginId: 'my-plugin' });
+ await service.startActiveSpan('op', async () => undefined);
+
+ const attrs = mocks.tracer.startActiveSpan.mock.calls[0][1].attributes;
+ expect(attrs['backstage.plugin.id']).toBe('my-plugin');
+ });
+
+ it('lets caller-supplied attributes override backstage.plugin.id at start time', async () => {
+ const service = createService({ pluginId: 'my-plugin' });
+ await service.startActiveSpan('op', async () => undefined, {
+ attributes: { 'backstage.plugin.id': 'other-plugin' },
+ });
+
+ const attrs = mocks.tracer.startActiveSpan.mock.calls[0][1].attributes;
+ expect(attrs['backstage.plugin.id']).toBe('other-plugin');
+ });
+
+ it('omits kind when none is supplied (OTel default applies)', async () => {
+ const service = createService();
+ await service.startActiveSpan('op', async () => undefined);
+
+ const [, options] = mocks.tracer.startActiveSpan.mock.calls[0];
+ expect(options.kind).toBeUndefined();
+ });
+
+ it('translates each Backstage span kind into the matching OTel SpanKind', async () => {
+ const service = createService();
+ const cases: Array<[string, SpanKind]> = [
+ ['internal', SpanKind.INTERNAL],
+ ['server', SpanKind.SERVER],
+ ['client', SpanKind.CLIENT],
+ ['producer', SpanKind.PRODUCER],
+ ['consumer', SpanKind.CONSUMER],
+ ];
+ for (const [kind, expected] of cases) {
+ mocks.tracer.startActiveSpan.mockClear();
+ await service.startActiveSpan('op', async () => undefined, {
+ kind: kind as any,
+ });
+ expect(mocks.tracer.startActiveSpan.mock.calls[0][1].kind).toBe(expected);
+ }
+ });
+
+ it('adds backstage.principal.type but not enduser.id when capture is off', async () => {
+ const service = createService({ captureEndUser: false });
+ await service.startActiveSpan('op', async () => undefined, {
+ credentials: mockCredentials.user('user:default/alice'),
+ });
+
+ const attrs = mocks.tracer.startActiveSpan.mock.calls[0][1].attributes;
+ expect(attrs['backstage.principal.type']).toBe('user');
+ expect(attrs).not.toHaveProperty('enduser.id');
+ });
+
+ it('adds enduser.id from a user principal when capture is on', async () => {
+ const service = createService({ captureEndUser: true });
+ await service.startActiveSpan('op', async () => undefined, {
+ credentials: mockCredentials.user('user:default/alice'),
+ });
+
+ const attrs = mocks.tracer.startActiveSpan.mock.calls[0][1].attributes;
+ expect(attrs['enduser.id']).toBe('user:default/alice');
+ });
+
+ it('adds enduser.id from a service principal subject when capture is on', async () => {
+ const service = createService({ captureEndUser: true });
+ await service.startActiveSpan('op', async () => undefined, {
+ credentials: mockCredentials.service('plugin:test'),
+ });
+
+ const attrs = mocks.tracer.startActiveSpan.mock.calls[0][1].attributes;
+ expect(attrs['enduser.id']).toBe('plugin:test');
+ expect(attrs['backstage.principal.type']).toBe('service');
+ });
+
+ it('extracts credentials from a request via httpAuth when credentials are not supplied', async () => {
+ const httpAuth = mockServices.httpAuth();
+ const credSpy = jest.spyOn(httpAuth, 'credentials');
+ const service = DefaultTracingService.create({
+ name: 'backstage-plugin-test',
+ pluginId: 'test',
+ captureEndUser: true,
+ httpAuth,
+ });
+
+ await service.startActiveSpan('op', async () => undefined, {
+ request: { headers: {} } as any,
+ });
+
+ expect(credSpy).toHaveBeenCalledTimes(1);
+ const attrs = mocks.tracer.startActiveSpan.mock.calls[0][1].attributes;
+ expect(attrs['enduser.id']).toBe('user:default/mock');
+ });
+
+ it('prefers credentials over request when both are supplied (no httpAuth call)', async () => {
+ const httpAuth = mockServices.httpAuth();
+ const credSpy = jest.spyOn(httpAuth, 'credentials');
+ const service = DefaultTracingService.create({
+ name: 'backstage-plugin-test',
+ pluginId: 'test',
+ captureEndUser: true,
+ httpAuth,
+ });
+
+ await service.startActiveSpan('op', async () => undefined, {
+ credentials: mockCredentials.user('user:default/explicit'),
+ request: { headers: {} } as any,
+ });
+
+ expect(credSpy).not.toHaveBeenCalled();
+ const attrs = mocks.tracer.startActiveSpan.mock.calls[0][1].attributes;
+ expect(attrs['enduser.id']).toBe('user:default/explicit');
+ });
+
+ it('translates Backstage span status codes to OTel SpanStatusCode on the underlying span', async () => {
+ const service = createService();
+ await service.startActiveSpan('op', async span => {
+ span.setStatus({ code: 'ok' });
+ });
+ expect(mocks.span.setStatus).toHaveBeenCalledWith({
+ code: SpanStatusCode.OK,
+ message: undefined,
+ });
+
+ mocks.span.setStatus.mockClear();
+ await service.startActiveSpan('op', async span => {
+ span.setStatus({ code: 'error', message: 'boom' });
+ });
+ expect(mocks.span.setStatus).toHaveBeenCalledWith({
+ code: SpanStatusCode.ERROR,
+ message: 'boom',
+ });
+ });
+
+ it('records exceptions, sets error.type, sets ERROR status, and ends the span on throw', async () => {
+ const service = createService();
+ const boom = new Error('Boom');
+ boom.name = 'CustomError';
+
+ await expect(
+ service.startActiveSpan('op', async () => {
+ throw boom;
+ }),
+ ).rejects.toThrow('Boom');
+
+ expect(mocks.span.recordException).toHaveBeenCalledWith(boom);
+ expect(mocks.span.setAttribute).toHaveBeenCalledWith(
+ 'error.type',
+ 'CustomError',
+ );
+ expect(mocks.span.setStatus).toHaveBeenCalledWith({
+ code: SpanStatusCode.ERROR,
+ message: 'Boom',
+ });
+ expect(mocks.span.end).toHaveBeenCalled();
+ });
+
+ it('ends the span and returns the value on success', async () => {
+ const service = createService();
+ const value = await service.startActiveSpan('op', () => 42);
+ expect(value).toBe(42);
+ expect(mocks.span.end).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/backend-defaults/src/alpha/entrypoints/tracing/DefaultTracingService.ts b/packages/backend-defaults/src/alpha/entrypoints/tracing/DefaultTracingService.ts
new file mode 100644
index 0000000000..77049cedfd
--- /dev/null
+++ b/packages/backend-defaults/src/alpha/entrypoints/tracing/DefaultTracingService.ts
@@ -0,0 +1,171 @@
+/*
+ * 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 { SpanKind, SpanStatusCode, Tracer, trace } from '@opentelemetry/api';
+import {
+ BackstageCredentials,
+ HttpAuthService,
+} from '@backstage/backend-plugin-api';
+import {
+ TracingService,
+ TracingServiceAttributes,
+ TracingServiceSpan,
+ TracingServiceSpanKind,
+ TracingServiceSpanOptions,
+ TracingServiceSpanStatus,
+} from '@backstage/backend-plugin-api/alpha';
+
+/**
+ * Options for creating a {@link DefaultTracingService}.
+ *
+ * @alpha
+ */
+export interface DefaultTracingServiceOptions {
+ name: string;
+ version?: string;
+ schemaUrl?: string;
+ pluginId: string;
+ captureEndUser: boolean;
+ httpAuth: HttpAuthService;
+}
+
+/**
+ * Default implementation of the {@link TracingService} interface.
+ *
+ * @alpha
+ */
+export class DefaultTracingService implements TracingService {
+ private readonly tracer: Tracer;
+ private readonly pluginId: string;
+ private readonly captureEndUser: boolean;
+ private readonly httpAuth: HttpAuthService;
+
+ private constructor(opts: DefaultTracingServiceOptions) {
+ this.tracer = trace
+ .getTracerProvider()
+ .getTracer(opts.name, opts.version, { schemaUrl: opts.schemaUrl });
+ this.pluginId = opts.pluginId;
+ this.captureEndUser = opts.captureEndUser;
+ this.httpAuth = opts.httpAuth;
+ }
+
+ static create(opts: DefaultTracingServiceOptions): TracingService {
+ return new DefaultTracingService(opts);
+ }
+
+ async startActiveSpan(
+ name: string,
+ fn: (span: TracingServiceSpan) => T | Promise,
+ options: TracingServiceSpanOptions = {},
+ ): Promise {
+ let credentials = options.credentials;
+ if (!credentials && options.request) {
+ credentials = await this.httpAuth.credentials(options.request);
+ }
+
+ const principalAttributes = this.getPrincipalAttributes(credentials);
+ const attributes: TracingServiceAttributes = {
+ 'backstage.plugin.id': this.pluginId,
+ ...options.attributes,
+ ...principalAttributes,
+ };
+
+ return this.tracer.startActiveSpan(
+ name,
+ { kind: toSpanKind(options.kind), attributes },
+ async span => {
+ try {
+ const wrapped: TracingServiceSpan = {
+ setAttribute(key, value) {
+ span.setAttribute(key, value);
+ },
+ setStatus(status) {
+ span.setStatus({
+ code: toSpanStatusCode(status.code),
+ message: status.message,
+ });
+ },
+ };
+ const result = await fn(wrapped);
+ span.end();
+ return result;
+ } catch (err) {
+ const error = err as Error;
+ span.recordException(error);
+ span.setAttribute('error.type', error.name || 'Error');
+ span.setStatus({
+ code: SpanStatusCode.ERROR,
+ message: error.message || String(error),
+ });
+ span.end();
+ throw err;
+ }
+ },
+ );
+ }
+
+ private getPrincipalAttributes(
+ credentials: BackstageCredentials | undefined,
+ ): TracingServiceAttributes {
+ if (!credentials) return {};
+ const principal = credentials.principal as
+ | { type?: string; userEntityRef?: string; subject?: string }
+ | undefined;
+ if (!principal?.type) return {};
+ const attrs: TracingServiceAttributes = {
+ 'backstage.principal.type': principal.type,
+ };
+ if (!this.captureEndUser) return attrs;
+ if (principal.type === 'user' && principal.userEntityRef) {
+ attrs['enduser.id'] = principal.userEntityRef;
+ } else if (principal.type === 'service' && principal.subject) {
+ attrs['enduser.id'] = principal.subject;
+ }
+ return attrs;
+ }
+}
+
+function toSpanKind(
+ kind: TracingServiceSpanKind | undefined,
+): SpanKind | undefined {
+ switch (kind) {
+ case 'internal':
+ return SpanKind.INTERNAL;
+ case 'server':
+ return SpanKind.SERVER;
+ case 'client':
+ return SpanKind.CLIENT;
+ case 'producer':
+ return SpanKind.PRODUCER;
+ case 'consumer':
+ return SpanKind.CONSUMER;
+ default:
+ return undefined;
+ }
+}
+
+function toSpanStatusCode(
+ code: TracingServiceSpanStatus['code'],
+): SpanStatusCode {
+ switch (code) {
+ case 'ok':
+ return SpanStatusCode.OK;
+ case 'error':
+ return SpanStatusCode.ERROR;
+ default:
+ return SpanStatusCode.UNSET;
+ }
+}
diff --git a/packages/backend-defaults/src/lib/urls.ts b/packages/backend-defaults/src/alpha/entrypoints/tracing/index.ts
similarity index 73%
rename from packages/backend-defaults/src/lib/urls.ts
rename to packages/backend-defaults/src/alpha/entrypoints/tracing/index.ts
index 848cea25d9..5d432d4ea3 100644
--- a/packages/backend-defaults/src/lib/urls.ts
+++ b/packages/backend-defaults/src/alpha/entrypoints/tracing/index.ts
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 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.
@@ -13,13 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-export function isValidUrl(url: string): boolean {
- try {
- // eslint-disable-next-line no-new
- new URL(url);
- return true;
- } catch {
- return false;
- }
-}
+export { tracingServiceFactory } from './tracingServiceFactory';
diff --git a/packages/backend-defaults/src/alpha/entrypoints/tracing/tracingServiceFactory.test.ts b/packages/backend-defaults/src/alpha/entrypoints/tracing/tracingServiceFactory.test.ts
new file mode 100644
index 0000000000..06870818de
--- /dev/null
+++ b/packages/backend-defaults/src/alpha/entrypoints/tracing/tracingServiceFactory.test.ts
@@ -0,0 +1,133 @@
+/*
+ * 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 { tracingServiceFactory } from './tracingServiceFactory';
+import { DefaultTracingService } from './DefaultTracingService';
+
+describe('tracingServiceFactory', () => {
+ let createSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ createSpy = jest.spyOn(DefaultTracingService, 'create');
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ const defaultDeps = [
+ mockServices.rootConfig.factory(),
+ mockServices.httpAuth.factory(),
+ tracingServiceFactory,
+ ];
+
+ it('uses backstage-plugin-{pluginId} as the tracer name when no config is set', async () => {
+ await ServiceFactoryTester.from(tracingServiceFactory, {
+ dependencies: defaultDeps,
+ }).getSubject('my-plugin');
+
+ expect(createSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'backstage-plugin-my-plugin',
+ version: undefined,
+ schemaUrl: undefined,
+ pluginId: 'my-plugin',
+ captureEndUser: false,
+ }),
+ );
+ });
+
+ it('uses a custom tracer name from config', async () => {
+ await ServiceFactoryTester.from(tracingServiceFactory, {
+ dependencies: [
+ mockServices.rootConfig.factory({
+ data: {
+ backend: {
+ tracing: {
+ plugin: {
+ 'my-plugin': {
+ tracer: { name: 'custom-tracer-name' },
+ },
+ },
+ },
+ },
+ },
+ }),
+ mockServices.httpAuth.factory(),
+ tracingServiceFactory,
+ ],
+ }).getSubject('my-plugin');
+
+ expect(createSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'custom-tracer-name',
+ }),
+ );
+ });
+
+ it('accepts version and schemaUrl from config', async () => {
+ await ServiceFactoryTester.from(tracingServiceFactory, {
+ dependencies: [
+ mockServices.rootConfig.factory({
+ data: {
+ backend: {
+ tracing: {
+ plugin: {
+ 'my-plugin': {
+ tracer: {
+ name: 'my-plugin-tracer',
+ version: '1.2.3',
+ schemaUrl: 'https://example.com/schema',
+ },
+ },
+ },
+ },
+ },
+ },
+ }),
+ mockServices.httpAuth.factory(),
+ tracingServiceFactory,
+ ],
+ }).getSubject('my-plugin');
+
+ expect(createSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'my-plugin-tracer',
+ version: '1.2.3',
+ schemaUrl: 'https://example.com/schema',
+ }),
+ );
+ });
+
+ it('reads backend.tracing.capture.endUser into the service', async () => {
+ await ServiceFactoryTester.from(tracingServiceFactory, {
+ dependencies: [
+ mockServices.rootConfig.factory({
+ data: { backend: { tracing: { capture: { endUser: true } } } },
+ }),
+ mockServices.httpAuth.factory(),
+ tracingServiceFactory,
+ ],
+ }).getSubject('my-plugin');
+
+ expect(createSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ captureEndUser: true }),
+ );
+ });
+});
diff --git a/packages/backend-defaults/src/alpha/entrypoints/tracing/tracingServiceFactory.ts b/packages/backend-defaults/src/alpha/entrypoints/tracing/tracingServiceFactory.ts
new file mode 100644
index 0000000000..69008656a0
--- /dev/null
+++ b/packages/backend-defaults/src/alpha/entrypoints/tracing/tracingServiceFactory.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 { tracingServiceRef } from '@backstage/backend-plugin-api/alpha';
+import {
+ coreServices,
+ createServiceFactory,
+} from '@backstage/backend-plugin-api';
+import { DefaultTracingService } from './DefaultTracingService';
+
+/**
+ * Service factory for emitting plugin-scoped trace spans.
+ *
+ * @alpha
+ */
+export const tracingServiceFactory = createServiceFactory({
+ service: tracingServiceRef,
+ deps: {
+ config: coreServices.rootConfig,
+ pluginMetadata: coreServices.pluginMetadata,
+ httpAuth: coreServices.httpAuth,
+ },
+ factory: ({ config, pluginMetadata, httpAuth }) => {
+ const pluginId = pluginMetadata.getId();
+
+ const tracerConfig = config.getOptionalConfig(
+ `backend.tracing.plugin.${pluginId}.tracer`,
+ );
+ const scopeName = `backstage-plugin-${pluginId}`;
+ const name = tracerConfig?.getOptionalString('name') ?? scopeName;
+ const version = tracerConfig?.getOptionalString('version');
+ const schemaUrl = tracerConfig?.getOptionalString('schemaUrl');
+
+ const captureEndUser =
+ config.getOptionalBoolean('backend.tracing.capture.endUser') ?? false;
+
+ return DefaultTracingService.create({
+ name,
+ version,
+ schemaUrl,
+ pluginId,
+ captureEndUser,
+ httpAuth,
+ });
+ },
+});
diff --git a/packages/backend-defaults/src/alpha/index.ts b/packages/backend-defaults/src/alpha/index.ts
index 1d0ec158af..7dfefbdae5 100644
--- a/packages/backend-defaults/src/alpha/index.ts
+++ b/packages/backend-defaults/src/alpha/index.ts
@@ -18,3 +18,4 @@ export { actionsRegistryServiceFactory } from './entrypoints/actionsRegistry';
export { actionsServiceFactory } from './entrypoints/actions';
export { metricsServiceFactory } from './entrypoints/metrics';
export { rootSystemMetadataServiceFactory } from './entrypoints/rootSystemMetadata';
+export { tracingServiceFactory } from './entrypoints/tracing';
diff --git a/packages/backend-defaults/src/entrypoints/auditor/utils.ts b/packages/backend-defaults/src/entrypoints/auditor/utils.ts
index bf1e1f4756..0513aa84b2 100644
--- a/packages/backend-defaults/src/entrypoints/auditor/utils.ts
+++ b/packages/backend-defaults/src/entrypoints/auditor/utils.ts
@@ -16,8 +16,20 @@
import type { Config } from '@backstage/config';
import { InputError } from '@backstage/errors';
-import { z } from 'zod/v3';
-import { CONFIG_ROOT_KEY, severityLogLevelMappingsSchema } from './types';
+import { z } from 'zod/v4';
+
+const CONFIG_ROOT_KEY = 'backend.auditor';
+
+const logLevel = z.enum(['debug', 'info', 'warn', 'error']);
+
+const severityLogLevelMappingsSchema = z.object({
+ low: logLevel.default('debug'),
+ medium: logLevel.default('info'),
+ high: logLevel.default('info'),
+ critical: logLevel.default('info'),
+});
+
+type SeverityLogLevelMappings = z.infer;
/**
* Gets the `backend.auditor.severityLogLevelMappings` configuration.
@@ -26,41 +38,32 @@ import { CONFIG_ROOT_KEY, severityLogLevelMappingsSchema } from './types';
* @returns The validated severity-to-log-level mappings.
* @throws error - {@link @backstage/errors#InputError} if the mapping configuration is invalid.
*/
-export function getSeverityLogLevelMappings(config: Config) {
+export function getSeverityLogLevelMappings(
+ config: Config,
+): SeverityLogLevelMappings {
const auditorConfig = config.getOptionalConfig(CONFIG_ROOT_KEY);
- const severityLogLevelMappings = {
- low:
- auditorConfig?.getOptionalString('severityLogLevelMappings.low') ??
- 'debug',
- medium:
- auditorConfig?.getOptionalString('severityLogLevelMappings.medium') ??
- 'info',
- high:
- auditorConfig?.getOptionalString('severityLogLevelMappings.high') ??
- 'info',
- critical:
- auditorConfig?.getOptionalString('severityLogLevelMappings.critical') ??
- 'info',
- } as Required>;
+ const input = {
+ low: auditorConfig?.getOptionalString('severityLogLevelMappings.low'),
+ medium: auditorConfig?.getOptionalString('severityLogLevelMappings.medium'),
+ high: auditorConfig?.getOptionalString('severityLogLevelMappings.high'),
+ critical: auditorConfig?.getOptionalString(
+ 'severityLogLevelMappings.critical',
+ ),
+ };
- const res = severityLogLevelMappingsSchema.safeParse(
- severityLogLevelMappings,
- );
- if (!res.success) {
- const key = res.error.issues.at(0)?.path.at(0) as string;
- const value = (
- res.error.issues.at(0) as unknown as Record
- ).received as string;
- const validKeys = (
- res.error.issues.at(0) as unknown as Record
- ).options as string[];
+ const parsed = severityLogLevelMappingsSchema.safeParse(input);
+
+ if (!parsed.success) {
+ const issue = parsed.error.issues[0];
+ const key = issue.path[0] as keyof typeof input;
+ const receivedValue = input[key];
throw new InputError(
- `The configuration value for 'backend.auditor.severityLogLevelMappings.${key}' was given an invalid value: '${value}'. Expected one of the following valid values: '${validKeys.join(
+ `The configuration value for '${CONFIG_ROOT_KEY}.severityLogLevelMappings.${key}' was given an invalid value: '${receivedValue}'. Expected one of the following valid values: '${logLevel.options.join(
', ',
)}'.`,
);
}
- return severityLogLevelMappings;
+ return parsed.data;
}
diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts
index 349ad9bc19..97eb72e36c 100644
--- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts
+++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts
@@ -25,7 +25,7 @@ import {
import { randomBytes } from 'node:crypto';
import { SignJWT, exportJWK, generateKeyPair } from 'jose';
import { DateTime } from 'luxon';
-import { v4 as uuid } from 'uuid';
+import { randomUUID as uuid } from 'node:crypto';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
diff --git a/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts
index 327173fdee..f8164dae19 100644
--- a/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts
+++ b/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts
@@ -19,7 +19,7 @@ import { ConfigReader } from '@backstage/config';
import { SignJWT, exportJWK, generateKeyPair } from 'jose';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
-import { v4 as uuid } from 'uuid';
+import { randomUUID as uuid } from 'node:crypto';
import { jwksTokenHandler } from './jwks';
// Simplified copy of TokenFactory in @backstage/plugin-auth-backend
diff --git a/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts
index 2bd797c929..9f8ce2c561 100644
--- a/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts
+++ b/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts
@@ -16,7 +16,7 @@
import { DiscoveryService, LoggerService } from '@backstage/backend-plugin-api';
import { decodeJwt, importJWK, SignJWT, decodeProtectedHeader } from 'jose';
-import { assertError, AuthenticationError } from '@backstage/errors';
+import { AuthenticationError, toError } from '@backstage/errors';
import { jwtVerify } from 'jose';
import { tokenTypes } from '@backstage/plugin-auth-node';
import { JwksClient } from '../JwksClient';
@@ -210,8 +210,10 @@ export class DefaultPluginTokenHandler implements PluginTokenHandler {
this.supportedTargetPlugins.add(targetPluginId);
return true;
} catch (error) {
- assertError(error);
- this.logger.error('Unexpected failure for target JWKS check', error);
+ this.logger.error(
+ 'Unexpected failure for target JWKS check',
+ toError(error),
+ );
return false;
} finally {
this.targetPluginInflightChecks.delete(targetPluginId);
diff --git a/packages/backend-defaults/src/entrypoints/auth/plugin/keys/DatabasePluginKeySource.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/keys/DatabasePluginKeySource.ts
index 562086a07f..40a47c4ea2 100644
--- a/packages/backend-defaults/src/entrypoints/auth/plugin/keys/DatabasePluginKeySource.ts
+++ b/packages/backend-defaults/src/entrypoints/auth/plugin/keys/DatabasePluginKeySource.ts
@@ -17,7 +17,7 @@
import { DatabaseService, LoggerService } from '@backstage/backend-plugin-api';
import { HumanDuration, durationToMilliseconds } from '@backstage/types';
import { JWK, exportJWK, generateKeyPair } from 'jose';
-import { v4 as uuid } from 'uuid';
+import { randomUUID as uuid } from 'node:crypto';
import { DatabaseKeyStore } from './DatabaseKeyStore';
import { InternalKey, KeyPayload, KeyStore } from './types';
import { PluginKeySource } from './types';
diff --git a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts
index 7205fbd2f5..012b5d9f13 100644
--- a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts
+++ b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts
@@ -26,6 +26,7 @@ import { type Knex } from 'knex';
jest.mock('@google-cloud/cloud-sql-connector');
jest.mock('@azure/identity');
+jest.mock('@aws-sdk/rds-signer');
describe('postgres', () => {
const createMockConnection = () => ({
@@ -587,6 +588,221 @@ describe('postgres', () => {
});
});
+ it('uses the correct config when using rds IAM auth', async () => {
+ const { Signer } = jest.requireMock('@aws-sdk/rds-signer') as jest.Mocked<
+ typeof import('@aws-sdk/rds-signer')
+ >;
+
+ Signer.prototype.getAuthToken.mockResolvedValue('mock-iam-token');
+
+ const configResult = await buildPgDatabaseConfig(
+ new ConfigReader({
+ client: 'pg',
+ connection: {
+ type: 'rds',
+ host: 'mydb.cluster.eu-west-1.rds.amazonaws.com',
+ port: 5432,
+ user: 'postgres',
+ region: 'eu-west-1',
+ },
+ }),
+ );
+
+ expect(Signer).toHaveBeenCalledWith({
+ hostname: 'mydb.cluster.eu-west-1.rds.amazonaws.com',
+ port: 5432,
+ username: 'postgres',
+ region: 'eu-west-1',
+ });
+ expect(configResult).toMatchObject({
+ client: 'pg',
+ connection: expect.any(Function),
+ useNullAsDefault: true,
+ });
+
+ const connectionResult = await (
+ configResult.connection as () => Promise
+ )();
+
+ expect(connectionResult).toMatchObject({
+ host: 'mydb.cluster.eu-west-1.rds.amazonaws.com',
+ port: 5432,
+ user: 'postgres',
+ password: 'mock-iam-token',
+ });
+ expect(connectionResult).not.toHaveProperty('type');
+ expect(connectionResult).not.toHaveProperty('region');
+ });
+
+ it('generates a fresh IAM token on each connection factory call', async () => {
+ const { Signer } = jest.requireMock('@aws-sdk/rds-signer') as jest.Mocked<
+ typeof import('@aws-sdk/rds-signer')
+ >;
+
+ Signer.prototype.getAuthToken
+ .mockResolvedValueOnce('token-1')
+ .mockResolvedValueOnce('token-2');
+
+ const configResult = await buildPgDatabaseConfig(
+ new ConfigReader({
+ client: 'pg',
+ connection: {
+ type: 'rds',
+ host: 'mydb.cluster.eu-west-1.rds.amazonaws.com',
+ port: 5432,
+ user: 'postgres',
+ region: 'eu-west-1',
+ },
+ }),
+ );
+
+ const conn1 = await (configResult.connection as () => Promise)();
+ const conn2 = await (configResult.connection as () => Promise)();
+
+ expect(conn1.password).toBe('token-1');
+ expect(conn2.password).toBe('token-2');
+ });
+
+ it('returns an expirationChecker that reflects the token TTL', async () => {
+ const { Signer } = jest.requireMock('@aws-sdk/rds-signer') as jest.Mocked<
+ typeof import('@aws-sdk/rds-signer')
+ >;
+
+ Signer.prototype.getAuthToken.mockResolvedValue('mock-iam-token');
+
+ const configResult = await buildPgDatabaseConfig(
+ new ConfigReader({
+ client: 'pg',
+ connection: {
+ type: 'rds',
+ host: 'mydb.cluster.eu-west-1.rds.amazonaws.com',
+ port: 5432,
+ user: 'postgres',
+ region: 'eu-west-1',
+ },
+ }),
+ );
+
+ const conn = await (configResult.connection as () => Promise)();
+
+ expect(conn.expirationChecker).toBeInstanceOf(Function);
+ // Token was just issued, so it should not yet be considered expired.
+ expect(conn.expirationChecker()).toBe(false);
+ });
+
+ it('throws when port is missing for rds connection', async () => {
+ await expect(
+ buildPgDatabaseConfig(
+ new ConfigReader({
+ client: 'pg',
+ connection: {
+ type: 'rds',
+ host: 'mydb.cluster.eu-west-1.rds.amazonaws.com',
+ user: 'postgres',
+ region: 'eu-west-1',
+ },
+ }),
+ ),
+ ).rejects.toThrow(/connection\.port/);
+ });
+
+ it('falls back to AWS_REGION env var when region is not set in config', async () => {
+ const { Signer } = jest.requireMock('@aws-sdk/rds-signer') as jest.Mocked<
+ typeof import('@aws-sdk/rds-signer')
+ >;
+
+ Signer.prototype.getAuthToken.mockResolvedValue('mock-iam-token');
+
+ const originalRegion = process.env.AWS_REGION;
+ process.env.AWS_REGION = 'us-east-1';
+
+ try {
+ await buildPgDatabaseConfig(
+ new ConfigReader({
+ client: 'pg',
+ connection: {
+ type: 'rds',
+ host: 'mydb.cluster.us-east-1.rds.amazonaws.com',
+ port: 5432,
+ user: 'postgres',
+ },
+ }),
+ );
+
+ expect(Signer).toHaveBeenCalledWith(
+ expect.objectContaining({ region: 'us-east-1' }),
+ );
+ } finally {
+ if (originalRegion === undefined) {
+ delete process.env.AWS_REGION;
+ } else {
+ process.env.AWS_REGION = originalRegion;
+ }
+ }
+ });
+
+ it('throws when host is missing for rds connection', async () => {
+ await expect(
+ buildPgDatabaseConfig(
+ new ConfigReader({
+ client: 'pg',
+ connection: {
+ type: 'rds',
+ port: 5432,
+ user: 'postgres',
+ region: 'eu-west-1',
+ },
+ }),
+ ),
+ ).rejects.toThrow(/connection\.host/);
+ });
+
+ it('throws when user is missing for rds connection', async () => {
+ await expect(
+ buildPgDatabaseConfig(
+ new ConfigReader({
+ client: 'pg',
+ connection: {
+ type: 'rds',
+ host: 'mydb.cluster.eu-west-1.rds.amazonaws.com',
+ port: 5432,
+ region: 'eu-west-1',
+ },
+ }),
+ ),
+ ).rejects.toThrow(/connection\.user/);
+ });
+
+ it('throws when region is missing and no env var is set for rds connection', async () => {
+ const originalRegion = process.env.AWS_REGION;
+ const originalDefaultRegion = process.env.AWS_DEFAULT_REGION;
+ delete process.env.AWS_REGION;
+ delete process.env.AWS_DEFAULT_REGION;
+
+ try {
+ await expect(
+ buildPgDatabaseConfig(
+ new ConfigReader({
+ client: 'pg',
+ connection: {
+ type: 'rds',
+ host: 'mydb.cluster.eu-west-1.rds.amazonaws.com',
+ port: 5432,
+ user: 'postgres',
+ },
+ }),
+ ),
+ ).rejects.toThrow(/Missing region for AWS RDS IAM auth/);
+ } finally {
+ if (originalRegion !== undefined) {
+ process.env.AWS_REGION = originalRegion;
+ }
+ if (originalDefaultRegion !== undefined) {
+ process.env.AWS_DEFAULT_REGION = originalDefaultRegion;
+ }
+ }
+ });
+
it('throws an error when the connection type is not supported', async () => {
await expect(
buildPgDatabaseConfig(
diff --git a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts
index afc2125c00..8d880e129f 100644
--- a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts
+++ b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts
@@ -108,6 +108,8 @@ export async function buildPgDatabaseConfig(
return buildAzurePgConfig(mergedConfigReader);
case 'cloudsql':
return buildCloudSqlConfig(mergedConfigReader);
+ case 'rds':
+ return buildRdsPgConfig(mergedConfigReader);
default:
throw new Error(`Unknown connection type: ${config.connection.type}`);
}
@@ -271,6 +273,69 @@ export async function buildCloudSqlConfig(
};
}
+export async function buildRdsPgConfig(config: Config): Promise {
+ const { Signer } =
+ require('@aws-sdk/rds-signer') as typeof import('@aws-sdk/rds-signer');
+
+ let hostname: string;
+ let port: number;
+ let username: string;
+ try {
+ hostname = config.getString('connection.host');
+ port = config.getNumber('connection.port');
+ username = config.getString('connection.user');
+ } catch (err) {
+ throw new ForwardedError(
+ 'AWS RDS IAM auth: missing required database connection config — make sure connection.host, connection.port, and connection.user are set and any environment variables they reference are set',
+ err,
+ );
+ }
+ const region =
+ config.getOptionalString('connection.region') ??
+ process.env.AWS_REGION ??
+ process.env.AWS_DEFAULT_REGION;
+ if (!region) {
+ throw new Error(
+ 'Missing region for AWS RDS IAM auth: set connection.region or the AWS_REGION environment variable',
+ );
+ }
+
+ const rawConfig = config.get() as Record;
+ const sanitizedConnection = omit(
+ config.get('connection') as Record,
+ ['type', 'region'],
+ ) as Partial;
+
+ const signer = new Signer({ hostname, port, username, region });
+
+ // RDS IAM auth tokens are valid for 15 minutes. Renew 1 minute early so
+ // that pooled connections are refreshed before the token actually expires.
+ const tokenTtlMs = 15 * 60 * 1000;
+ const renewalOffsetMs = 60 * 1000;
+
+ async function getConnectionConfig() {
+ try {
+ const password = await signer.getAuthToken();
+ const tokenExpiration = Date.now() + tokenTtlMs - renewalOffsetMs;
+ return {
+ ...sanitizedConnection,
+ password,
+ expirationChecker: () => tokenExpiration <= Date.now(),
+ };
+ } catch (err) {
+ throw new ForwardedError(
+ `AWS RDS IAM auth token acquisition failed for ${username}@${hostname}:${port}`,
+ err,
+ );
+ }
+ }
+
+ return {
+ ...(rawConfig as Record),
+ connection: getConnectionConfig,
+ };
+}
+
/**
* Gets the postgres connection config
*
@@ -323,18 +388,20 @@ export async function ensurePgDatabaseExists(
dbConfig: Config,
...databases: Array
) {
- const admin = await createPgDatabaseClient(dbConfig, {
- connection: {
- database: 'postgres',
- },
- pool: {
- min: 0,
- acquireTimeoutMillis: 10000,
- },
- });
+ // Implements a single existence check attempt
+ const ensureDatabase = async (database: string) => {
+ const admin = await createPgDatabaseClient(dbConfig, {
+ connection: {
+ database: 'postgres',
+ },
+ pool: {
+ min: 0,
+ max: 1,
+ acquireTimeoutMillis: 10000,
+ },
+ });
- try {
- const ensureDatabase = async (database: string) => {
+ try {
const result = await admin
.from('pg_database')
.where('datname', database)
@@ -345,28 +412,30 @@ export async function ensurePgDatabaseExists(
}
await admin.raw(`CREATE DATABASE ??`, [database]);
- };
+ } finally {
+ await admin.destroy();
+ }
+ };
- await Promise.all(
- databases.map(async database => {
- // For initial setup we use a smaller timeout but several retries. Given that this
- // is a separate connection pool we should never really run into issues with connection
- // acquisition timeouts, but we do anyway. This might be a bug in knex or some other dependency.
- let lastErr: Error | undefined = undefined;
- for (let i = 0; i < 3; i++) {
- try {
- return await ddlLimiter(() => ensureDatabase(database));
- } catch (err) {
- lastErr = err;
+ await Promise.all(
+ databases.map(async database => {
+ // For initial setup we use a smaller timeout but several retries. Given that this
+ // is a separate connection pool we should never really run into issues with connection
+ // acquisition timeouts, but we do anyway. This might be a bug in knex or some other dependency.
+ const maxAttempts = 3;
+ for (let attempt = 1; ; attempt++) {
+ try {
+ return await ddlLimiter(() => ensureDatabase(database));
+ } catch (err) {
+ if (attempt >= maxAttempts) {
+ throw err;
+ } else {
+ await new Promise(resolve => setTimeout(resolve, 100));
}
- await new Promise(resolve => setTimeout(resolve, 100));
}
- throw lastErr;
- }),
- );
- } finally {
- await admin.destroy();
- }
+ }
+ }),
+ );
}
/**
diff --git a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts
index 1eb2d7d42b..85cbaaa73b 100644
--- a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts
+++ b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts
@@ -413,6 +413,56 @@ describe('HostDiscovery', () => {
);
});
+ describe('backend.baseUrl warnings', () => {
+ const env = process.env as Record;
+ const originalNodeEnv = env.NODE_ENV;
+
+ afterEach(() => {
+ if (originalNodeEnv) {
+ env.NODE_ENV = originalNodeEnv;
+ } else {
+ delete env.NODE_ENV;
+ }
+ });
+
+ it('warns when backend.baseUrl is a localhost URL and NODE_ENV is production', () => {
+ env.NODE_ENV = 'production';
+ const logger = mockServices.logger.mock();
+
+ HostDiscovery.fromConfig(
+ new ConfigReader({
+ backend: {
+ baseUrl: 'http://localhost:7007',
+ listen: { port: 7007, host: 'localhost' },
+ },
+ }),
+ { logger },
+ );
+
+ expect(logger.warn).toHaveBeenCalledWith(
+ `backend.baseUrl is set to a localhost URL and NODE_ENV is 'production'. This is likely a misconfiguration — localhost URLs are not reachable by other services in a deployed environment. Prefer setting it to a routable URL that can be resolved and reached both by your app and by other plugin deployments / services.`,
+ );
+ });
+
+ it('warns when backend.baseUrl is not a valid URL', () => {
+ const logger = mockServices.logger.mock();
+
+ HostDiscovery.fromConfig(
+ new ConfigReader({
+ backend: {
+ baseUrl: 'not-a-valid-url',
+ listen: { port: 7007, host: 'localhost' },
+ },
+ }),
+ { logger },
+ );
+
+ expect(logger.warn).toHaveBeenCalledWith(
+ `backend.baseUrl config value 'not-a-valid-url' does not appear to be a valid URL.`,
+ );
+ });
+ });
+
it('only accepts SRV URLs in the internal target', async () => {
expect(() =>
HostDiscovery.fromConfig(
diff --git a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts
index 18ed72a085..5e47a9910f 100644
--- a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts
+++ b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts
@@ -100,7 +100,7 @@ export interface HostDiscoveryOptions {
* These endpoints have lower priority than any that are defined in
* app-config, but higher priority than the fallback ones.
*
- * This parameter is usedful for example if you want to provide a shared
+ * This parameter is useful for example if you want to provide a shared
* library of core services to your plugin developers, which is set up for the
* default behaviors in your org. This alleviates the need for replicating any
* given set of endpoint config in every backend that you deploy.
@@ -152,6 +152,27 @@ export class HostDiscovery implements DiscoveryService {
};
static fromConfig(config: RootConfigService, options?: HostDiscoveryOptions) {
+ // The getExternalBaseUrl implementation relies on the backend base URL
+ // being a valid, non-local URL that others will be able to route to.
+ const baseUrl = config.getString('backend.baseUrl');
+ try {
+ const { hostname } = new URL(baseUrl);
+ const isLocalhost =
+ hostname === 'localhost' ||
+ hostname === '127.0.0.1' ||
+ hostname === '::1' ||
+ hostname === '::';
+ if (isLocalhost && process.env.NODE_ENV === 'production') {
+ options?.logger?.warn(
+ `backend.baseUrl is set to a localhost URL and NODE_ENV is '${process.env.NODE_ENV}'. This is likely a misconfiguration — localhost URLs are not reachable by other services in a deployed environment. Prefer setting it to a routable URL that can be resolved and reached both by your app and by other plugin deployments / services.`,
+ );
+ }
+ } catch {
+ options?.logger?.warn(
+ `backend.baseUrl config value '${baseUrl}' does not appear to be a valid URL.`,
+ );
+ }
+
const discovery = new HostDiscovery(new SrvResolvers());
discovery.#updateResolvers(config, options?.defaultEndpoints);
diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/applyInternalErrorFilter.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/applyInternalErrorFilter.ts
index 1771fabcd9..afb3a68e72 100644
--- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/applyInternalErrorFilter.ts
+++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/applyInternalErrorFilter.ts
@@ -15,7 +15,7 @@
*/
import { LoggerService } from '@backstage/backend-plugin-api';
-import { assertError } from '@backstage/errors';
+import { isError, toError } from '@backstage/errors';
import { randomBytes } from 'node:crypto';
function handleBadError(error: Error, logger: LoggerService) {
@@ -37,11 +37,8 @@ export function applyInternalErrorFilter(
error: unknown,
logger: LoggerService,
): Error {
- try {
- assertError(error);
- } catch (assertionError: unknown) {
- assertError(assertionError);
- return handleBadError(assertionError, logger);
+ if (!isError(error)) {
+ return handleBadError(toError(error), logger);
}
const constructorName = error.constructor.name;
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 cc8aac5f51..c91466f667 100644
--- a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts
+++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts
@@ -150,13 +150,13 @@ describe('PluginTaskManagerImpl', () => {
id: 'task1',
timeout: Duration.fromMillis(5000),
frequency: Duration.fromObject({ years: 1 }),
- initialDelay: Duration.fromObject({ years: 1 }),
+ initialDelay: Duration.fromObject({ seconds: 60 }),
fn,
scope: 'global',
});
await manager.triggerTask('task1');
- jest.advanceTimersByTime(5000);
+ await jest.advanceTimersByTimeAsync(65_000);
await promise;
expect(fn).toHaveBeenCalledWith(expect.any(AbortSignal));
diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts
index 4f32dc6863..8481302ba3 100644
--- a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts
+++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts
@@ -19,7 +19,7 @@ import { ConflictError, NotFoundError } from '@backstage/errors';
import { CronTime } from 'cron';
import { Knex } from 'knex';
import { DateTime, Duration } from 'luxon';
-import { v4 as uuid } from 'uuid';
+import { randomUUID as uuid } from 'node:crypto';
import { DB_TASKS_TABLE, DbTasksRow } from '../database/tables';
import {
TaskSettingsV2,
diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts
index c7f77e7a85..7aa26e63fb 100644
--- a/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts
+++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts
@@ -17,7 +17,7 @@
import { JsonObject } from '@backstage/types';
import { CronTime } from 'cron';
import { Duration } from 'luxon';
-import { z } from 'zod/v3';
+import { z } from 'zod/v4';
function isValidOptionalDurationString(d: string | undefined): boolean {
try {
@@ -45,29 +45,6 @@ function isValidTrigger(t: string): boolean {
return t === 'manual';
}
-export const taskSettingsV1Schema = z.object({
- version: z.literal(1),
- initialDelayDuration: z
- .string()
- .optional()
- .refine(isValidOptionalDurationString, {
- message: 'Invalid duration, expecting ISO Period',
- }),
- recurringAtMostEveryDuration: z
- .string()
- .refine(isValidOptionalDurationString, {
- message: 'Invalid duration, expecting ISO Period',
- }),
- timeoutAfterDuration: z.string().refine(isValidOptionalDurationString, {
- message: 'Invalid duration, expecting ISO Period',
- }),
-});
-
-/**
- * The properties that control a scheduled task (version 1).
- */
-export type TaskSettingsV1 = z.infer;
-
export const taskSettingsV2Schema = z.object({
version: z.literal(2),
cadence: z
diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/util.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/util.test.ts
index e2536abb88..c4126588fb 100644
--- a/packages/backend-defaults/src/entrypoints/scheduler/lib/util.test.ts
+++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/util.test.ts
@@ -56,6 +56,29 @@ describe('util', () => {
await promise;
expect(true).toBe(true);
}, 1_000);
+
+ it('handles durations longer than 2^31 ms by chunking setTimeout calls', async () => {
+ jest.useFakeTimers();
+ try {
+ const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000;
+ let resolved = false;
+ const promise = sleep(Duration.fromMillis(thirtyDaysMs)).then(() => {
+ resolved = true;
+ });
+
+ expect(jest.getTimerCount()).toBe(1);
+
+ await jest.advanceTimersByTimeAsync(2 ** 30);
+ expect(resolved).toBe(false);
+ expect(jest.getTimerCount()).toBe(1);
+
+ await jest.advanceTimersByTimeAsync(thirtyDaysMs - 2 ** 30);
+ await promise;
+ expect(resolved).toBe(true);
+ } finally {
+ jest.useRealTimers();
+ }
+ });
});
describe('delegateAbortController', () => {
diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/util.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/util.ts
index 3e42d198df..06d6750ba0 100644
--- a/packages/backend-defaults/src/entrypoints/scheduler/lib/util.ts
+++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/util.ts
@@ -54,6 +54,11 @@ export function nowPlus(duration: Duration | undefined, knex: Knex) {
return knex.raw(`now() + interval '${seconds} seconds'`);
}
+// Node.js setTimeout uses a 32-bit signed integer internally, so timeouts
+// longer than 2^31-1 ms (~24.8 days) fire immediately. We cap each individual
+// wait at 2^30 ms (~12.4 days) and loop until the full duration has elapsed.
+const MAX_TIMEOUT_MS = 2 ** 30;
+
/**
* Sleep for the given duration, but return sooner if the abort signal
* triggers.
@@ -69,19 +74,34 @@ export async function sleep(
return;
}
- await new Promise(resolve => {
- let timeoutHandle: NodeJS.Timeout | undefined = undefined;
+ let remaining = duration.as('milliseconds');
+ if (!Number.isFinite(remaining) || remaining <= 0) {
+ return;
+ }
- const done = () => {
+ await new Promise(resolve => {
+ let timeoutHandle: NodeJS.Timeout | undefined;
+
+ const finish = () => {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
- abortSignal?.removeEventListener('abort', done);
+ abortSignal?.removeEventListener('abort', finish);
resolve();
};
- timeoutHandle = setTimeout(done, duration.as('milliseconds'));
- abortSignal?.addEventListener('abort', done);
+ const tick = () => {
+ if (remaining <= 0) {
+ finish();
+ return;
+ }
+ const chunk = Math.min(remaining, MAX_TIMEOUT_MS);
+ remaining -= chunk;
+ timeoutHandle = setTimeout(tick, chunk);
+ };
+
+ abortSignal?.addEventListener('abort', finish);
+ tick();
});
}
diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.ts
index 10b1061c2d..3202953383 100644
--- a/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.ts
+++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.ts
@@ -32,11 +32,7 @@ import {
AwsCodeCommitIntegration,
ScmIntegrations,
} from '@backstage/integration';
-import {
- assertError,
- ForwardedError,
- NotModifiedError,
-} from '@backstage/errors';
+import { toError, ForwardedError, NotModifiedError } from '@backstage/errors';
import { fromTemporaryCredentials } from '@aws-sdk/credential-providers';
import {
CodeCommitClient,
@@ -418,8 +414,8 @@ export class AwsCodeCommitUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
- } catch (error) {
- assertError(error);
+ } catch (e) {
+ const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsS3UrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsS3UrlReader.ts
index 0b17ffe326..946182ab2e 100644
--- a/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsS3UrlReader.ts
+++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsS3UrlReader.ts
@@ -33,11 +33,7 @@ import {
ScmIntegrations,
AwsS3IntegrationConfig,
} from '@backstage/integration';
-import {
- assertError,
- ForwardedError,
- NotModifiedError,
-} from '@backstage/errors';
+import { toError, ForwardedError, NotModifiedError } from '@backstage/errors';
import { fromTemporaryCredentials } from '@aws-sdk/credential-providers';
import { AwsCredentialIdentityProvider } from '@aws-sdk/types';
import {
@@ -400,8 +396,8 @@ export class AwsS3UrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
- } catch (error) {
- assertError(error);
+ } catch (e) {
+ const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureBlobStorageUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureBlobStorageUrlReader.ts
index b19295ef99..ec04fd2fd8 100644
--- a/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureBlobStorageUrlReader.ts
+++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureBlobStorageUrlReader.ts
@@ -21,11 +21,7 @@ import {
StorageSharedKeyCredential,
} from '@azure/storage-blob';
import { ReaderFactory, ReadTreeResponseFactory } from './types';
-import {
- assertError,
- ForwardedError,
- NotModifiedError,
-} from '@backstage/errors';
+import { toError, ForwardedError, NotModifiedError } from '@backstage/errors';
import { Readable } from 'node:stream';
import { relative } from 'node:path/posix';
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
@@ -261,9 +257,8 @@ export class AzureBlobStorageUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
- } catch (error) {
- assertError(error);
- throw error;
+ } catch (e) {
+ throw toError(e);
}
}
diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.ts
index 66fd1cc95a..91bf000272 100644
--- a/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.ts
+++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.ts
@@ -34,11 +34,7 @@ import {
} from '@backstage/integration';
import parseGitUrl from 'git-url-parse';
import { Minimatch } from 'minimatch';
-import {
- assertError,
- NotFoundError,
- NotModifiedError,
-} from '@backstage/errors';
+import { toError, NotFoundError, NotModifiedError } from '@backstage/errors';
import { ReadTreeResponseFactory, ReaderFactory } from './types';
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
@@ -212,8 +208,8 @@ export class AzureUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
- } catch (error) {
- assertError(error);
+ } catch (e) {
+ const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.ts
index 73302d37e5..b4555ed093 100644
--- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.ts
+++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.ts
@@ -23,11 +23,7 @@ import {
UrlReaderServiceSearchOptions,
UrlReaderServiceSearchResponse,
} from '@backstage/backend-plugin-api';
-import {
- assertError,
- NotFoundError,
- NotModifiedError,
-} from '@backstage/errors';
+import { toError, NotFoundError, NotModifiedError } from '@backstage/errors';
import {
BitbucketCloudIntegration,
getBitbucketCloudDefaultBranch,
@@ -196,8 +192,8 @@ export class BitbucketCloudUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
- } catch (error) {
- assertError(error);
+ } catch (e) {
+ const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.ts
index aba28b4fd7..ed25272dc8 100644
--- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.ts
+++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.ts
@@ -23,11 +23,7 @@ import {
UrlReaderServiceSearchOptions,
UrlReaderServiceSearchResponse,
} from '@backstage/backend-plugin-api';
-import {
- assertError,
- NotFoundError,
- NotModifiedError,
-} from '@backstage/errors';
+import { toError, NotFoundError, NotModifiedError } from '@backstage/errors';
import {
BitbucketServerIntegration,
getBitbucketServerDownloadUrl,
@@ -180,8 +176,8 @@ export class BitbucketServerUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
- } catch (error) {
- assertError(error);
+ } catch (e) {
+ const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.ts
index f49aae9abb..33920d2112 100644
--- a/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.ts
+++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.ts
@@ -22,11 +22,7 @@ import {
UrlReaderServiceSearchOptions,
UrlReaderServiceSearchResponse,
} from '@backstage/backend-plugin-api';
-import {
- assertError,
- NotFoundError,
- NotModifiedError,
-} from '@backstage/errors';
+import { toError, NotFoundError, NotModifiedError } from '@backstage/errors';
import { ReaderFactory } from './types';
import path from 'node:path';
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
@@ -236,8 +232,8 @@ export class FetchUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
- } catch (error) {
- assertError(error);
+ } catch (e) {
+ const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.ts
index 02c36c323b..8cf41f96ff 100644
--- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.ts
+++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.ts
@@ -40,7 +40,7 @@ import {
NotFoundError,
NotModifiedError,
ResponseError,
- assertError,
+ toError,
} from '@backstage/errors';
import { ReadTreeResponseFactory, ReaderFactory } from './types';
import { Minimatch } from 'minimatch';
@@ -174,8 +174,8 @@ export class GerritUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
- } catch (error) {
- assertError(error);
+ } catch (e) {
+ const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.ts
index a7579dca38..ab3bef4e8d 100644
--- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.ts
+++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.ts
@@ -35,7 +35,7 @@ import {
import { ReaderFactory, ReadTreeResponseFactory } from './types';
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
import {
- assertError,
+ toError,
AuthenticationError,
NotFoundError,
NotModifiedError,
@@ -189,8 +189,8 @@ export class GiteaUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
- } catch (error) {
- assertError(error);
+ } catch (e) {
+ const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.ts
index e5aa08275e..442899d50e 100644
--- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.ts
+++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.ts
@@ -37,11 +37,7 @@ import fetch, { RequestInit, Response } from 'node-fetch';
import parseGitUrl from 'git-url-parse';
import { Minimatch } from 'minimatch';
import { Readable } from 'node:stream';
-import {
- assertError,
- NotFoundError,
- NotModifiedError,
-} from '@backstage/errors';
+import { toError, NotFoundError, NotModifiedError } from '@backstage/errors';
import { ReadTreeResponseFactory, ReaderFactory } from './types';
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
import { parseLastModified } from './util';
@@ -209,8 +205,8 @@ export class GithubUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
- } catch (error) {
- assertError(error);
+ } catch (e) {
+ const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts
index a85b9fbb89..23a39003b6 100644
--- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts
+++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts
@@ -23,11 +23,7 @@ import {
UrlReaderServiceSearchOptions,
UrlReaderServiceSearchResponse,
} from '@backstage/backend-plugin-api';
-import {
- assertError,
- NotFoundError,
- NotModifiedError,
-} from '@backstage/errors';
+import { toError, NotFoundError, NotModifiedError } from '@backstage/errors';
import {
getGitLabFileFetchUrl,
getGitLabIntegrationRelativePath,
@@ -260,8 +256,8 @@ export class GitlabUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
- } catch (error) {
- assertError(error);
+ } catch (e) {
+ const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GoogleGcsUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GoogleGcsUrlReader.ts
index 8657125671..618f075089 100644
--- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GoogleGcsUrlReader.ts
+++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GoogleGcsUrlReader.ts
@@ -34,7 +34,7 @@ import {
import { Readable } from 'node:stream';
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
import packageinfo from '../../../../package.json';
-import { assertError } from '@backstage/errors';
+import { toError } from '@backstage/errors';
import { relative } from 'node:path/posix';
const GOOGLE_GCS_HOST = 'storage.cloud.google.com';
@@ -182,8 +182,8 @@ export class GoogleGcsUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
- } catch (error) {
- assertError(error);
+ } catch (e) {
+ const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.ts
index 478bfa14bc..277fe42def 100644
--- a/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.ts
+++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.ts
@@ -36,7 +36,7 @@ import { ReadTreeResponseFactory, ReaderFactory } from './types';
import fetch, { Response } from 'node-fetch';
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
import {
- assertError,
+ toError,
AuthenticationError,
NotFoundError,
NotModifiedError,
@@ -187,8 +187,8 @@ export class HarnessUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
- } catch (error) {
- assertError(error);
+ } catch (e) {
+ const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/util.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/util.ts
index 68c66e5239..0ae0aabdf1 100644
--- a/packages/backend-defaults/src/entrypoints/urlReader/lib/util.ts
+++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/util.ts
@@ -42,7 +42,7 @@ export function responseToReadable(response: Response): Readable {
// The NodeJS ReadableStream is that fetch returns is super basic and not even
// iterable. This function converts it to the smarter, iterable stream/web
// variant instead.
-export function toWeb(
+function toWeb(
responseBody: ReadableStream,
): WebReadableStream {
const reader = responseBody.getReader();
diff --git a/packages/backend-defaults/src/index.ts b/packages/backend-defaults/src/index.ts
index 5f26df8921..ccf75ed9f8 100644
--- a/packages/backend-defaults/src/index.ts
+++ b/packages/backend-defaults/src/index.ts
@@ -20,5 +20,5 @@
* @packageDocumentation
*/
-export { createBackend } from './CreateBackend';
+export { createBackend, defaultServiceFactories } from './CreateBackend';
export { discoveryFeatureLoader } from './discoveryFeatureLoader';
diff --git a/packages/backend-dynamic-feature-service/CHANGELOG.md b/packages/backend-dynamic-feature-service/CHANGELOG.md
index 4174bcb02a..3a9a4f7d10 100644
--- a/packages/backend-dynamic-feature-service/CHANGELOG.md
+++ b/packages/backend-dynamic-feature-service/CHANGELOG.md
@@ -1,5 +1,96 @@
# @backstage/backend-dynamic-feature-service
+## 0.8.2-next.0
+
+### Patch Changes
+
+- 41070b8: Upgraded `@module-federation/enhanced`, `@module-federation/runtime`, and `@module-federation/sdk` from `^0.21.6` to `^2.3.3` to address known vulnerabilities.
+- Updated dependencies
+ - @backstage/errors@1.3.1-next.0
+ - @backstage/backend-defaults@0.17.1-next.0
+ - @backstage/plugin-catalog-backend@3.6.1-next.0
+ - @backstage/plugin-auth-node@0.7.1-next.0
+ - @backstage/backend-openapi-utils@0.6.9-next.0
+ - @backstage/backend-plugin-api@1.9.1-next.0
+ - @backstage/cli-common@0.2.2-next.0
+ - @backstage/cli-node@0.3.2-next.0
+ - @backstage/config@1.3.8-next.0
+ - @backstage/config-loader@1.10.11-next.0
+ - @backstage/plugin-events-backend@0.6.2-next.0
+ - @backstage/plugin-events-node@0.4.22-next.0
+ - @backstage/plugin-permission-common@0.9.9-next.0
+ - @backstage/plugin-permission-node@0.10.13-next.0
+ - @backstage/plugin-scaffolder-node@0.13.3-next.0
+ - @backstage/plugin-search-backend-node@1.4.4-next.0
+ - @backstage/types@1.2.2
+ - @backstage/plugin-app-node@0.1.45-next.0
+ - @backstage/plugin-search-common@1.2.24-next.0
+
+## 0.8.1
+
+### Patch Changes
+
+- 79453c0: Updated dependency `wait-for-expect` to `^4.0.0`.
+- Updated dependencies
+ - @backstage/backend-plugin-api@1.9.0
+ - @backstage/backend-defaults@0.17.0
+ - @backstage/errors@1.3.0
+ - @backstage/plugin-auth-node@0.7.0
+ - @backstage/plugin-catalog-backend@3.6.0
+ - @backstage/cli-common@0.2.1
+ - @backstage/cli-node@0.3.1
+ - @backstage/config-loader@1.10.10
+ - @backstage/plugin-search-backend-node@1.4.3
+ - @backstage/plugin-scaffolder-node@0.13.2
+ - @backstage/backend-openapi-utils@0.6.8
+ - @backstage/plugin-events-backend@0.6.1
+ - @backstage/plugin-permission-node@0.10.12
+ - @backstage/config@1.3.7
+ - @backstage/plugin-app-node@0.1.44
+ - @backstage/plugin-events-node@0.4.21
+ - @backstage/plugin-permission-common@0.9.8
+ - @backstage/plugin-search-common@1.2.23
+
+## 0.8.1-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/errors@1.3.0-next.0
+ - @backstage/plugin-auth-node@0.7.0-next.2
+ - @backstage/plugin-catalog-backend@3.6.0-next.2
+ - @backstage/backend-defaults@0.16.1-next.2
+ - @backstage/cli-common@0.2.1-next.1
+ - @backstage/cli-node@0.3.1-next.1
+ - @backstage/config-loader@1.10.10-next.1
+ - @backstage/plugin-search-backend-node@1.4.3-next.2
+ - @backstage/plugin-scaffolder-node@0.13.2-next.2
+ - @backstage/backend-openapi-utils@0.6.8-next.2
+ - @backstage/backend-plugin-api@1.9.0-next.2
+ - @backstage/config@1.3.7-next.0
+ - @backstage/plugin-events-backend@0.6.1-next.2
+ - @backstage/plugin-events-node@0.4.21-next.2
+ - @backstage/plugin-permission-common@0.9.8-next.0
+ - @backstage/plugin-permission-node@0.10.12-next.2
+ - @backstage/plugin-search-common@1.2.23-next.0
+
+## 0.8.1-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-plugin-api@1.9.0-next.1
+ - @backstage/backend-defaults@0.16.1-next.1
+ - @backstage/plugin-catalog-backend@3.5.1-next.1
+ - @backstage/plugin-auth-node@0.7.0-next.1
+ - @backstage/backend-openapi-utils@0.6.8-next.1
+ - @backstage/plugin-app-node@0.1.44-next.1
+ - @backstage/plugin-events-backend@0.6.1-next.1
+ - @backstage/plugin-events-node@0.4.21-next.1
+ - @backstage/plugin-permission-node@0.10.12-next.1
+ - @backstage/plugin-scaffolder-node@0.13.1-next.1
+ - @backstage/plugin-search-backend-node@1.4.3-next.1
+
## 0.8.1-next.0
### Patch Changes
diff --git a/packages/backend-dynamic-feature-service/knip-report.md b/packages/backend-dynamic-feature-service/knip-report.md
index 01af03f52e..ca7583c881 100644
--- a/packages/backend-dynamic-feature-service/knip-report.md
+++ b/packages/backend-dynamic-feature-service/knip-report.md
@@ -2,16 +2,16 @@
## Unused dependencies (10)
-| Name | Location | Severity |
-| :------------------------------------ | :----------- | :------- |
-| @backstage/plugin-search-backend-node | packages/backend-dynamic-feature-service/package.json | error |
-| @backstage/plugin-permission-common | packages/backend-dynamic-feature-service/package.json | error |
-| @backstage/plugin-catalog-backend | packages/backend-dynamic-feature-service/package.json | error |
-| @backstage/plugin-permission-node | packages/backend-dynamic-feature-service/package.json | error |
-| @backstage/plugin-scaffolder-node | packages/backend-dynamic-feature-service/package.json | error |
-| @backstage/plugin-events-backend | packages/backend-dynamic-feature-service/package.json | error |
-| @backstage/plugin-search-common | packages/backend-dynamic-feature-service/package.json | error |
-| @backstage/plugin-events-node | packages/backend-dynamic-feature-service/package.json | error |
-| @backstage/plugin-auth-node | packages/backend-dynamic-feature-service/package.json | error |
-| express-promise-router | packages/backend-dynamic-feature-service/package.json | error |
+| Name | Location | Severity |
+| :------------------------------------ | :---------------- | :------- |
+| @backstage/plugin-search-backend-node | package.json:67:6 | error |
+| @backstage/plugin-permission-common | package.json:64:6 | error |
+| @backstage/plugin-catalog-backend | package.json:61:6 | error |
+| @backstage/plugin-permission-node | package.json:65:6 | error |
+| @backstage/plugin-scaffolder-node | package.json:66:6 | error |
+| @backstage/plugin-events-backend | package.json:62:6 | error |
+| @backstage/plugin-search-common | package.json:68:6 | error |
+| @backstage/plugin-events-node | package.json:63:6 | error |
+| @backstage/plugin-auth-node | package.json:60:6 | error |
+| express-promise-router | package.json:74:6 | error |
diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json
index 64fc088367..381a9525d6 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.8.1-next.0",
+ "version": "0.8.2-next.0",
"description": "Backstage dynamic feature service",
"backstage": {
"role": "node-library"
@@ -68,7 +68,7 @@
"@backstage/plugin-search-common": "workspace:^",
"@backstage/types": "workspace:^",
"@manypkg/get-packages": "^1.1.3",
- "@module-federation/sdk": "^0.21.6",
+ "@module-federation/sdk": "^2.3.3",
"chokidar": "^3.5.3",
"express": "^4.22.0",
"express-promise-router": "^4.1.0",
diff --git a/packages/create-app/templates/next-app/packages/app/.eslintrc.js.hbs b/packages/backend-internal/.eslintrc.js
similarity index 100%
rename from packages/create-app/templates/next-app/packages/app/.eslintrc.js.hbs
rename to packages/backend-internal/.eslintrc.js
diff --git a/packages/backend-internal/README.md b/packages/backend-internal/README.md
new file mode 100644
index 0000000000..8994693a1b
--- /dev/null
+++ b/packages/backend-internal/README.md
@@ -0,0 +1,3 @@
+# @internal/backend
+
+This is an internal package used by the other backend packages. It does not get published to NPM, but instead inlined into consuming packages due to the `backstage.inline` flag in `package.json`.
diff --git a/packages/backend-internal/catalog-info.yaml b/packages/backend-internal/catalog-info.yaml
new file mode 100644
index 0000000000..37bc00382a
--- /dev/null
+++ b/packages/backend-internal/catalog-info.yaml
@@ -0,0 +1,9 @@
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: internal-backend
+ title: '@internal/backend'
+spec:
+ lifecycle: experimental
+ type: backstage-node-library
+ owner: framework-maintainers
diff --git a/packages/backend-internal/knip-report.md b/packages/backend-internal/knip-report.md
new file mode 100644
index 0000000000..2661c35327
--- /dev/null
+++ b/packages/backend-internal/knip-report.md
@@ -0,0 +1,2 @@
+# Knip report
+
diff --git a/packages/backend-internal/package.json b/packages/backend-internal/package.json
new file mode 100644
index 0000000000..b043d1d42f
--- /dev/null
+++ b/packages/backend-internal/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@internal/backend",
+ "version": "0.0.1",
+ "backstage": {
+ "role": "node-library",
+ "inline": true
+ },
+ "private": true,
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "packages/backend-internal"
+ },
+ "license": "Apache-2.0",
+ "sideEffects": false,
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test"
+ },
+ "devDependencies": {
+ "@backstage/cli": "workspace:^"
+ }
+}
diff --git a/packages/backend-internal/src/index.ts b/packages/backend-internal/src/index.ts
new file mode 100644
index 0000000000..add807a2c4
--- /dev/null
+++ b/packages/backend-internal/src/index.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 { OpaqueExtensionPointFactoryMiddleware } from './wiring';
diff --git a/packages/backend-internal/src/wiring/OpaqueExtensionPointFactoryMiddleware.ts b/packages/backend-internal/src/wiring/OpaqueExtensionPointFactoryMiddleware.ts
new file mode 100644
index 0000000000..d94226ed93
--- /dev/null
+++ b/packages/backend-internal/src/wiring/OpaqueExtensionPointFactoryMiddleware.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 { OpaqueType } from '@internal/opaque';
+
+export const OpaqueExtensionPointFactoryMiddleware = OpaqueType.create<{
+ public: { $$type: '@backstage/ExtensionPointFactoryMiddleware' };
+ versions: {
+ readonly version: 'v1';
+ readonly extensionPointId: string;
+ readonly middleware: (original: unknown) => Promise;
+ };
+}>({
+ type: '@backstage/ExtensionPointFactoryMiddleware',
+ versions: ['v1'],
+});
diff --git a/packages/backend-internal/src/wiring/index.ts b/packages/backend-internal/src/wiring/index.ts
new file mode 100644
index 0000000000..72c745bc9b
--- /dev/null
+++ b/packages/backend-internal/src/wiring/index.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 { OpaqueExtensionPointFactoryMiddleware } from './OpaqueExtensionPointFactoryMiddleware';
diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md
index 6882d614a4..55eb1b995d 100644
--- a/packages/backend-openapi-utils/CHANGELOG.md
+++ b/packages/backend-openapi-utils/CHANGELOG.md
@@ -1,5 +1,38 @@
# @backstage/backend-openapi-utils
+## 0.6.9-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/errors@1.3.1-next.0
+ - @backstage/backend-plugin-api@1.9.1-next.0
+ - @backstage/types@1.2.2
+
+## 0.6.8
+
+### Patch Changes
+
+- 59dd9b9: Fixes a memory leak during `wrapServer` where stopped servers weren't removed from the clean up list.
+- Updated dependencies
+ - @backstage/backend-plugin-api@1.9.0
+ - @backstage/errors@1.3.0
+
+## 0.6.8-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/errors@1.3.0-next.0
+ - @backstage/backend-plugin-api@1.9.0-next.2
+
+## 0.6.8-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-plugin-api@1.9.0-next.1
+
## 0.6.8-next.0
### Patch Changes
diff --git a/packages/backend-openapi-utils/knip-report.md b/packages/backend-openapi-utils/knip-report.md
index d2f286fde1..466d97a444 100644
--- a/packages/backend-openapi-utils/knip-report.md
+++ b/packages/backend-openapi-utils/knip-report.md
@@ -2,7 +2,7 @@
## Unused dependencies (1)
-| Name | Location | Severity |
-| :---------------------------- | :----------- | :------- |
-| @backstage/backend-plugin-api | packages/backend-openapi-utils/package.json | error |
+| Name | Location | Severity |
+| :---------------------------- | :---------------- | :------- |
+| @backstage/backend-plugin-api | package.json:49:6 | error |
diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json
index 96f18c5352..7ea0e9a374 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.8-next.0",
+ "version": "0.6.9-next.0",
"description": "OpenAPI typescript support.",
"backstage": {
"role": "node-library"
diff --git a/packages/backend-openapi-utils/src/proxy/setup.ts b/packages/backend-openapi-utils/src/proxy/setup.ts
index ed35a0d3c5..0e47e4a5d6 100644
--- a/packages/backend-openapi-utils/src/proxy/setup.ts
+++ b/packages/backend-openapi-utils/src/proxy/setup.ts
@@ -58,14 +58,40 @@ export class Proxy {
this.express.server = server;
}
- stop() {
+ async stop() {
if (Object.keys(this.#openRequests).length > 0) {
throw new Error('There are still open requests');
}
- this.server.stop();
+ if (!this.express.server) {
+ throw new Error(
+ 'Proxy server was not initialized with an express server',
+ );
+ }
+ const results = await Promise.allSettled([
+ this.server.stop(),
- // If this isn't expressly closed, it will cause a jest memory leak warning.
- this.express.server?.close();
+ // If this isn't expressly closed, it will cause a jest memory leak warning.
+ new Promise((resolve, reject) => {
+ this.express.server?.close(err => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(undefined);
+ }
+ });
+ }),
+ ]);
+ if (results.some(result => result.status === 'rejected')) {
+ const errors = results
+ .filter(
+ (result): result is PromiseRejectedResult =>
+ result.status === 'rejected',
+ )
+ .map(result => result.reason);
+ throw new Error(
+ `Failed to stop proxy server: ${errors.map(e => e.message).join(', ')}`,
+ );
+ }
}
get url() {
diff --git a/packages/backend-openapi-utils/src/testUtils.ts b/packages/backend-openapi-utils/src/testUtils.ts
index 88bb3db5bf..f5244e9e6c 100644
--- a/packages/backend-openapi-utils/src/testUtils.ts
+++ b/packages/backend-openapi-utils/src/testUtils.ts
@@ -17,7 +17,7 @@ import { Express } from 'express';
import { Server } from 'node:http';
import { Proxy } from './proxy/setup';
-const proxiesToCleanup: Proxy[] = [];
+const proxiesToCleanup: Set = new Set();
/**
* !!! THIS CURRENTLY ONLY SUPPORTS SUPERTEST !!!
@@ -29,7 +29,7 @@ const proxiesToCleanup: Proxy[] = [];
*/
export async function wrapServer(app: Express): Promise {
const proxy = new Proxy();
- proxiesToCleanup.push(proxy);
+ proxiesToCleanup.add(proxy);
await proxy.setup();
const server = app.listen(proxy.forwardTo.port);
@@ -48,10 +48,12 @@ function registerHooks() {
}
registered = true;
- afterAll(() => {
- for (const proxy of proxiesToCleanup) {
- proxy.stop();
- }
+ afterAll(async () => {
+ const stopPromises = Array.from(proxiesToCleanup).map(proxy =>
+ proxy.stop(),
+ );
+ await Promise.allSettled(stopPromises);
+ proxiesToCleanup.clear();
});
}
diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md
index ec7df4e13d..78ea0628af 100644
--- a/packages/backend-plugin-api/CHANGELOG.md
+++ b/packages/backend-plugin-api/CHANGELOG.md
@@ -1,5 +1,60 @@
# @backstage/backend-plugin-api
+## 1.9.1-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/errors@1.3.1-next.0
+ - @backstage/plugin-auth-node@0.7.1-next.0
+ - @backstage/cli-common@0.2.2-next.0
+ - @backstage/config@1.3.8-next.0
+ - @backstage/plugin-permission-common@0.9.9-next.0
+ - @backstage/plugin-permission-node@0.10.13-next.0
+ - @backstage/types@1.2.2
+
+## 1.9.0
+
+### Minor Changes
+
+- 4559806: Added support for typed `examples` on actions registered via the actions registry. Action authors can now provide examples with compile-time-checked `input` and `output` values that match their schema definitions.
+
+### Patch Changes
+
+- 213ebe7: Aligned `.T` behavior between `ExtensionPoint` and `ServiceRef` to consistently return `null` instead of throwing.
+- 68c557b: Added stricter type checks in `isDatabaseConflictError`.
+- Updated dependencies
+ - @backstage/errors@1.3.0
+ - @backstage/plugin-auth-node@0.7.0
+ - @backstage/cli-common@0.2.1
+ - @backstage/plugin-permission-node@0.10.12
+ - @backstage/config@1.3.7
+ - @backstage/plugin-permission-common@0.9.8
+
+## 1.9.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/errors@1.3.0-next.0
+ - @backstage/plugin-auth-node@0.7.0-next.2
+ - @backstage/cli-common@0.2.1-next.1
+ - @backstage/config@1.3.7-next.0
+ - @backstage/plugin-permission-common@0.9.8-next.0
+ - @backstage/plugin-permission-node@0.10.12-next.2
+
+## 1.9.0-next.1
+
+### Minor Changes
+
+- 4559806: Added support for typed `examples` on actions registered via the actions registry. Action authors can now provide examples with compile-time-checked `input` and `output` values that match their schema definitions.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-auth-node@0.7.0-next.1
+ - @backstage/plugin-permission-node@0.10.12-next.1
+
## 1.8.1-next.0
### Patch Changes
diff --git a/packages/backend-plugin-api/knip-report.md b/packages/backend-plugin-api/knip-report.md
index 869066c9f6..00fda55abc 100644
--- a/packages/backend-plugin-api/knip-report.md
+++ b/packages/backend-plugin-api/knip-report.md
@@ -2,7 +2,7 @@
## Unused dependencies (1)
-| Name | Location | Severity |
-| :-------------------------- | :----------- | :------- |
-| @backstage/plugin-auth-node | packages/backend-plugin-api/package.json | error |
+| Name | Location | Severity |
+| :-------------------------- | :---------------- | :------- |
+| @backstage/plugin-auth-node | package.json:59:6 | error |
diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json
index f5f7e85db2..c6c8eb2a40 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.8.1-next.0",
+ "version": "1.9.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 d1db9c7eeb..0603f4310e 100644
--- a/packages/backend-plugin-api/report-alpha.api.md
+++ b/packages/backend-plugin-api/report-alpha.api.md
@@ -10,6 +10,7 @@ import { JsonObject } from '@backstage/types';
import { JSONSchema7 } from 'json-schema';
import { JsonValue } from '@backstage/types';
import { LoggerService } from '@backstage/backend-plugin-api';
+import type { Request as Request_2 } from 'express';
import { ServiceRef } from '@backstage/backend-plugin-api';
import { z } from 'zod/v3';
@@ -289,5 +290,68 @@ export const rootSystemMetadataServiceRef: ServiceRef<
'singleton'
>;
+// @alpha
+export interface TracingService {
+ startActiveSpan(
+ name: string,
+ fn: (span: TracingServiceSpan) => T | Promise,
+ options?: TracingServiceSpanOptions,
+ ): Promise;
+}
+
+// @alpha
+export interface TracingServiceAttributes {
+ // (undocumented)
+ [key: string]: TracingServiceAttributeValue | undefined;
+}
+
+// @alpha
+export type TracingServiceAttributeValue =
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | Array;
+
+// @alpha
+export const tracingServiceRef: ServiceRef<
+ TracingService,
+ 'plugin',
+ 'singleton'
+>;
+
+// @alpha
+export interface TracingServiceSpan {
+ // (undocumented)
+ setAttribute(key: string, value: TracingServiceAttributeValue): void;
+ // (undocumented)
+ setStatus(status: TracingServiceSpanStatus): void;
+}
+
+// @alpha
+export type TracingServiceSpanKind =
+ | 'internal'
+ | 'server'
+ | 'client'
+ | 'producer'
+ | 'consumer';
+
+// @alpha
+export interface TracingServiceSpanOptions {
+ attributes?: TracingServiceAttributes;
+ credentials?: BackstageCredentials;
+ kind?: TracingServiceSpanKind;
+ request?: Request_2;
+}
+
+// @alpha
+export interface TracingServiceSpanStatus {
+ // (undocumented)
+ code: 'unset' | 'ok' | 'error';
+ // (undocumented)
+ message?: string;
+}
+
// (No @packageDocumentation comment for this package)
```
diff --git a/packages/backend-plugin-api/src/alpha/TracingService.ts b/packages/backend-plugin-api/src/alpha/TracingService.ts
new file mode 100644
index 0000000000..e0ccf26116
--- /dev/null
+++ b/packages/backend-plugin-api/src/alpha/TracingService.ts
@@ -0,0 +1,109 @@
+/*
+ * 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 type { Request } from 'express';
+import { BackstageCredentials } from '@backstage/backend-plugin-api';
+
+/**
+ * Attribute values that can be attached to spans.
+ *
+ * @alpha
+ */
+export type TracingServiceAttributeValue =
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | Array;
+
+/**
+ * A set of key-value pairs that can be attached to spans.
+ *
+ * @alpha
+ */
+export interface TracingServiceAttributes {
+ [key: string]: TracingServiceAttributeValue | undefined;
+}
+
+/**
+ * The kind of operation a span represents.
+ *
+ * @alpha
+ */
+export type TracingServiceSpanKind =
+ | 'internal'
+ | 'server'
+ | 'client'
+ | 'producer'
+ | 'consumer';
+
+/**
+ * The status of a span.
+ *
+ * @alpha
+ */
+export interface TracingServiceSpanStatus {
+ code: 'unset' | 'ok' | 'error';
+ message?: string;
+}
+
+/**
+ * A trace span. Provided to `startActiveSpan` so
+ * additional attributes or status can be set from within the callback.
+ *
+ * @alpha
+ */
+export interface TracingServiceSpan {
+ setAttribute(key: string, value: TracingServiceAttributeValue): void;
+ setStatus(status: TracingServiceSpanStatus): void;
+}
+
+/**
+ * Options for `startActiveSpan`.
+ *
+ * @alpha
+ */
+export interface TracingServiceSpanOptions {
+ /** Attributes to attach to the span. */
+ attributes?: TracingServiceAttributes;
+ /** The kind of span. */
+ kind?: TracingServiceSpanKind;
+ /**
+ * Authenticated principal source for span enrichment. Preferred when
+ * both `credentials` and `request` are supplied.
+ */
+ credentials?: BackstageCredentials;
+ /** HTTP request to extract credentials from for span enrichment. */
+ request?: Request;
+}
+
+/**
+ * A service for emitting trace spans from a backend plugin.
+ *
+ * @alpha
+ */
+export interface TracingService {
+ /**
+ * Runs `fn` inside a new active span. The span is finished when `fn`
+ * resolves or throws.
+ */
+ startActiveSpan(
+ name: string,
+ fn: (span: TracingServiceSpan) => T | Promise,
+ options?: TracingServiceSpanOptions,
+ ): Promise;
+}
diff --git a/packages/backend-plugin-api/src/alpha/index.ts b/packages/backend-plugin-api/src/alpha/index.ts
index 9ec7ca5b09..4e1e73f23e 100644
--- a/packages/backend-plugin-api/src/alpha/index.ts
+++ b/packages/backend-plugin-api/src/alpha/index.ts
@@ -46,9 +46,20 @@ export type {
MetricsServiceObservableUpDownCounter,
} from './MetricsService';
+export type {
+ TracingService,
+ TracingServiceAttributeValue,
+ TracingServiceAttributes,
+ TracingServiceSpan,
+ TracingServiceSpanKind,
+ TracingServiceSpanOptions,
+ TracingServiceSpanStatus,
+} from './TracingService';
+
export {
actionsRegistryServiceRef,
actionsServiceRef,
metricsServiceRef,
rootSystemMetadataServiceRef,
+ tracingServiceRef,
} from './refs';
diff --git a/packages/backend-plugin-api/src/alpha/refs.ts b/packages/backend-plugin-api/src/alpha/refs.ts
index bd87ea8467..967fce6344 100644
--- a/packages/backend-plugin-api/src/alpha/refs.ts
+++ b/packages/backend-plugin-api/src/alpha/refs.ts
@@ -67,3 +67,16 @@ export const metricsServiceRef = createServiceRef<
>({
id: 'alpha.core.metrics',
});
+
+/**
+ * Service for managing trace spans.
+ *
+ * See {@link TracingService} for the API surface.
+ *
+ * @alpha
+ */
+export const tracingServiceRef = createServiceRef<
+ import('./TracingService').TracingService
+>({
+ id: 'alpha.core.tracing',
+});
diff --git a/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts b/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts
deleted file mode 100644
index 39c62ce731..0000000000
--- a/packages/backend-plugin-api/src/services/definitions/ActionsRegistryService.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * 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 { z, AnyZodObject } from 'zod/v3';
-import { BasicPermission } from '@backstage/plugin-permission-common';
-import { LoggerService } from './LoggerService';
-import { BackstageCredentials } from './AuthService';
-
-/**
- * @public
- */
-export type ActionsRegistryActionContext = {
- input: z.infer;
- logger: LoggerService;
- credentials: BackstageCredentials;
-};
-
-/**
- * An example of how to use an action registered in the actions registry.
- *
- * @public
- */
-export type ActionsRegistryActionExample<
- TInputSchema extends AnyZodObject,
- TOutputSchema extends AnyZodObject,
-> = {
- title: string;
- description?: string;
- input: z.infer;
- output?: z.infer;
-};
-
-/**
- * @public
- */
-export type ActionsRegistryActionOptions<
- TInputSchema extends AnyZodObject,
- TOutputSchema extends AnyZodObject,
-> = {
- name: string;
- title: string;
- description: string;
- schema: {
- input: (zod: typeof z) => TInputSchema;
- output: (zod: typeof z) => TOutputSchema;
- };
- examples?: Array>;
- visibilityPermission?: BasicPermission;
- attributes?: {
- destructive?: boolean;
- idempotent?: boolean;
- readOnly?: boolean;
- };
- action: (
- context: ActionsRegistryActionContext,
- ) => Promise<
- z.infer extends void
- ? void
- : { output: z.infer }
- >;
-};
-
-/**
- * @public
- */
-export interface ActionsRegistryService {
- register<
- TInputSchema extends AnyZodObject,
- TOutputSchema extends AnyZodObject,
- >(
- options: ActionsRegistryActionOptions,
- ): void;
-}
diff --git a/packages/backend-plugin-api/src/services/definitions/ActionsService.ts b/packages/backend-plugin-api/src/services/definitions/ActionsService.ts
deleted file mode 100644
index 3afa131915..0000000000
--- a/packages/backend-plugin-api/src/services/definitions/ActionsService.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 2025 The Backstage Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import { JsonObject, JsonValue } from '@backstage/types';
-import { JSONSchema7 } from 'json-schema';
-import { BackstageCredentials } from './AuthService';
-
-/**
- * @public
- */
-export type ActionsServiceAction = {
- id: string;
- name: string;
- title: string;
- description: string;
- schema: {
- input: JSONSchema7;
- output: JSONSchema7;
- };
- examples?: Array<{
- title: string;
- description?: string;
- input: JsonObject;
- output?: JsonObject;
- }>;
- attributes: {
- readOnly: boolean;
- destructive: boolean;
- idempotent: boolean;
- };
-};
-
-/**
- * @public
- */
-export interface ActionsService {
- list: (opts: {
- credentials: BackstageCredentials;
- }) => Promise<{ actions: ActionsServiceAction[] }>;
- invoke(opts: {
- id: string;
- input?: JsonObject;
- credentials: BackstageCredentials;
- }): Promise<{ output: JsonValue }>;
-}
diff --git a/packages/backend-plugin-api/src/services/system/types.test.ts b/packages/backend-plugin-api/src/services/system/types.test.ts
index a98b8c44ee..be588f2036 100644
--- a/packages/backend-plugin-api/src/services/system/types.test.ts
+++ b/packages/backend-plugin-api/src/services/system/types.test.ts
@@ -25,6 +25,18 @@ const rootDep = createServiceRef({ id: 'y', scope: 'root' });
const pluginDep = createServiceRef({ id: 'z' });
function unused(..._any: any[]) {}
+describe('createServiceRef', () => {
+ it('should create a ServiceRef', () => {
+ expect(ref.id).toBe('x');
+ expect(ref.scope).toBe('plugin');
+ expect(ref.T).toBe(null);
+ expect(String(ref)).toBe('serviceRef{x}');
+ expect(JSON.stringify(ref)).toBe(
+ '{"$$type":"@backstage/ServiceRef","id":"x","scope":"plugin","multiton":false}',
+ );
+ });
+});
+
describe('createServiceFactory', () => {
it('should create a plugin scoped factory', () => {
const factory = createServiceFactory({
diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts
index 7e4b78b5a1..cc4c28c8c1 100644
--- a/packages/backend-plugin-api/src/services/system/types.ts
+++ b/packages/backend-plugin-api/src/services/system/types.ts
@@ -50,7 +50,7 @@ export type ServiceRef<
/**
* Utility for getting the type of the service, using `typeof serviceRef.T`.
- * Attempting to actually read this value will result in an exception.
+ * Reading this value will always return `null`. It is only intended for use with `typeof serviceRef.T`.
*/
T: TService;
@@ -136,21 +136,18 @@ export function createServiceRef<
TService,
TInstances extends 'singleton' | 'multiton',
>(
- options: ServiceRefOptions,
-): ServiceRef {
+ options: ServiceRefOptions,
+): ServiceRef {
const { id, scope = 'plugin', multiton = false, defaultFactory } = options;
return {
id,
scope,
multiton,
- get T(): TService {
- throw new Error(`tried to read ServiceRef.T of ${this}`);
- },
+ T: null as TService,
toString() {
return `serviceRef{${options.id}}`;
},
toJSON() {
- // This avoids accidental calls to T happening e.g. in tests
return {
$$type: '@backstage/ServiceRef',
id,
diff --git a/packages/backend-plugin-api/src/services/utilities/database.ts b/packages/backend-plugin-api/src/services/utilities/database.ts
index f5abcd7fba..6a550c0d78 100644
--- a/packages/backend-plugin-api/src/services/utilities/database.ts
+++ b/packages/backend-plugin-api/src/services/utilities/database.ts
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import { isError } from '@backstage/errors';
+
/**
* Tries to deduce whether a thrown error is a database conflict.
*
@@ -23,7 +25,7 @@
* known database engine
*/
export function isDatabaseConflictError(e: unknown) {
- const message = (e as any)?.message;
+ const message = isError(e) ? e.message : undefined;
return (
typeof message === 'string' &&
diff --git a/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts b/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts
index 825063e845..df29833432 100644
--- a/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts
+++ b/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts
@@ -18,10 +18,10 @@ import { createExtensionPoint } from './createExtensionPoint';
describe('createExtensionPoint', () => {
it('should create an ExtensionPoint', () => {
- const extensionPoint = createExtensionPoint({ id: 'x' });
+ const extensionPoint = createExtensionPoint({ id: 'x' });
expect(extensionPoint).toBeDefined();
expect(extensionPoint.id).toBe('x');
- expect(() => extensionPoint.T).not.toThrow();
+ expect(extensionPoint.T).toBe(null);
expect(String(extensionPoint)).toBe('extensionPoint{x}');
});
});
diff --git a/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts b/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts
index 9dbae249ea..7f8a45ff5a 100644
--- a/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts
+++ b/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts
@@ -43,13 +43,7 @@ export function createExtensionPoint(
): ExtensionPoint {
return {
id: options.id,
- get T(): T {
- if (process.env.NODE_ENV === 'test') {
- // Avoid throwing errors so tests asserting extensions' properties cannot be easily broken
- return null as T;
- }
- throw new Error(`tried to read ExtensionPoint.T of ${this}`);
- },
+ T: null as T,
toString() {
return `extensionPoint{${options.id}}`;
},
diff --git a/packages/backend-plugin-api/src/wiring/types.ts b/packages/backend-plugin-api/src/wiring/types.ts
index 28b70b547f..8d75c8923f 100644
--- a/packages/backend-plugin-api/src/wiring/types.ts
+++ b/packages/backend-plugin-api/src/wiring/types.ts
@@ -27,7 +27,7 @@ export type ExtensionPoint = {
/**
* Utility for getting the type of the extension point, using `typeof extensionPoint.T`.
- * Attempting to actually read this value will result in an exception.
+ * Reading this value will always return `null`. It is only intended for use with `typeof extensionPoint.T`.
*/
T: T;
@@ -56,7 +56,11 @@ type DepsToInstances<
[key in string]: ServiceRef | ExtensionPoint;
},
> = {
- [key in keyof TDeps]: TDeps[key] extends ServiceRef
+ [key in keyof TDeps]: TDeps[key] extends ServiceRef<
+ unknown,
+ 'root' | 'plugin',
+ 'multiton'
+ >
? Array
: TDeps[key]['T'];
};
diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md
index 27c2d93828..beb06f083b 100644
--- a/packages/backend-test-utils/CHANGELOG.md
+++ b/packages/backend-test-utils/CHANGELOG.md
@@ -1,5 +1,72 @@
# @backstage/backend-test-utils
+## 1.11.3-next.1
+
+### Patch Changes
+
+- e9b78e9: Removed the `uuid` dependency and replaced usage with the built-in `crypto.randomUUID()`.
+- Updated dependencies
+ - @backstage/backend-defaults@0.17.1-next.1
+ - @backstage/plugin-auth-node@0.7.1-next.1
+ - @backstage/plugin-permission-common@0.9.9-next.1
+
+## 1.11.3-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/errors@1.3.1-next.0
+ - @backstage/backend-app-api@1.7.0-next.0
+ - @backstage/backend-defaults@0.17.1-next.0
+ - @backstage/plugin-auth-node@0.7.1-next.0
+ - @backstage/backend-plugin-api@1.9.1-next.0
+ - @backstage/config@1.3.8-next.0
+ - @backstage/plugin-events-node@0.4.22-next.0
+ - @backstage/plugin-permission-common@0.9.9-next.0
+ - @backstage/types@1.2.2
+
+## 1.11.2
+
+### Patch Changes
+
+- 4559806: Added support for typed `examples` on actions registered via the actions registry. Action authors can now provide examples with compile-time-checked `input` and `output` values that match their schema definitions.
+- f44c6bd: Deduplicated internal readiness-polling helpers used by the database and cache test infrastructure.
+- Updated dependencies
+ - @backstage/backend-plugin-api@1.9.0
+ - @backstage/backend-defaults@0.17.0
+ - @backstage/errors@1.3.0
+ - @backstage/plugin-auth-node@0.7.0
+ - @backstage/backend-app-api@1.6.1
+ - @backstage/config@1.3.7
+ - @backstage/plugin-events-node@0.4.21
+ - @backstage/plugin-permission-common@0.9.8
+
+## 1.11.2-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/errors@1.3.0-next.0
+ - @backstage/plugin-auth-node@0.7.0-next.2
+ - @backstage/backend-app-api@1.6.1-next.2
+ - @backstage/backend-defaults@0.16.1-next.2
+ - @backstage/backend-plugin-api@1.9.0-next.2
+ - @backstage/config@1.3.7-next.0
+ - @backstage/plugin-events-node@0.4.21-next.2
+ - @backstage/plugin-permission-common@0.9.8-next.0
+
+## 1.11.2-next.1
+
+### Patch Changes
+
+- 4559806: Added support for typed `examples` on actions registered via the actions registry. Action authors can now provide examples with compile-time-checked `input` and `output` values that match their schema definitions.
+- Updated dependencies
+ - @backstage/backend-plugin-api@1.9.0-next.1
+ - @backstage/backend-defaults@0.16.1-next.1
+ - @backstage/plugin-auth-node@0.7.0-next.1
+ - @backstage/backend-app-api@1.6.1-next.1
+ - @backstage/plugin-events-node@0.4.21-next.1
+
## 1.11.2-next.0
### Patch Changes
diff --git a/packages/backend-test-utils/knip-report.md b/packages/backend-test-utils/knip-report.md
index 860e474541..221cf77a52 100644
--- a/packages/backend-test-utils/knip-report.md
+++ b/packages/backend-test-utils/knip-report.md
@@ -2,19 +2,19 @@
## Unused dependencies (7)
-| Name | Location | Severity |
-| :------------------------------- | :----------- | :------- |
-| @types/express-serve-static-core | packages/backend-test-utils/package.json | error |
-| @backstage/plugin-auth-node | packages/backend-test-utils/package.json | error |
-| better-sqlite3 | packages/backend-test-utils/package.json | error |
-| @types/qs | packages/backend-test-utils/package.json | error |
-| lodash | packages/backend-test-utils/package.json | error |
-| mysql2 | packages/backend-test-utils/package.json | error |
-| pg | packages/backend-test-utils/package.json | error |
+| Name | Location | Severity |
+| :------------------------------- | :---------------- | :------- |
+| @types/express-serve-static-core | package.json:65:6 | error |
+| @backstage/plugin-auth-node | package.json:57:6 | error |
+| @types/keyv | package.json:66:6 | error |
+| @types/qs | package.json:67:6 | error |
+| lodash | package.json:74:6 | error |
+| mysql2 | package.json:75:6 | error |
+| pg | package.json:76:6 | error |
## Unused devDependencies (1)
-| Name | Location | Severity |
-| :------------ | :----------- | :------- |
-| @types/lodash | packages/backend-test-utils/package.json | error |
+| Name | Location | Severity |
+| :------------ | :---------------- | :------- |
+| @types/lodash | package.json:88:6 | error |
diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json
index 87e6f85ba5..0256992ba2 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.2-next.0",
+ "version": "1.11.3-next.1",
"description": "Test helpers library for Backstage backends",
"backstage": {
"role": "node-library"
@@ -77,7 +77,6 @@
"pg-connection-string": "^2.3.0",
"testcontainers": "^11.9.0",
"text-extensions": "^2.4.0",
- "uuid": "^11.0.0",
"yn": "^4.0.0",
"zod": "^3.25.76 || ^4.0.0",
"zod-to-json-schema": "^3.25.1"
diff --git a/packages/backend-test-utils/report-alpha.api.md b/packages/backend-test-utils/report-alpha.api.md
index 4813f49c35..71848aaaa1 100644
--- a/packages/backend-test-utils/report-alpha.api.md
+++ b/packages/backend-test-utils/report-alpha.api.md
@@ -14,6 +14,10 @@ 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';
+import { TracingService } from '@backstage/backend-plugin-api/alpha';
+import { TracingServiceAttributeValue } from '@backstage/backend-plugin-api/alpha';
+import { TracingServiceSpan } from '@backstage/backend-plugin-api/alpha';
+import { TracingServiceSpanStatus } from '@backstage/backend-plugin-api/alpha';
// @alpha (undocumented)
export function actionsRegistryServiceMock(options?: {
@@ -81,6 +85,14 @@ export class MockActionsRegistry
>(options: ActionsRegistryActionOptions): void;
}
+// @alpha
+export interface MockedTracingServiceSpan extends TracingServiceSpan {
+ // (undocumented)
+ setAttribute: jest.Mock;
+ // (undocumented)
+ setStatus: jest.Mock;
+}
+
// @public (undocumented)
export type ServiceMock = {
factory: ServiceFactory;
@@ -92,5 +104,20 @@ export type ServiceMock