From 9dc1bd53d32a8e8f05471c8ef6bee51807bbf78f Mon Sep 17 00:00:00 2001 From: Rajib Quayum Date: Fri, 27 Feb 2026 12:32:54 -0500 Subject: [PATCH 01/48] fix: move MUI 5 class name setup to own file with new entry point in package, update various index files to import class name setup as the first thing Signed-off-by: Rajib Quayum --- packages/app-legacy/src/index.tsx | 1 + packages/app/src/index.tsx | 1 + .../default-app/packages/app/src/index.tsx | 1 + .../next-app/packages/app/src/index.tsx | 1 + packages/theme/package.json | 9 ++++- .../theme/src/unified/MuiClassNameSetup.ts | 40 +++++++++++++++++++ .../src/unified/UnifiedThemeProvider.tsx | 11 +---- packages/theme/src/unified/index.ts | 1 + 8 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 packages/theme/src/unified/MuiClassNameSetup.ts diff --git a/packages/app-legacy/src/index.tsx b/packages/app-legacy/src/index.tsx index 05dcc024bf..d7699e3d9d 100644 --- a/packages/app-legacy/src/index.tsx +++ b/packages/app-legacy/src/index.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import '@backstage/theme/MuiClassNameSetup'; import '@backstage/cli/asset-types'; import ReactDOM from 'react-dom/client'; import App from './App'; diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx index fd86261385..873670de0f 100644 --- a/packages/app/src/index.tsx +++ b/packages/app/src/index.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import '@backstage/theme/MuiClassNameSetup'; import '@backstage/cli/asset-types'; import ReactDOM from 'react-dom/client'; import app from './App'; diff --git a/packages/create-app/templates/default-app/packages/app/src/index.tsx b/packages/create-app/templates/default-app/packages/app/src/index.tsx index 46f31902f4..3cc56c886c 100644 --- a/packages/create-app/templates/default-app/packages/app/src/index.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/index.tsx @@ -1,3 +1,4 @@ +import '@backstage/theme/MuiClassNameSetup'; import '@backstage/cli/asset-types'; import ReactDOM from 'react-dom/client'; import App from './App'; diff --git a/packages/create-app/templates/next-app/packages/app/src/index.tsx b/packages/create-app/templates/next-app/packages/app/src/index.tsx index ac9e52bdc1..6936ff50bc 100644 --- a/packages/create-app/templates/next-app/packages/app/src/index.tsx +++ b/packages/create-app/templates/next-app/packages/app/src/index.tsx @@ -1,3 +1,4 @@ +import '@backstage/theme/MuiClassNameSetup'; import '@backstage/cli/asset-types'; import ReactDOM from 'react-dom/client'; import App from './App'; diff --git a/packages/theme/package.json b/packages/theme/package.json index e95ae20b36..f27fc9e385 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -20,7 +20,14 @@ "directory": "packages/theme" }, "license": "Apache-2.0", - "sideEffects": false, + "sideEffects": [ + "./src/unified/MuiClassNameSetup.ts" + ], + "exports": { + ".": "./src/index.ts", + "./MuiClassNameSetup": "./src/unified/MuiClassNameSetup.ts", + "./package.json": "./package.json" + }, "main": "src/index.ts", "types": "src/index.ts", "files": [ diff --git a/packages/theme/src/unified/MuiClassNameSetup.ts b/packages/theme/src/unified/MuiClassNameSetup.ts new file mode 100644 index 0000000000..1c1b854211 --- /dev/null +++ b/packages/theme/src/unified/MuiClassNameSetup.ts @@ -0,0 +1,40 @@ +/* + * 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 { unstable_ClassNameGenerator as ClassNameGenerator } from '@mui/material/className'; + +/** + * This API is introduced in @mui/material (v5.0.5) as a replacement of deprecated createGenerateClassName & only affects v5 Material UI components from `@mui/*`. + * + * This needs to be configured before any MUI 5 component can possibly be imported. See: https://v5.mui.com/material-ui/experimental-api/classname-generator/#caveat + * + * ```packages/app/index.ts + * + * import '@backstage/theme/MuiClassNameSetup'; // must be the very first import! + * import '@backstage/cli/asset-types'; + * import ReactDOM from 'react-dom/client'; + * import app from './App'; + * import '@backstage/ui/css/styles.css'; + * + * ReactDOM.createRoot(document.getElementById('root')!).render(app); + * ``` + */ +ClassNameGenerator.configure(componentName => { + return componentName.startsWith('v5-') + ? componentName + : `v5-${componentName}`; +}); + +export {}; diff --git a/packages/theme/src/unified/UnifiedThemeProvider.tsx b/packages/theme/src/unified/UnifiedThemeProvider.tsx index 69d843ed3e..b93746dad4 100644 --- a/packages/theme/src/unified/UnifiedThemeProvider.tsx +++ b/packages/theme/src/unified/UnifiedThemeProvider.tsx @@ -27,7 +27,7 @@ import { Theme as Mui5Theme, } from '@mui/material/styles'; import { UnifiedTheme } from './types'; -import { unstable_ClassNameGenerator as ClassNameGenerator } from '@mui/material/className'; +import './MuiClassNameSetup'; /** * Props for {@link UnifiedThemeProvider}. @@ -41,15 +41,6 @@ export interface UnifiedThemeProviderProps { themeName?: string; } -/** - * This API is introduced in @mui/material (v5.0.5) as a replacement of deprecated createGenerateClassName & only affects v5 Material UI components from `@mui/*`. - * - * This call needs to be in the same module as the `UnifiedThemeProvider` to ensure that it doesn't get removed by tree shaking - */ -ClassNameGenerator.configure(componentName => { - return `v5-${componentName}`; -}); - // Background at https://mui.com/x/migration/migration-data-grid-v4/#using-mui-core-v4-with-v5 // Rather than disabling globals and custom seed, we instead only set a production prefix that // won't collide with Material UI 5 styles. We've already got the separate class name generator diff --git a/packages/theme/src/unified/index.ts b/packages/theme/src/unified/index.ts index 747662e195..2428902981 100644 --- a/packages/theme/src/unified/index.ts +++ b/packages/theme/src/unified/index.ts @@ -21,3 +21,4 @@ export { themes } from './themes'; export { UnifiedThemeProvider } from './UnifiedThemeProvider'; export type { UnifiedThemeProviderProps } from './UnifiedThemeProvider'; export type { UnifiedTheme, SupportedThemes, SupportedVersions } from './types'; +export * from './MuiClassNameSetup'; From 83856ffe55a2b936e286ed11f3042e44f288ee64 Mon Sep 17 00:00:00 2001 From: Rajib Quayum Date: Fri, 27 Feb 2026 12:45:57 -0500 Subject: [PATCH 02/48] chore: add changeset Signed-off-by: Rajib Quayum --- .changeset/vast-jeans-boil.md | 24 +++++++++++++++++++ .../theme/src/unified/MuiClassNameSetup.ts | 5 ++-- 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 .changeset/vast-jeans-boil.md diff --git a/.changeset/vast-jeans-boil.md b/.changeset/vast-jeans-boil.md new file mode 100644 index 0000000000..86f63b2a6d --- /dev/null +++ b/.changeset/vast-jeans-boil.md @@ -0,0 +1,24 @@ +--- +'example-app-legacy': minor +'@backstage/create-app': minor +'@backstage/theme': minor +'example-app': minor +--- + +Separates MUI 5 class name generator code into separate file and entry point that can be imported before any MUI 5 component loads. + +This addresses the problem where the elements contain the `v5-` prefix for MUI class names, but the static class names from the library do not. + +Import should be made as follows to ensure the prefix problem is addressed correctly: + +```diff +// packages/app/index.ts + ++ import '@backstage/theme/MuiClassNameSetup'; // must be the very first import! +import '@backstage/cli/asset-types'; +import ReactDOM from 'react-dom/client'; +import app from './App'; +import '@backstage/ui/css/styles.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render(app); +``` diff --git a/packages/theme/src/unified/MuiClassNameSetup.ts b/packages/theme/src/unified/MuiClassNameSetup.ts index 1c1b854211..5d9fcbd93d 100644 --- a/packages/theme/src/unified/MuiClassNameSetup.ts +++ b/packages/theme/src/unified/MuiClassNameSetup.ts @@ -20,9 +20,10 @@ import { unstable_ClassNameGenerator as ClassNameGenerator } from '@mui/material * * This needs to be configured before any MUI 5 component can possibly be imported. See: https://v5.mui.com/material-ui/experimental-api/classname-generator/#caveat * - * ```packages/app/index.ts + * ```diff + * // packages/app/index.ts * - * import '@backstage/theme/MuiClassNameSetup'; // must be the very first import! + * + import '@backstage/theme/MuiClassNameSetup'; // must be the very first import! * import '@backstage/cli/asset-types'; * import ReactDOM from 'react-dom/client'; * import app from './App'; From f738d47c357f7df52d8688254e5498f549004d46 Mon Sep 17 00:00:00 2001 From: Rajib Quayum Date: Fri, 27 Feb 2026 12:50:04 -0500 Subject: [PATCH 03/48] chore: add api report file for MuiClassNameSetup Signed-off-by: Rajib Quayum --- packages/theme/report-MuiClassNameSetup.api.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/theme/report-MuiClassNameSetup.api.md diff --git a/packages/theme/report-MuiClassNameSetup.api.md b/packages/theme/report-MuiClassNameSetup.api.md new file mode 100644 index 0000000000..c05c3c72a8 --- /dev/null +++ b/packages/theme/report-MuiClassNameSetup.api.md @@ -0,0 +1,7 @@ +## API Report File for "@backstage/theme" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// (No @packageDocumentation comment for this package) +``` From 8ac301afc1b0855ece294b6631d3a166b3d46513 Mon Sep 17 00:00:00 2001 From: Rajib Quayum Date: Fri, 27 Feb 2026 13:01:58 -0500 Subject: [PATCH 04/48] chore: fix index.ts path Signed-off-by: Rajib Quayum --- .changeset/vast-jeans-boil.md | 2 +- packages/theme/src/unified/MuiClassNameSetup.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/vast-jeans-boil.md b/.changeset/vast-jeans-boil.md index 86f63b2a6d..9eb9ed235b 100644 --- a/.changeset/vast-jeans-boil.md +++ b/.changeset/vast-jeans-boil.md @@ -12,7 +12,7 @@ This addresses the problem where the elements contain the `v5-` prefix for MUI c Import should be made as follows to ensure the prefix problem is addressed correctly: ```diff -// packages/app/index.ts +// packages/app/src/index.ts + import '@backstage/theme/MuiClassNameSetup'; // must be the very first import! import '@backstage/cli/asset-types'; diff --git a/packages/theme/src/unified/MuiClassNameSetup.ts b/packages/theme/src/unified/MuiClassNameSetup.ts index 5d9fcbd93d..9511376f5a 100644 --- a/packages/theme/src/unified/MuiClassNameSetup.ts +++ b/packages/theme/src/unified/MuiClassNameSetup.ts @@ -21,7 +21,7 @@ import { unstable_ClassNameGenerator as ClassNameGenerator } from '@mui/material * This needs to be configured before any MUI 5 component can possibly be imported. See: https://v5.mui.com/material-ui/experimental-api/classname-generator/#caveat * * ```diff - * // packages/app/index.ts + * // packages/app/src/index.ts * * + import '@backstage/theme/MuiClassNameSetup'; // must be the very first import! * import '@backstage/cli/asset-types'; From bac85bb5a8da5e4351982f2978a12ae20db02260 Mon Sep 17 00:00:00 2001 From: Rajib Quayum Date: Fri, 27 Feb 2026 13:10:23 -0500 Subject: [PATCH 05/48] chore: update changeset to remove unnecessary ones Signed-off-by: Rajib Quayum --- .changeset/vast-jeans-boil.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.changeset/vast-jeans-boil.md b/.changeset/vast-jeans-boil.md index 9eb9ed235b..74b2c481b4 100644 --- a/.changeset/vast-jeans-boil.md +++ b/.changeset/vast-jeans-boil.md @@ -1,8 +1,6 @@ --- -'example-app-legacy': minor '@backstage/create-app': minor '@backstage/theme': minor -'example-app': minor --- Separates MUI 5 class name generator code into separate file and entry point that can be imported before any MUI 5 component loads. From 91f86f80b62905a3166ee238565cc4d1cdd1e9bb Mon Sep 17 00:00:00 2001 From: Rajib Quayum Date: Fri, 27 Feb 2026 13:33:08 -0500 Subject: [PATCH 06/48] fix: repo fix Signed-off-by: Rajib Quayum --- packages/theme/package.json | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/theme/package.json b/packages/theme/package.json index f27fc9e385..a23c87c13d 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -6,9 +6,7 @@ "role": "web-library" }, "publishConfig": { - "access": "public", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "access": "public" }, "keywords": [ "backstage" @@ -28,6 +26,16 @@ "./MuiClassNameSetup": "./src/unified/MuiClassNameSetup.ts", "./package.json": "./package.json" }, + "typesVersions": { + "*": { + "MuiClassNameSetup": [ + "src/unified/MuiClassNameSetup.ts" + ], + "package.json": [ + "package.json" + ] + } + }, "main": "src/index.ts", "types": "src/index.ts", "files": [ From 15279864d68dbd272cdc56d7f371c59056db5194 Mon Sep 17 00:00:00 2001 From: Rajib Quayum Date: Mon, 2 Mar 2026 09:57:19 -0500 Subject: [PATCH 07/48] chore: remove unnecessary export, reorder import in UnififedThemeProvider Signed-off-by: Rajib Quayum --- packages/theme/src/unified/UnifiedThemeProvider.tsx | 2 +- packages/theme/src/unified/index.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/theme/src/unified/UnifiedThemeProvider.tsx b/packages/theme/src/unified/UnifiedThemeProvider.tsx index b93746dad4..678be82209 100644 --- a/packages/theme/src/unified/UnifiedThemeProvider.tsx +++ b/packages/theme/src/unified/UnifiedThemeProvider.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import './MuiClassNameSetup'; import { ReactNode } from 'react'; import { ThemeProvider, @@ -27,7 +28,6 @@ import { Theme as Mui5Theme, } from '@mui/material/styles'; import { UnifiedTheme } from './types'; -import './MuiClassNameSetup'; /** * Props for {@link UnifiedThemeProvider}. diff --git a/packages/theme/src/unified/index.ts b/packages/theme/src/unified/index.ts index 2428902981..747662e195 100644 --- a/packages/theme/src/unified/index.ts +++ b/packages/theme/src/unified/index.ts @@ -21,4 +21,3 @@ export { themes } from './themes'; export { UnifiedThemeProvider } from './UnifiedThemeProvider'; export type { UnifiedThemeProviderProps } from './UnifiedThemeProvider'; export type { UnifiedTheme, SupportedThemes, SupportedVersions } from './types'; -export * from './MuiClassNameSetup'; From 39557edd70b1667a3fcd825789ae9169a5af9d06 Mon Sep 17 00:00:00 2001 From: Rajib Quayum Date: Mon, 2 Mar 2026 10:01:02 -0500 Subject: [PATCH 08/48] chore: update sideEffects Signed-off-by: Rajib Quayum --- packages/theme/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/theme/package.json b/packages/theme/package.json index a23c87c13d..fe272bcf1c 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -19,7 +19,8 @@ }, "license": "Apache-2.0", "sideEffects": [ - "./src/unified/MuiClassNameSetup.ts" + "./src/unified/MuiClassNameSetup.ts", + "./dist/MuiClassNameSetup.*" ], "exports": { ".": "./src/index.ts", From beb8eda6a3eb39107a4b0e63ce5057ab73ef54ae Mon Sep 17 00:00:00 2001 From: Rajib Quayum Date: Mon, 2 Mar 2026 10:01:28 -0500 Subject: [PATCH 09/48] chore: fix references to index.tsx in docs Signed-off-by: Rajib Quayum --- .changeset/vast-jeans-boil.md | 2 +- packages/theme/src/unified/MuiClassNameSetup.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/vast-jeans-boil.md b/.changeset/vast-jeans-boil.md index 74b2c481b4..7fbc49cdfa 100644 --- a/.changeset/vast-jeans-boil.md +++ b/.changeset/vast-jeans-boil.md @@ -10,7 +10,7 @@ This addresses the problem where the elements contain the `v5-` prefix for MUI c Import should be made as follows to ensure the prefix problem is addressed correctly: ```diff -// packages/app/src/index.ts +// packages/app/src/index.tsx + import '@backstage/theme/MuiClassNameSetup'; // must be the very first import! import '@backstage/cli/asset-types'; diff --git a/packages/theme/src/unified/MuiClassNameSetup.ts b/packages/theme/src/unified/MuiClassNameSetup.ts index 9511376f5a..b069394838 100644 --- a/packages/theme/src/unified/MuiClassNameSetup.ts +++ b/packages/theme/src/unified/MuiClassNameSetup.ts @@ -21,7 +21,7 @@ import { unstable_ClassNameGenerator as ClassNameGenerator } from '@mui/material * This needs to be configured before any MUI 5 component can possibly be imported. See: https://v5.mui.com/material-ui/experimental-api/classname-generator/#caveat * * ```diff - * // packages/app/src/index.ts + * // packages/app/src/index.tsx * * + import '@backstage/theme/MuiClassNameSetup'; // must be the very first import! * import '@backstage/cli/asset-types'; From 961e2745487f247e7340230901e39af4845e4f17 Mon Sep 17 00:00:00 2001 From: Kurt King Date: Mon, 23 Mar 2026 23:37:58 -0600 Subject: [PATCH 10/48] refactor: use MetricsService Signed-off-by: Kurt King --- .changeset/evil-seals-smell.md | 5 ++++ plugins/scaffolder-backend/package.json | 1 - .../src/ScaffolderPlugin.ts | 4 ++++ .../src/scaffolder/dryrun/createDryRunner.ts | 2 ++ .../tasks/NunjucksWorkflowRunner.test.ts | 6 ++++- .../tasks/NunjucksWorkflowRunner.ts | 23 ++++++++++--------- .../src/scaffolder/tasks/TaskWorker.test.ts | 6 +++++ .../src/scaffolder/tasks/TaskWorker.ts | 4 ++++ .../src/service/router.test.ts | 6 ++++- .../scaffolder-backend/src/service/router.ts | 9 +++++++- yarn.lock | 1 - 11 files changed, 51 insertions(+), 16 deletions(-) create mode 100644 .changeset/evil-seals-smell.md diff --git a/.changeset/evil-seals-smell.md b/.changeset/evil-seals-smell.md new file mode 100644 index 0000000000..a533720434 --- /dev/null +++ b/.changeset/evil-seals-smell.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Migrated OpenTelemetry metrics to use the `MetricsService` from `@backstage/backend-plugin-api/alpha` instead of the raw `@opentelemetry/api` meter. diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index df6fcb9c89..57730ec700 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -78,7 +78,6 @@ "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/types": "workspace:^", - "@opentelemetry/api": "^1.9.0", "@types/luxon": "^3.0.0", "express": "^4.22.0", "fs-extra": "^11.2.0", diff --git a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts index 17997da485..6cef0d3cde 100644 --- a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts +++ b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts @@ -63,6 +63,7 @@ import { import { actionsServiceRef, actionsRegistryServiceRef, + metricsServiceRef, } from '@backstage/backend-plugin-api/alpha'; import { createScaffolderActions } from './actions'; @@ -151,6 +152,7 @@ export const scaffolderPlugin = createBackendPlugin({ actionsRegistry: actionsServiceRef, actionsRegistryService: actionsRegistryServiceRef, scaffolderService: scaffolderServiceRef, + metrics: metricsServiceRef, }, async init({ logger, @@ -168,6 +170,7 @@ export const scaffolderPlugin = createBackendPlugin({ actionsRegistry, actionsRegistryService, scaffolderService, + metrics, }) { const log = loggerToWinstonLogger(logger); const integrations = ScmIntegrations.fromConfig(config); @@ -244,6 +247,7 @@ export const scaffolderPlugin = createBackendPlugin({ events, auditor, actionsRegistry, + metrics, }); httpRouter.use(router); }, diff --git a/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts b/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts index e01e76064e..1c56248081 100644 --- a/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts @@ -19,6 +19,7 @@ import { BackstageCredentials, LoggerService, } from '@backstage/backend-plugin-api'; +import type { MetricsService } from '@backstage/backend-plugin-api/alpha'; import type { UserEntity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -81,6 +82,7 @@ export type TemplateTesterCreateOptions = { additionalTemplateGlobals?: Record; permissions?: PermissionEvaluator; config?: Config; + metrics: MetricsService; }; /** diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index c063e38cb2..6b07936e57 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -39,7 +39,10 @@ import { mockCredentials, mockServices, } from '@backstage/backend-test-utils'; -import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha'; +import { + actionsRegistryServiceMock, + metricsServiceMock, +} from '@backstage/backend-test-utils/alpha'; describe('NunjucksWorkflowRunner', () => { let actionRegistry: TemplateActionRegistry; @@ -249,6 +252,7 @@ describe('NunjucksWorkflowRunner', () => { logger, permissions: mockedPermissionApi, config, + metrics: metricsServiceMock.mock(), }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 589ad2e204..5e291973a8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -23,7 +23,6 @@ import { TaskStep, } from '@backstage/plugin-scaffolder-common'; import { JsonArray, JsonObject, JsonValue } from '@backstage/types'; -import { metrics } from '@opentelemetry/api'; import fs from 'fs-extra'; import { validate as validateJsonSchema } from 'jsonschema'; import nunjucks from 'nunjucks'; @@ -42,6 +41,7 @@ import type { LoggerService, PermissionsService, } from '@backstage/backend-plugin-api'; +import type { MetricsService } from '@backstage/backend-plugin-api/alpha'; import { UserEntity } from '@backstage/catalog-model'; import { AuthorizeResult, @@ -78,6 +78,7 @@ type NunjucksWorkflowRunnerOptions = { additionalTemplateGlobals?: Record; permissions?: PermissionsService; config?: Config; + metrics: MetricsService; }; type TemplateContext = { @@ -188,6 +189,8 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { secrets?: Record; } = { parameters: {}, secrets: {} }; + private readonly tracker; + constructor(options: NunjucksWorkflowRunnerOptions) { this.options = options; this.defaultTemplateFilters = convertFiltersToRecord( @@ -195,10 +198,9 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { integrations: this.options.integrations, }), ); + this.tracker = scaffoldingTracker(options.metrics); } - private readonly tracker = scaffoldingTracker(); - async getEnvironmentConfig(): Promise<{ parameters: JsonObject; secrets?: TaskSecrets; @@ -700,7 +702,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { } } -function scaffoldingTracker() { +function scaffoldingTracker(metrics: MetricsService) { // prom-client metrics are deprecated in favour of OpenTelemetry metrics. const promTaskCount = createCounterMetric({ name: 'scaffolder_task_count', @@ -723,23 +725,22 @@ function scaffoldingTracker() { labelNames: ['template', 'step', 'result'], }); - const meter = metrics.getMeter('default'); - const taskCount = meter.createCounter('scaffolder.task.count', { + const taskCount = metrics.createCounter('scaffolder.task.count', { description: 'Count of task runs', }); - const taskDuration = meter.createHistogram('scaffolder.task.duration', { + const taskDuration = metrics.createHistogram('scaffolder.task.duration', { description: 'Duration of a task run', - unit: 'seconds', + unit: 's', }); - const stepCount = meter.createCounter('scaffolder.step.count', { + const stepCount = metrics.createCounter('scaffolder.step.count', { description: 'Count of step runs', }); - const stepDuration = meter.createHistogram('scaffolder.step.duration', { + const stepDuration = metrics.createHistogram('scaffolder.step.duration', { description: 'Duration of a step runs', - unit: 'seconds', + unit: 's', }); async function taskStart(task: TaskContext) { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index 578786cc65..71ed6b9eb4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -36,6 +36,7 @@ import { WorkflowRunner } from './types'; import ObservableImpl from 'zen-observable'; import waitForExpect from 'wait-for-expect'; import { mockServices } from '@backstage/backend-test-utils'; +import { metricsServiceMock } from '@backstage/backend-test-utils/alpha'; import { loggerToWinstonLogger } from '../../util/loggerToWinstonLogger'; jest.mock('./NunjucksWorkflowRunner'); @@ -93,6 +94,7 @@ describe('TaskWorker', () => { integrations, taskBroker: broker, actionRegistry, + metrics: metricsServiceMock.mock(), }); await broker.dispatch({ @@ -124,6 +126,7 @@ describe('TaskWorker', () => { integrations, taskBroker: broker, actionRegistry, + metrics: metricsServiceMock.mock(), }); const { taskId } = await broker.dispatch({ @@ -174,6 +177,7 @@ describe('TaskWorker', () => { }, }, }), + metrics: metricsServiceMock.mock(), }); await taskWorker.runOneTask({ @@ -261,6 +265,7 @@ describe('Concurrent TaskWorker', () => { taskBroker: broker, actionRegistry, concurrentTasksLimit: expectedConcurrentTasks, + metrics: metricsServiceMock.mock(), }); taskWorker.start(); @@ -307,6 +312,7 @@ describe('Cancellable TaskWorker', () => { integrations, taskBroker, actionRegistry, + metrics: metricsServiceMock.mock(), }); const steps = [...Array(10)].map(n => ({ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index b98badd9f6..fdae61a84b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -15,6 +15,7 @@ */ import { AuditorService, LoggerService } from '@backstage/backend-plugin-api'; +import type { MetricsService } from '@backstage/backend-plugin-api/alpha'; import { assertError, InputError, stringifyError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; @@ -78,6 +79,7 @@ export type CreateWorkerOptions = { additionalTemplateGlobals?: Record; permissions?: PermissionEvaluator; gracefulShutdown?: boolean; + metrics: MetricsService; }; /** @@ -123,6 +125,7 @@ export class TaskWorker { additionalTemplateGlobals, permissions, gracefulShutdown, + metrics, } = options; const workflowRunner = new NunjucksWorkflowRunner({ @@ -135,6 +138,7 @@ export class TaskWorker { additionalTemplateGlobals, permissions, config, + metrics, }); return new TaskWorker({ diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 9da276c67b..464614fbea 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -58,7 +58,10 @@ import { import { createDefaultFilters } from '../lib/templating/filters/createDefaultFilters'; import { createRouter } from './router'; import { DatabaseTaskStore } from '../scaffolder/tasks/DatabaseTaskStore'; -import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha'; +import { + actionsRegistryServiceMock, + metricsServiceMock, +} from '@backstage/backend-test-utils/alpha'; import { ActionsService } from '@backstage/backend-plugin-api/alpha'; function createDatabase(): DatabaseService { @@ -229,6 +232,7 @@ const createTestRouter = async ( createDebugLogAction(), ], actionsRegistry: overrides.actionsRegistry ?? actionsRegistryServiceMock(), + metrics: metricsServiceMock.mock(), }); router.use(mockErrorHandler()); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 9e84b38e39..a393df253e 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -131,7 +131,10 @@ import { scaffolderTaskRules, scaffolderTemplateRules, } from './rules'; -import { ActionsService } from '@backstage/backend-plugin-api/alpha'; +import { + ActionsService, + MetricsService, +} from '@backstage/backend-plugin-api/alpha'; /** * RouterOptions @@ -165,6 +168,7 @@ export interface RouterOptions { auditor?: AuditorService; autocompleteHandlers?: Record; actionsRegistry: ActionsService; + metrics: MetricsService; } function isSupportedTemplate(entity: TemplateEntityV1beta3) { @@ -256,6 +260,7 @@ export async function createRouter( httpAuth, auditor, actionsRegistry, + metrics, } = options; const concurrentTasksLimit = @@ -344,6 +349,7 @@ export async function createRouter( concurrentTasksLimit, permissions, gracefulShutdown, + metrics, ...templateExtensions, }); @@ -375,6 +381,7 @@ export async function createRouter( workingDirectory, permissions, config, + metrics, ...templateExtensions, }); diff --git a/yarn.lock b/yarn.lock index 6e0cb55ac9..48fb115c86 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6813,7 +6813,6 @@ __metadata: "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" "@backstage/repo-tools": "workspace:^" "@backstage/types": "workspace:^" - "@opentelemetry/api": "npm:^1.9.0" "@types/express": "npm:^4.17.6" "@types/fs-extra": "npm:^11.0.0" "@types/luxon": "npm:^3.0.0" From 61f754504b2f1acb16d737c571bf67c5ba2d5088 Mon Sep 17 00:00:00 2001 From: Kurt King Date: Tue, 24 Mar 2026 17:28:00 -0600 Subject: [PATCH 11/48] Add type to tracker Signed-off-by: Kurt King --- .../src/scaffolder/tasks/NunjucksWorkflowRunner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 5e291973a8..505dac00f2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -189,7 +189,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { secrets?: Record; } = { parameters: {}, secrets: {} }; - private readonly tracker; + private readonly tracker: ReturnType; constructor(options: NunjucksWorkflowRunnerOptions) { this.options = options; From 088454063c743c8d9ecaf020c15080ef251da94d Mon Sep 17 00:00:00 2001 From: Kurt King Date: Tue, 24 Mar 2026 17:37:27 -0600 Subject: [PATCH 12/48] unpuralize run Signed-off-by: Kurt King --- .../src/scaffolder/tasks/NunjucksWorkflowRunner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 505dac00f2..6f6f0a6d93 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -739,7 +739,7 @@ function scaffoldingTracker(metrics: MetricsService) { }); const stepDuration = metrics.createHistogram('scaffolder.step.duration', { - description: 'Duration of a step runs', + description: 'Duration of a step run', unit: 's', }); From 90aadc64992cb23c350c473ec0a2657a8a85eed7 Mon Sep 17 00:00:00 2001 From: Kurt King Date: Tue, 24 Mar 2026 17:44:02 -0600 Subject: [PATCH 13/48] Be more specific in metric descriptions Signed-off-by: Kurt King --- .../src/scaffolder/tasks/NunjucksWorkflowRunner.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 6f6f0a6d93..a6a2eb2483 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -726,20 +726,20 @@ function scaffoldingTracker(metrics: MetricsService) { }); const taskCount = metrics.createCounter('scaffolder.task.count', { - description: 'Count of task runs', + description: 'Total number of scaffolder tasks executed', }); const taskDuration = metrics.createHistogram('scaffolder.task.duration', { - description: 'Duration of a task run', + description: 'Time taken to complete a scaffolder task end-to-end', unit: 's', }); const stepCount = metrics.createCounter('scaffolder.step.count', { - description: 'Count of step runs', + description: 'Total number of individual scaffolder action steps executed', }); const stepDuration = metrics.createHistogram('scaffolder.step.duration', { - description: 'Duration of a step run', + description: 'Time taken to complete a single scaffolder action step', unit: 's', }); From f14df56222a1ed754b0608ad66e4aa2aaa48c213 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 30 Dec 2024 13:24:31 +0100 Subject: [PATCH 14/48] cli: experimental support for using embedded-postgres as dev DB Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/thin-elephants-joke.md | 5 + packages/cli-module-build/package.json | 2 + .../src/lib/runner/runBackend.ts | 12 +++ .../src/lib/runner/startEmbeddedDb.ts | 67 +++++++++++++ packages/cli-module-build/src/types.d.ts | 20 ++++ yarn.lock | 95 ++++++++++++++++++- 6 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 .changeset/thin-elephants-joke.md create mode 100644 packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts create mode 100644 packages/cli-module-build/src/types.d.ts diff --git a/.changeset/thin-elephants-joke.md b/.changeset/thin-elephants-joke.md new file mode 100644 index 0000000000..e64940551b --- /dev/null +++ b/.changeset/thin-elephants-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The `package start` command now supports an experimental `EXPERIMENTAL_DEV_DB` env flag that can be set to enable the use of `embedded-postgres` as the database for local development, rather than SQLite. For this to work the desired version of the `embedded-postgres` package must be installed in your project, typically as a `devDependency`. diff --git a/packages/cli-module-build/package.json b/packages/cli-module-build/package.json index 605df9a542..ce64c5268f 100644 --- a/packages/cli-module-build/package.json +++ b/packages/cli-module-build/package.json @@ -65,6 +65,7 @@ "cross-spawn": "^7.0.3", "css-loader": "^6.5.1", "ctrlc-windows": "^2.1.0", + "embedded-postgres": "^17.2.0-beta.15", "esbuild-loader": "^4.0.0", "eslint-rspack-plugin": "^4.2.1", "eslint-webpack-plugin": "^4.2.0", @@ -77,6 +78,7 @@ "node-stdlib-browser": "^1.3.1", "npm-packlist": "^5.0.0", "p-queue": "^6.6.2", + "portfinder": "^1.0.32", "postcss": "^8.1.0", "postcss-import": "^16.1.0", "process": "^0.11.10", diff --git a/packages/cli-module-build/src/lib/runner/runBackend.ts b/packages/cli-module-build/src/lib/runner/runBackend.ts index 86c249e8be..4c64c5154e 100644 --- a/packages/cli-module-build/src/lib/runner/runBackend.ts +++ b/packages/cli-module-build/src/lib/runner/runBackend.ts @@ -24,6 +24,7 @@ import { isAbsolute as isAbsolutePath } from 'node:path'; import { targetPaths } from '@backstage/cli-common'; import spawn from 'cross-spawn'; +import { startEmbeddedDb } from './startEmbeddedDb'; const loaderArgs = [ '--enable-source-maps', @@ -57,6 +58,16 @@ export async function runBackend(options: RunBackendOptions) { const server = new IpcServer(); ServerDataStore.bind(server); + const extraEnv: Record = {}; + + if (process.env.EXPERIMENTAL_DEV_DB) { + const db = await startEmbeddedDb(); + extraEnv.APP_CONFIG_backend_database = JSON.stringify({ + client: 'pg', + connection: db.connection, + }); + } + let exiting = false; let firstStart = true; let child: ChildProcess | undefined; @@ -134,6 +145,7 @@ export async function runBackend(options: RunBackendOptions) { cwd: options.targetDir, env: { ...process.env, + ...extraEnv, BACKSTAGE_CLI_LINKED_WORKSPACE: options.linkedWorkspace, BACKSTAGE_CLI_CHANNEL: '1', ESBK_TSCONFIG_PATH: targetPaths.resolveRoot('tsconfig.json'), diff --git a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts new file mode 100644 index 0000000000..85de29cda2 --- /dev/null +++ b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import os from 'node:os'; +import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import { getPortPromise } from 'portfinder'; + +export async function startEmbeddedDb() { + const { default: EmbeddedPostgres } = await import('embedded-postgres').catch( + error => { + throw new Error( + `Failed to load peer dependency 'embedded-postgres' for generating SQL reports. ` + + `It must be installed as an explicit dependency in your project. Caused by; ${error}`, + ); + }, + ); + + const host = 'localhost'; + const user = 'postgres'; + const password = 'password'; + const port = await getPortPromise(); + const tmpDir = await fs.mkdtemp( + resolvePath(os.tmpdir(), 'backstage-dev-db-'), + ); + const pg = new EmbeddedPostgres({ + databaseDir: tmpDir, + user, + password, + port, + persistent: false, + onError(_messageOrError) {}, + onLog(_message) {}, + }); + + // Create the cluster config files + await pg.initialise(); + + // Start the server + await pg.start(); + + return { + connection: { + host, + user, + password, + port, + }, + async close() { + await pg.stop(); + await fs.rmdir(tmpDir, { recursive: true, maxRetries: 3 }); + }, + }; +} diff --git a/packages/cli-module-build/src/types.d.ts b/packages/cli-module-build/src/types.d.ts new file mode 100644 index 0000000000..2e30114f10 --- /dev/null +++ b/packages/cli-module-build/src/types.d.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// It's missing a types entry point, but has types in dist +declare module 'embedded-postgres' { + export { default } from 'embedded-postgres/dist/index'; +} diff --git a/yarn.lock b/yarn.lock index 13b51ed309..35bca1fb75 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2900,6 +2900,7 @@ __metadata: cross-spawn: "npm:^7.0.3" css-loader: "npm:^6.5.1" ctrlc-windows: "npm:^2.1.0" + embedded-postgres: "npm:^17.2.0-beta.15" esbuild-loader: "npm:^4.0.0" eslint-rspack-plugin: "npm:^4.2.1" eslint-webpack-plugin: "npm:^4.2.0" @@ -2912,6 +2913,7 @@ __metadata: node-stdlib-browser: "npm:^1.3.1" npm-packlist: "npm:^5.0.0" p-queue: "npm:^6.6.2" + portfinder: "npm:^1.0.32" postcss: "npm:^8.1.0" postcss-import: "npm:^16.1.0" process: "npm:^0.11.10" @@ -8557,6 +8559,62 @@ __metadata: languageName: node linkType: hard +"@embedded-postgres/darwin-arm64@npm:^17.2.0-beta.15": + version: 17.2.0-beta.15 + resolution: "@embedded-postgres/darwin-arm64@npm:17.2.0-beta.15" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@embedded-postgres/darwin-x64@npm:^17.2.0-beta.15": + version: 17.2.0-beta.15 + resolution: "@embedded-postgres/darwin-x64@npm:17.2.0-beta.15" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@embedded-postgres/linux-arm64@npm:^17.2.0-beta.15": + version: 17.2.0-beta.15 + resolution: "@embedded-postgres/linux-arm64@npm:17.2.0-beta.15" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@embedded-postgres/linux-arm@npm:^17.2.0-beta.15": + version: 17.2.0-beta.15 + resolution: "@embedded-postgres/linux-arm@npm:17.2.0-beta.15" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@embedded-postgres/linux-ia32@npm:^17.2.0-beta.15": + version: 17.2.0-beta.15 + resolution: "@embedded-postgres/linux-ia32@npm:17.2.0-beta.15" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@embedded-postgres/linux-ppc64@npm:^17.2.0-beta.15": + version: 17.2.0-beta.15 + resolution: "@embedded-postgres/linux-ppc64@npm:17.2.0-beta.15" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@embedded-postgres/linux-x64@npm:^17.2.0-beta.15": + version: 17.2.0-beta.15 + resolution: "@embedded-postgres/linux-x64@npm:17.2.0-beta.15" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@embedded-postgres/windows-x64@npm:^17.2.0-beta.15": + version: 17.2.0-beta.15 + resolution: "@embedded-postgres/windows-x64@npm:17.2.0-beta.15" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@emnapi/core@npm:^1.4.3, @emnapi/core@npm:^1.5.0, @emnapi/core@npm:^1.7.1": version: 1.7.1 resolution: "@emnapi/core@npm:1.7.1" @@ -29366,6 +29424,41 @@ __metadata: languageName: node linkType: hard +"embedded-postgres@npm:^17.2.0-beta.15": + version: 17.2.0-beta.15 + resolution: "embedded-postgres@npm:17.2.0-beta.15" + dependencies: + "@embedded-postgres/darwin-arm64": "npm:^17.2.0-beta.15" + "@embedded-postgres/darwin-x64": "npm:^17.2.0-beta.15" + "@embedded-postgres/linux-arm": "npm:^17.2.0-beta.15" + "@embedded-postgres/linux-arm64": "npm:^17.2.0-beta.15" + "@embedded-postgres/linux-ia32": "npm:^17.2.0-beta.15" + "@embedded-postgres/linux-ppc64": "npm:^17.2.0-beta.15" + "@embedded-postgres/linux-x64": "npm:^17.2.0-beta.15" + "@embedded-postgres/windows-x64": "npm:^17.2.0-beta.15" + async-exit-hook: "npm:^2.0.1" + pg: "npm:^8.7.3" + dependenciesMeta: + "@embedded-postgres/darwin-arm64": + optional: true + "@embedded-postgres/darwin-x64": + optional: true + "@embedded-postgres/linux-arm": + optional: true + "@embedded-postgres/linux-arm64": + optional: true + "@embedded-postgres/linux-ia32": + optional: true + "@embedded-postgres/linux-ppc64": + optional: true + "@embedded-postgres/linux-x64": + optional: true + "@embedded-postgres/windows-x64": + optional: true + checksum: 10/bba9ba0f584bbfba854c60932eafc62c4338e8231793f1c7c3cc4e066895877c606c68b7f5b07ec005de4c7506e81f70bf1cd27d74bf5719c7be334272294ce4 + languageName: node + linkType: hard + "emittery@npm:^0.13.1": version: 0.13.1 resolution: "emittery@npm:0.13.1" @@ -41558,7 +41651,7 @@ __metadata: languageName: node linkType: hard -"pg@npm:^8.11.3, pg@npm:^8.9.0": +"pg@npm:^8.11.3, pg@npm:^8.7.3, pg@npm:^8.9.0": version: 8.20.0 resolution: "pg@npm:8.20.0" dependencies: From 538d0a148871406e0cd28e0a15af445e5c9d1e9e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Apr 2026 13:03:11 +0200 Subject: [PATCH 15/48] cli: enable embedded-postgres via config instead of env var Rather than requiring the `EXPERIMENTAL_DEV_DB` environment variable, the embedded postgres server is now started automatically when `backend.database.client` is set to `embedded-postgres` in the app config. The CLI reads the config before spawning the backend and injects the actual pg connection details via env override. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/thin-elephants-joke.md | 5 ++- packages/backend-defaults/config.d.ts | 2 +- .../commands/package/start/startBackend.ts | 3 ++ .../src/lib/runner/runBackend.ts | 37 ++++++++++++++++++- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/.changeset/thin-elephants-joke.md b/.changeset/thin-elephants-joke.md index e64940551b..a7ed28bc9e 100644 --- a/.changeset/thin-elephants-joke.md +++ b/.changeset/thin-elephants-joke.md @@ -1,5 +1,6 @@ --- -'@backstage/cli': patch +'@backstage/cli-module-build': patch +'@backstage/backend-defaults': patch --- -The `package start` command now supports an experimental `EXPERIMENTAL_DEV_DB` env flag that can be set to enable the use of `embedded-postgres` as the database for local development, rather than SQLite. For this to work the desired version of the `embedded-postgres` package must be installed in your project, typically as a `devDependency`. +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. diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts index 3d09146b0d..40352c5972 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 diff --git a/packages/cli-module-build/src/commands/package/start/startBackend.ts b/packages/cli-module-build/src/commands/package/start/startBackend.ts index a36a93b8ff..7b71e52da3 100644 --- a/packages/cli-module-build/src/commands/package/start/startBackend.ts +++ b/packages/cli-module-build/src/commands/package/start/startBackend.ts @@ -23,6 +23,7 @@ import { runBackend } from '../../../lib/runner'; interface StartBackendOptions { targetDir: string; checksEnabled: boolean; + configPaths?: string[]; inspectEnabled?: boolean | string; inspectBrkEnabled?: boolean | string; linkedWorkspace?: string; @@ -33,6 +34,7 @@ export async function startBackend(options: StartBackendOptions) { const waitForExit = await runBackend({ targetDir: options.targetDir, entry: 'src/index', + configPaths: options.configPaths, inspectEnabled: options.inspectEnabled, inspectBrkEnabled: options.inspectBrkEnabled, linkedWorkspace: options.linkedWorkspace, @@ -56,6 +58,7 @@ export async function startBackendPlugin(options: StartBackendOptions) { const waitForExit = await runBackend({ targetDir: options.targetDir, entry: 'dev/index', + configPaths: options.configPaths, inspectEnabled: options.inspectEnabled, inspectBrkEnabled: options.inspectBrkEnabled, require: options.require, diff --git a/packages/cli-module-build/src/lib/runner/runBackend.ts b/packages/cli-module-build/src/lib/runner/runBackend.ts index 4c64c5154e..c0c88e42db 100644 --- a/packages/cli-module-build/src/lib/runner/runBackend.ts +++ b/packages/cli-module-build/src/lib/runner/runBackend.ts @@ -20,8 +20,13 @@ import { ctrlc } from 'ctrlc-windows'; import { IpcServer, ServerDataStore } from '../ipc'; import debounce from 'lodash/debounce'; import { fileURLToPath } from 'node:url'; -import { isAbsolute as isAbsolutePath } from 'node:path'; +import { + isAbsolute as isAbsolutePath, + resolve as resolvePath, +} from 'node:path'; import { targetPaths } from '@backstage/cli-common'; +import { ConfigSources } from '@backstage/config-loader'; +import { ConfigReader } from '@backstage/config'; import spawn from 'cross-spawn'; import { startEmbeddedDb } from './startEmbeddedDb'; @@ -46,6 +51,8 @@ export type RunBackendOptions = { require?: string | string[]; /** An external linked workspace to override module resolution towards */ linkedWorkspace?: string; + /** Config file paths from --config flags */ + configPaths?: string[]; }; export async function runBackend(options: RunBackendOptions) { @@ -60,7 +67,8 @@ export async function runBackend(options: RunBackendOptions) { const extraEnv: Record = {}; - if (process.env.EXPERIMENTAL_DEV_DB) { + const dbClient = await readDatabaseClient(options.configPaths); + if (dbClient === 'embedded-postgres') { const db = await startEmbeddedDb(); extraEnv.APP_CONFIG_backend_database = JSON.stringify({ client: 'pg', @@ -207,3 +215,28 @@ export async function runBackend(options: RunBackendOptions) { return () => exitPromise; } + +async function readDatabaseClient( + configPaths?: string[], +): Promise { + const rootDir = targetPaths.rootDir; + const source = ConfigSources.default({ + rootDir, + allowMissingDefaultConfig: true, + argv: (configPaths ?? []).flatMap(p => [ + '--config', + resolvePath(rootDir, p), + ]), + }); + + const abortController = new AbortController(); + for await (const { configs } of source.readConfigData({ + signal: abortController.signal, + })) { + abortController.abort(); + return ConfigReader.fromConfigs(configs).getOptionalString( + 'backend.database.client', + ); + } + return undefined; +} From cca9fc2bb62ad5a908c484ca89f8201b960ddf59 Mon Sep 17 00:00:00 2001 From: wpessers Date: Fri, 27 Mar 2026 14:54:37 +0100 Subject: [PATCH 16/48] feat(catalog): add retries to octokit client Signed-off-by: wpessers --- .changeset/true-groups-slide.md | 5 +++++ plugins/catalog-backend-module-github/package.json | 1 + .../catalog-backend-module-github/src/lib/github.test.ts | 3 ++- plugins/catalog-backend-module-github/src/lib/github.ts | 5 +++-- .../src/providers/GithubEntityProvider.test.ts | 1 + .../src/providers/GithubEntityProvider.ts | 7 ++++--- yarn.lock | 1 + 7 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 .changeset/true-groups-slide.md diff --git a/.changeset/true-groups-slide.md b/.changeset/true-groups-slide.md new file mode 100644 index 0000000000..0bf2bdb233 --- /dev/null +++ b/.changeset/true-groups-slide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-github': patch +--- + +Added automatic retry on temporary errors (like 5XX) to the shared GitHub GraphQL client used by `GithubOrgEntityProvider` and replaced the GraphQL client in `GithubEntityProvider` by this one as well, improving resilience against intermittent GitHub API failures. diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index e42357d60c..08c20fd0aa 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -63,6 +63,7 @@ "@octokit/auth-callback": "^5.0.0", "@octokit/core": "^5.2.0", "@octokit/graphql": "^7.0.2", + "@octokit/plugin-retry": "^6.0.0", "@octokit/plugin-throttling": "^8.1.3", "@octokit/rest": "^19.0.3", "@octokit/webhooks-types": "^7.6.1", diff --git a/plugins/catalog-backend-module-github/src/lib/github.test.ts b/plugins/catalog-backend-module-github/src/lib/github.test.ts index 554a482422..81dc77ab5f 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.test.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.test.ts @@ -41,6 +41,7 @@ import { } from './github'; import { Octokit } from '@octokit/core'; import { throttling } from '@octokit/plugin-throttling'; +import { retry } from '@octokit/plugin-retry'; jest.mock('@octokit/core', () => ({ ...jest.requireActual('@octokit/core'), @@ -1011,7 +1012,7 @@ describe('github', () => { }); it('should return a graphql client with throttling', async () => { expect(client).toBeDefined(); - expect(Octokit.plugin).toHaveBeenCalledWith(throttling); + expect(Octokit.plugin).toHaveBeenCalledWith(throttling, retry); }); it('should return a graphql client with the correct options', async () => { diff --git a/plugins/catalog-backend-module-github/src/lib/github.ts b/plugins/catalog-backend-module-github/src/lib/github.ts index 200c7d260d..fc72004d9c 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.ts @@ -30,6 +30,7 @@ import { DeferredEntity } from '@backstage/plugin-catalog-node'; import { Octokit } from '@octokit/core'; import { LoggerService } from '@backstage/backend-plugin-api'; import { throttling } from '@octokit/plugin-throttling'; +import { retry } from '@octokit/plugin-retry'; /** * Configuration for GitHub GraphQL API page sizes. @@ -874,7 +875,7 @@ export const createReplaceEntitiesOperation = }; /** - * Creates a GraphQL Client with Throttling + * Creates a GraphQL Client with Throttling and Retries */ export const createGraphqlClient = (args: { headers: @@ -886,7 +887,7 @@ export const createGraphqlClient = (args: { logger: LoggerService; }): typeof graphql => { const { headers, baseUrl, logger } = args; - const ThrottledOctokit = Octokit.plugin(throttling); + const ThrottledOctokit = Octokit.plugin(throttling, retry); const octokit = new ThrottledOctokit({ throttle: { onRateLimit: (retryAfter, rateLimitData, _, retryCount) => { diff --git a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts index e9e106cce9..dc61d8ecd1 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts @@ -47,6 +47,7 @@ type PartialDeep = T extends (...args: unknown[]) => unknown jest.mock('../lib/github', () => { return { getOrganizationRepositories: jest.fn(), + createGraphqlClient: jest.fn().mockReturnValue(jest.fn()), }; }); class PersistingTaskRunner implements SchedulerServiceTaskRunner { diff --git a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts index e215337d8c..5a830c20b5 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts @@ -32,13 +32,13 @@ import { import { LocationSpec } from '@backstage/plugin-catalog-common'; -import { graphql } from '@octokit/graphql'; import * as uuid from 'uuid'; import { GithubEntityProviderConfig, readProviderConfigs, } from './GithubEntityProviderConfig'; import { + createGraphqlClient, getOrganizationRepositories, getOrganizationRepository, RepositoryResponse, @@ -249,9 +249,10 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { url: orgUrl, }); - return graphql.defaults({ - baseUrl: this.integration.apiBaseUrl, + return createGraphqlClient({ headers, + baseUrl: this.integration.apiBaseUrl!, + logger: this.logger, }); } diff --git a/yarn.lock b/yarn.lock index 13b51ed309..6cc6525165 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4916,6 +4916,7 @@ __metadata: "@octokit/auth-callback": "npm:^5.0.0" "@octokit/core": "npm:^5.2.0" "@octokit/graphql": "npm:^7.0.2" + "@octokit/plugin-retry": "npm:^6.0.0" "@octokit/plugin-throttling": "npm:^8.1.3" "@octokit/rest": "npm:^19.0.3" "@octokit/webhooks-types": "npm:^7.6.1" From 6537e5f8c1447d77d8071456bd8b0bec7f475719 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Apr 2026 15:40:31 +0200 Subject: [PATCH 17/48] cli: fix type errors and test for embedded-postgres Fix implicit any types in startEmbeddedDb callbacks, replace the re-export type declaration with an inline type definition for the embedded-postgres module, and update runBackend tests to mock the config loading and use async timer advancement. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../src/lib/runner/runBackend.test.ts | 73 +++++++++---------- .../src/lib/runner/startEmbeddedDb.ts | 6 +- packages/cli-module-build/src/types.d.ts | 18 ++++- 3 files changed, 55 insertions(+), 42 deletions(-) diff --git a/packages/cli-module-build/src/lib/runner/runBackend.test.ts b/packages/cli-module-build/src/lib/runner/runBackend.test.ts index 2b97ae352c..b4193cad52 100644 --- a/packages/cli-module-build/src/lib/runner/runBackend.test.ts +++ b/packages/cli-module-build/src/lib/runner/runBackend.test.ts @@ -49,6 +49,24 @@ jest.mock('ctrlc-windows', () => ({ ctrlc: jest.fn(), })); +jest.mock('@backstage/config-loader', () => ({ + ConfigSources: { + default: () => ({ + readConfigData: async function* readConfigData() { + yield { configs: [] }; + }, + }), + }, +})); + +jest.mock('@backstage/config', () => ({ + ConfigReader: { + fromConfigs: () => ({ + getOptionalString: () => undefined, + }), + }, +})); + describe('runBackend', () => { let originalEnv: NodeJS.ProcessEnv; let originalPlatform: string; @@ -82,92 +100,73 @@ describe('runBackend', () => { }); describe('--no-node-snapshot argument handling', () => { - it('should pass --no-node-snapshot when NODE_OPTIONS is not set', () => { + it('should pass --no-node-snapshot when NODE_OPTIONS is not set', async () => { delete process.env.NODE_OPTIONS; - runBackend({ - entry: 'src/index', - }); + runBackend({ entry: 'src/index' }); - // Fast-forward past the debounce delay (100ms) - jest.advanceTimersByTime(100); + await jest.advanceTimersByTimeAsync(100); expect(mockSpawn).toHaveBeenCalled(); const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; expect(spawnArgs).toContain('--no-node-snapshot'); }); - it('should pass --no-node-snapshot when NODE_OPTIONS exists without --node-snapshot', () => { + it('should pass --no-node-snapshot when NODE_OPTIONS exists without --node-snapshot', async () => { process.env.NODE_OPTIONS = '--max-old-space-size=4096'; - runBackend({ - entry: 'src/index', - }); + runBackend({ entry: 'src/index' }); - // Fast-forward past the debounce delay (100ms) - jest.advanceTimersByTime(100); + await jest.advanceTimersByTimeAsync(100); expect(mockSpawn).toHaveBeenCalled(); const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; expect(spawnArgs).toContain('--no-node-snapshot'); }); - it('should not pass --no-node-snapshot when --node-snapshot already exists in NODE_OPTIONS', () => { + it('should not pass --no-node-snapshot when --node-snapshot already exists in NODE_OPTIONS', async () => { process.env.NODE_OPTIONS = '--node-snapshot --max-old-space-size=4096'; - runBackend({ - entry: 'src/index', - }); + runBackend({ entry: 'src/index' }); - // Fast-forward past the debounce delay (100ms) - jest.advanceTimersByTime(100); + await jest.advanceTimersByTimeAsync(100); expect(mockSpawn).toHaveBeenCalled(); const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; expect(spawnArgs).not.toContain('--no-node-snapshot'); }); - it('should not pass --no-node-snapshot when --node-snapshot exists in the middle of NODE_OPTIONS', () => { + it('should not pass --no-node-snapshot when --node-snapshot exists in the middle of NODE_OPTIONS', async () => { process.env.NODE_OPTIONS = '--max-old-space-size=4096 --node-snapshot --inspect'; - runBackend({ - entry: 'src/index', - }); + runBackend({ entry: 'src/index' }); - // Fast-forward past the debounce delay (100ms) - jest.advanceTimersByTime(100); + await jest.advanceTimersByTimeAsync(100); expect(mockSpawn).toHaveBeenCalled(); const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; expect(spawnArgs).not.toContain('--no-node-snapshot'); }); - it('should pass --no-node-snapshot even with trailing spaces in NODE_OPTIONS', () => { + it('should pass --no-node-snapshot even with trailing spaces in NODE_OPTIONS', async () => { process.env.NODE_OPTIONS = '--max-old-space-size=4096 '; - runBackend({ - entry: 'src/index', - }); + runBackend({ entry: 'src/index' }); - // Fast-forward past the debounce delay (100ms) - jest.advanceTimersByTime(100); + await jest.advanceTimersByTimeAsync(100); expect(mockSpawn).toHaveBeenCalled(); const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; expect(spawnArgs).toContain('--no-node-snapshot'); }); - it('should pass --no-node-snapshot alongside other option args like --inspect', () => { + it('should pass --no-node-snapshot alongside other option args like --inspect', async () => { delete process.env.NODE_OPTIONS; - runBackend({ - entry: 'src/index', - inspectEnabled: true, - }); + runBackend({ entry: 'src/index', inspectEnabled: true }); - // Fast-forward past the debounce delay (100ms) - jest.advanceTimersByTime(100); + await jest.advanceTimersByTimeAsync(100); expect(mockSpawn).toHaveBeenCalled(); const spawnArgs = mockSpawn.mock.calls[0][1] as string[]; diff --git a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts index 85de29cda2..607a371003 100644 --- a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts +++ b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts @@ -16,7 +16,7 @@ import os from 'node:os'; import fs from 'fs-extra'; -import { resolve as resolvePath } from 'path'; +import { resolve as resolvePath } from 'node:path'; import { getPortPromise } from 'portfinder'; export async function startEmbeddedDb() { @@ -42,8 +42,8 @@ export async function startEmbeddedDb() { password, port, persistent: false, - onError(_messageOrError) {}, - onLog(_message) {}, + onError(_messageOrError: unknown) {}, + onLog(_message: unknown) {}, }); // Create the cluster config files diff --git a/packages/cli-module-build/src/types.d.ts b/packages/cli-module-build/src/types.d.ts index 2e30114f10..18f1c9c11a 100644 --- a/packages/cli-module-build/src/types.d.ts +++ b/packages/cli-module-build/src/types.d.ts @@ -14,7 +14,21 @@ * limitations under the License. */ -// It's missing a types entry point, but has types in dist declare module 'embedded-postgres' { - export { default } from 'embedded-postgres/dist/index'; + export interface EmbeddedPostgresOptions { + databaseDir: string; + user: string; + password: string; + port: number; + persistent: boolean; + onError?: (messageOrError: unknown) => void; + onLog?: (message: unknown) => void; + } + + export default class EmbeddedPostgres { + constructor(options: EmbeddedPostgresOptions); + initialise(): Promise; + start(): Promise; + stop(): Promise; + } } From b43c1f1bcbbc84618dd85a00e938d56124de393e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Apr 2026 19:55:11 +0200 Subject: [PATCH 18/48] cli: simplify config loading using ConfigSources.toConfig Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../src/lib/runner/runBackend.test.ts | 14 +++----------- .../cli-module-build/src/lib/runner/runBackend.ts | 14 +++----------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/packages/cli-module-build/src/lib/runner/runBackend.test.ts b/packages/cli-module-build/src/lib/runner/runBackend.test.ts index b4193cad52..6fd9f965aa 100644 --- a/packages/cli-module-build/src/lib/runner/runBackend.test.ts +++ b/packages/cli-module-build/src/lib/runner/runBackend.test.ts @@ -51,17 +51,9 @@ jest.mock('ctrlc-windows', () => ({ jest.mock('@backstage/config-loader', () => ({ ConfigSources: { - default: () => ({ - readConfigData: async function* readConfigData() { - yield { configs: [] }; - }, - }), - }, -})); - -jest.mock('@backstage/config', () => ({ - ConfigReader: { - fromConfigs: () => ({ + default: () => ({}), + toConfig: async () => ({ + close: jest.fn(), getOptionalString: () => undefined, }), }, diff --git a/packages/cli-module-build/src/lib/runner/runBackend.ts b/packages/cli-module-build/src/lib/runner/runBackend.ts index c0c88e42db..40124d39a0 100644 --- a/packages/cli-module-build/src/lib/runner/runBackend.ts +++ b/packages/cli-module-build/src/lib/runner/runBackend.ts @@ -26,7 +26,6 @@ import { } from 'node:path'; import { targetPaths } from '@backstage/cli-common'; import { ConfigSources } from '@backstage/config-loader'; -import { ConfigReader } from '@backstage/config'; import spawn from 'cross-spawn'; import { startEmbeddedDb } from './startEmbeddedDb'; @@ -229,14 +228,7 @@ async function readDatabaseClient( ]), }); - const abortController = new AbortController(); - for await (const { configs } of source.readConfigData({ - signal: abortController.signal, - })) { - abortController.abort(); - return ConfigReader.fromConfigs(configs).getOptionalString( - 'backend.database.client', - ); - } - return undefined; + const config = await ConfigSources.toConfig(source); + config.close(); + return config.getOptionalString('backend.database.client'); } From a922b3b921b0794531a9822296cf56796313ced0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Apr 2026 21:19:28 +0200 Subject: [PATCH 19/48] cli: fix error message for missing embedded-postgres dependency Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts index 607a371003..09ff206011 100644 --- a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts +++ b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts @@ -23,8 +23,9 @@ export async function startEmbeddedDb() { const { default: EmbeddedPostgres } = await import('embedded-postgres').catch( error => { throw new Error( - `Failed to load peer dependency 'embedded-postgres' for generating SQL reports. ` + - `It must be installed as an explicit dependency in your project. Caused by; ${error}`, + `Failed to load 'embedded-postgres' which is required when using ` + + `'embedded-postgres' as the database client. It must be installed as ` + + `an explicit dependency in your project. Caused by: ${error}`, ); }, ); From 45075dddcdfc6dc64f5eab296ce69f9c22bec267 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Apr 2026 21:20:10 +0200 Subject: [PATCH 20/48] cli: use ForwardedError for embedded-postgres import failure Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../cli-module-build/src/lib/runner/startEmbeddedDb.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts index 09ff206011..a5ab0540c2 100644 --- a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts +++ b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts @@ -18,14 +18,16 @@ import os from 'node:os'; import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { getPortPromise } from 'portfinder'; +import { ForwardedError } from '@backstage/errors'; export async function startEmbeddedDb() { const { default: EmbeddedPostgres } = await import('embedded-postgres').catch( error => { - throw new Error( + throw new ForwardedError( `Failed to load 'embedded-postgres' which is required when using ` + - `'embedded-postgres' as the database client. It must be installed as ` + - `an explicit dependency in your project. Caused by: ${error}`, + `'embedded-postgres' as the database client. It must be installed ` + + `as an explicit dependency in your project`, + error, ); }, ); From 7bf4814173d43a4c3c30632f15bf09fc18e6a08d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Apr 2026 21:22:14 +0200 Subject: [PATCH 21/48] cli: make embedded-postgres an optional peer dependency Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-build/package.json | 12 ++++++++++-- yarn.lock | 9 +++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/cli-module-build/package.json b/packages/cli-module-build/package.json index ce64c5268f..4460174f6b 100644 --- a/packages/cli-module-build/package.json +++ b/packages/cli-module-build/package.json @@ -65,7 +65,6 @@ "cross-spawn": "^7.0.3", "css-loader": "^6.5.1", "ctrlc-windows": "^2.1.0", - "embedded-postgres": "^17.2.0-beta.15", "esbuild-loader": "^4.0.0", "eslint-rspack-plugin": "^4.2.1", "eslint-webpack-plugin": "^4.2.0", @@ -108,6 +107,15 @@ "@types/fs-extra": "^11.0.0", "@types/lodash": "^4.14.151", "@types/npm-packlist": "^3.0.0", - "@types/shell-quote": "^1.7.5" + "@types/shell-quote": "^1.7.5", + "embedded-postgres": "17.2.0-beta.15" + }, + "peerDependencies": { + "embedded-postgres": "^17.2.0-beta.15" + }, + "peerDependenciesMeta": { + "embedded-postgres": { + "optional": true + } } } diff --git a/yarn.lock b/yarn.lock index 35bca1fb75..47eb32332b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2900,7 +2900,7 @@ __metadata: cross-spawn: "npm:^7.0.3" css-loader: "npm:^6.5.1" ctrlc-windows: "npm:^2.1.0" - embedded-postgres: "npm:^17.2.0-beta.15" + embedded-postgres: "npm:17.2.0-beta.15" esbuild-loader: "npm:^4.0.0" eslint-rspack-plugin: "npm:^4.2.1" eslint-webpack-plugin: "npm:^4.2.0" @@ -2936,6 +2936,11 @@ __metadata: webpack-dev-server: "npm:^5.0.0" yml-loader: "npm:^2.1.0" yn: "npm:^4.0.0" + peerDependencies: + embedded-postgres: ^17.2.0-beta.15 + peerDependenciesMeta: + embedded-postgres: + optional: true bin: cli-module-build: bin/backstage-cli-module-build languageName: unknown @@ -29424,7 +29429,7 @@ __metadata: languageName: node linkType: hard -"embedded-postgres@npm:^17.2.0-beta.15": +"embedded-postgres@npm:17.2.0-beta.15": version: 17.2.0-beta.15 resolution: "embedded-postgres@npm:17.2.0-beta.15" dependencies: From d80e59ce9e04e3059d0387ade20cc893ee155b11 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Apr 2026 21:26:28 +0200 Subject: [PATCH 22/48] cli: bump embedded-postgres to 18.3.0-beta.16 Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-build/package.json | 4 +- yarn.lock | 76 +++++++++++++------------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/packages/cli-module-build/package.json b/packages/cli-module-build/package.json index 4460174f6b..55d3a2558e 100644 --- a/packages/cli-module-build/package.json +++ b/packages/cli-module-build/package.json @@ -108,10 +108,10 @@ "@types/lodash": "^4.14.151", "@types/npm-packlist": "^3.0.0", "@types/shell-quote": "^1.7.5", - "embedded-postgres": "17.2.0-beta.15" + "embedded-postgres": "18.3.0-beta.16" }, "peerDependencies": { - "embedded-postgres": "^17.2.0-beta.15" + "embedded-postgres": "^18.3.0-beta.16" }, "peerDependenciesMeta": { "embedded-postgres": { diff --git a/yarn.lock b/yarn.lock index 47eb32332b..3a7dde3f7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2900,7 +2900,7 @@ __metadata: cross-spawn: "npm:^7.0.3" css-loader: "npm:^6.5.1" ctrlc-windows: "npm:^2.1.0" - embedded-postgres: "npm:17.2.0-beta.15" + embedded-postgres: "npm:18.3.0-beta.16" esbuild-loader: "npm:^4.0.0" eslint-rspack-plugin: "npm:^4.2.1" eslint-webpack-plugin: "npm:^4.2.0" @@ -2937,7 +2937,7 @@ __metadata: yml-loader: "npm:^2.1.0" yn: "npm:^4.0.0" peerDependencies: - embedded-postgres: ^17.2.0-beta.15 + embedded-postgres: ^18.3.0-beta.16 peerDependenciesMeta: embedded-postgres: optional: true @@ -8564,58 +8564,58 @@ __metadata: languageName: node linkType: hard -"@embedded-postgres/darwin-arm64@npm:^17.2.0-beta.15": - version: 17.2.0-beta.15 - resolution: "@embedded-postgres/darwin-arm64@npm:17.2.0-beta.15" +"@embedded-postgres/darwin-arm64@npm:^18.3.0-beta.16": + version: 18.3.0-beta.16 + resolution: "@embedded-postgres/darwin-arm64@npm:18.3.0-beta.16" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@embedded-postgres/darwin-x64@npm:^17.2.0-beta.15": - version: 17.2.0-beta.15 - resolution: "@embedded-postgres/darwin-x64@npm:17.2.0-beta.15" +"@embedded-postgres/darwin-x64@npm:^18.3.0-beta.16": + version: 18.3.0-beta.16 + resolution: "@embedded-postgres/darwin-x64@npm:18.3.0-beta.16" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@embedded-postgres/linux-arm64@npm:^17.2.0-beta.15": - version: 17.2.0-beta.15 - resolution: "@embedded-postgres/linux-arm64@npm:17.2.0-beta.15" +"@embedded-postgres/linux-arm64@npm:^18.3.0-beta.16": + version: 18.3.0-beta.16 + resolution: "@embedded-postgres/linux-arm64@npm:18.3.0-beta.16" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@embedded-postgres/linux-arm@npm:^17.2.0-beta.15": - version: 17.2.0-beta.15 - resolution: "@embedded-postgres/linux-arm@npm:17.2.0-beta.15" +"@embedded-postgres/linux-arm@npm:^18.3.0-beta.16": + version: 18.3.0-beta.16 + resolution: "@embedded-postgres/linux-arm@npm:18.3.0-beta.16" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@embedded-postgres/linux-ia32@npm:^17.2.0-beta.15": - version: 17.2.0-beta.15 - resolution: "@embedded-postgres/linux-ia32@npm:17.2.0-beta.15" +"@embedded-postgres/linux-ia32@npm:^18.3.0-beta.16": + version: 18.3.0-beta.16 + resolution: "@embedded-postgres/linux-ia32@npm:18.3.0-beta.16" conditions: os=linux & cpu=ia32 languageName: node linkType: hard -"@embedded-postgres/linux-ppc64@npm:^17.2.0-beta.15": - version: 17.2.0-beta.15 - resolution: "@embedded-postgres/linux-ppc64@npm:17.2.0-beta.15" +"@embedded-postgres/linux-ppc64@npm:^18.3.0-beta.16": + version: 18.3.0-beta.16 + resolution: "@embedded-postgres/linux-ppc64@npm:18.3.0-beta.16" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard -"@embedded-postgres/linux-x64@npm:^17.2.0-beta.15": - version: 17.2.0-beta.15 - resolution: "@embedded-postgres/linux-x64@npm:17.2.0-beta.15" +"@embedded-postgres/linux-x64@npm:^18.3.0-beta.16": + version: 18.3.0-beta.16 + resolution: "@embedded-postgres/linux-x64@npm:18.3.0-beta.16" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@embedded-postgres/windows-x64@npm:^17.2.0-beta.15": - version: 17.2.0-beta.15 - resolution: "@embedded-postgres/windows-x64@npm:17.2.0-beta.15" +"@embedded-postgres/windows-x64@npm:^18.3.0-beta.16": + version: 18.3.0-beta.16 + resolution: "@embedded-postgres/windows-x64@npm:18.3.0-beta.16" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -29429,18 +29429,18 @@ __metadata: languageName: node linkType: hard -"embedded-postgres@npm:17.2.0-beta.15": - version: 17.2.0-beta.15 - resolution: "embedded-postgres@npm:17.2.0-beta.15" +"embedded-postgres@npm:18.3.0-beta.16": + version: 18.3.0-beta.16 + resolution: "embedded-postgres@npm:18.3.0-beta.16" dependencies: - "@embedded-postgres/darwin-arm64": "npm:^17.2.0-beta.15" - "@embedded-postgres/darwin-x64": "npm:^17.2.0-beta.15" - "@embedded-postgres/linux-arm": "npm:^17.2.0-beta.15" - "@embedded-postgres/linux-arm64": "npm:^17.2.0-beta.15" - "@embedded-postgres/linux-ia32": "npm:^17.2.0-beta.15" - "@embedded-postgres/linux-ppc64": "npm:^17.2.0-beta.15" - "@embedded-postgres/linux-x64": "npm:^17.2.0-beta.15" - "@embedded-postgres/windows-x64": "npm:^17.2.0-beta.15" + "@embedded-postgres/darwin-arm64": "npm:^18.3.0-beta.16" + "@embedded-postgres/darwin-x64": "npm:^18.3.0-beta.16" + "@embedded-postgres/linux-arm": "npm:^18.3.0-beta.16" + "@embedded-postgres/linux-arm64": "npm:^18.3.0-beta.16" + "@embedded-postgres/linux-ia32": "npm:^18.3.0-beta.16" + "@embedded-postgres/linux-ppc64": "npm:^18.3.0-beta.16" + "@embedded-postgres/linux-x64": "npm:^18.3.0-beta.16" + "@embedded-postgres/windows-x64": "npm:^18.3.0-beta.16" async-exit-hook: "npm:^2.0.1" pg: "npm:^8.7.3" dependenciesMeta: @@ -29460,7 +29460,7 @@ __metadata: optional: true "@embedded-postgres/windows-x64": optional: true - checksum: 10/bba9ba0f584bbfba854c60932eafc62c4338e8231793f1c7c3cc4e066895877c606c68b7f5b07ec005de4c7506e81f70bf1cd27d74bf5719c7be334272294ce4 + checksum: 10/13ebdec978559d8d5496df521ec6d6a717a6a3e234a7daa1d3d85e8d050626cde927e5d1d382c70eec219afa721d3c28c26a39023de0a5919feb535470860b47 languageName: node linkType: hard From ae1cdd9e9f187936907a20d3e4364d808340d018 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Apr 2026 21:30:27 +0200 Subject: [PATCH 23/48] cli: remove custom embedded-postgres type declarations The 18.x version ships its own .d.ts files that TypeScript resolves correctly, so the custom module declaration is no longer needed. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../src/lib/runner/startEmbeddedDb.ts | 4 +-- packages/cli-module-build/src/types.d.ts | 34 ------------------- 2 files changed, 2 insertions(+), 36 deletions(-) delete mode 100644 packages/cli-module-build/src/types.d.ts diff --git a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts index a5ab0540c2..4e02bf5819 100644 --- a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts +++ b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts @@ -45,8 +45,8 @@ export async function startEmbeddedDb() { password, port, persistent: false, - onError(_messageOrError: unknown) {}, - onLog(_message: unknown) {}, + onError() {}, + onLog() {}, }); // Create the cluster config files diff --git a/packages/cli-module-build/src/types.d.ts b/packages/cli-module-build/src/types.d.ts deleted file mode 100644 index 18f1c9c11a..0000000000 --- a/packages/cli-module-build/src/types.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -declare module 'embedded-postgres' { - export interface EmbeddedPostgresOptions { - databaseDir: string; - user: string; - password: string; - port: number; - persistent: boolean; - onError?: (messageOrError: unknown) => void; - onLog?: (message: unknown) => void; - } - - export default class EmbeddedPostgres { - constructor(options: EmbeddedPostgresOptions); - initialise(): Promise; - start(): Promise; - stop(): Promise; - } -} From 1f88d2624b4a032adbc787bc47f887bf5a69990f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Apr 2026 21:36:04 +0200 Subject: [PATCH 24/48] cli: address review feedback for embedded-postgres - Close embedded DB on shutdown to avoid leaking the Postgres process and temp directory - Use fs.remove instead of deprecated fs.rmdir with recursive option - Guard against absolute config paths in readDatabaseClient - Forward embedded-postgres error logs to console.error Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-module-build/src/lib/runner/runBackend.ts | 9 ++++++--- .../cli-module-build/src/lib/runner/startEmbeddedDb.ts | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/cli-module-build/src/lib/runner/runBackend.ts b/packages/cli-module-build/src/lib/runner/runBackend.ts index 40124d39a0..272eddd360 100644 --- a/packages/cli-module-build/src/lib/runner/runBackend.ts +++ b/packages/cli-module-build/src/lib/runner/runBackend.ts @@ -66,12 +66,14 @@ export async function runBackend(options: RunBackendOptions) { const extraEnv: Record = {}; + let embeddedDb: Awaited> | undefined; + const dbClient = await readDatabaseClient(options.configPaths); if (dbClient === 'embedded-postgres') { - const db = await startEmbeddedDb(); + embeddedDb = await startEmbeddedDb(); extraEnv.APP_CONFIG_backend_database = JSON.stringify({ client: 'pg', - connection: db.connection, + connection: embeddedDb.connection, }); } @@ -205,6 +207,7 @@ export async function runBackend(options: RunBackendOptions) { }); } + await embeddedDb?.close(); resolveExitPromise(); } @@ -224,7 +227,7 @@ async function readDatabaseClient( allowMissingDefaultConfig: true, argv: (configPaths ?? []).flatMap(p => [ '--config', - resolvePath(rootDir, p), + isAbsolutePath(p) ? p : resolvePath(rootDir, p), ]), }); diff --git a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts index 4e02bf5819..504c9b219b 100644 --- a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts +++ b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts @@ -45,7 +45,9 @@ export async function startEmbeddedDb() { password, port, persistent: false, - onError() {}, + onError(messageOrError) { + console.error(`[embedded-postgres]`, messageOrError); + }, onLog() {}, }); @@ -64,7 +66,7 @@ export async function startEmbeddedDb() { }, async close() { await pg.stop(); - await fs.rmdir(tmpDir, { recursive: true, maxRetries: 3 }); + await fs.remove(tmpDir); }, }; } From 7e7e7631637d639a127228f0a18a3bdcb9e83145 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Apr 2026 21:51:37 +0200 Subject: [PATCH 25/48] cli: add tests for embedded-postgres config detection Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../src/lib/runner/runBackend.test.ts | 77 ++++++++++++++++++- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/packages/cli-module-build/src/lib/runner/runBackend.test.ts b/packages/cli-module-build/src/lib/runner/runBackend.test.ts index 6fd9f965aa..b6ac8fd54d 100644 --- a/packages/cli-module-build/src/lib/runner/runBackend.test.ts +++ b/packages/cli-module-build/src/lib/runner/runBackend.test.ts @@ -49,16 +49,21 @@ jest.mock('ctrlc-windows', () => ({ ctrlc: jest.fn(), })); +const mockToConfig = jest.fn(); + jest.mock('@backstage/config-loader', () => ({ ConfigSources: { default: () => ({}), - toConfig: async () => ({ - close: jest.fn(), - getOptionalString: () => undefined, - }), + toConfig: (...args: any[]) => mockToConfig(...args), }, })); +const mockStartEmbeddedDb = jest.fn(); + +jest.mock('./startEmbeddedDb', () => ({ + startEmbeddedDb: (...args: any[]) => mockStartEmbeddedDb(...args), +})); + describe('runBackend', () => { let originalEnv: NodeJS.ProcessEnv; let originalPlatform: string; @@ -78,6 +83,12 @@ describe('runBackend', () => { // Mock process.once to prevent actual signal handling jest.spyOn(process, 'once').mockReturnValue(process); + + mockToConfig.mockResolvedValue({ + close: jest.fn(), + getOptionalString: () => undefined, + }); + mockStartEmbeddedDb.mockReset(); }); afterEach(() => { @@ -166,4 +177,62 @@ describe('runBackend', () => { expect(spawnArgs).toContain('--inspect'); }); }); + + describe('embedded-postgres support', () => { + it('should start embedded DB and inject config when database client is embedded-postgres', async () => { + mockToConfig.mockResolvedValue({ + close: jest.fn(), + getOptionalString: (key: string) => + key === 'backend.database.client' ? 'embedded-postgres' : undefined, + }); + mockStartEmbeddedDb.mockResolvedValue({ + connection: { + host: 'localhost', + user: 'postgres', + password: 'password', + port: 5555, + }, + close: jest.fn(), + }); + + runBackend({ entry: 'src/index' }); + await jest.advanceTimersByTimeAsync(100); + + expect(mockStartEmbeddedDb).toHaveBeenCalled(); + expect(mockSpawn).toHaveBeenCalled(); + const spawnEnv = mockSpawn.mock.calls[0][2]?.env as Record< + string, + string + >; + const injected = JSON.parse(spawnEnv.APP_CONFIG_backend_database); + expect(injected).toEqual({ + client: 'pg', + connection: { + host: 'localhost', + user: 'postgres', + password: 'password', + port: 5555, + }, + }); + }); + + it('should not start embedded DB for other database clients', async () => { + mockToConfig.mockResolvedValue({ + close: jest.fn(), + getOptionalString: (key: string) => + key === 'backend.database.client' ? 'better-sqlite3' : undefined, + }); + + runBackend({ entry: 'src/index' }); + await jest.advanceTimersByTimeAsync(100); + + expect(mockStartEmbeddedDb).not.toHaveBeenCalled(); + expect(mockSpawn).toHaveBeenCalled(); + const spawnEnv = mockSpawn.mock.calls[0][2]?.env as Record< + string, + string + >; + expect(spawnEnv.APP_CONFIG_backend_database).toBeUndefined(); + }); + }); }); From 77d17a5110a664d192db3b62e5d6ae4a1227995f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Apr 2026 22:13:33 +0200 Subject: [PATCH 26/48] cli: add experimental warning for embedded-postgres Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../cli-module-build/src/lib/runner/startEmbeddedDb.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts index 504c9b219b..94b3d01b19 100644 --- a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts +++ b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts @@ -19,8 +19,15 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'node:path'; import { getPortPromise } from 'portfinder'; import { ForwardedError } from '@backstage/errors'; +import chalk from 'chalk'; export async function startEmbeddedDb() { + console.warn( + chalk.yellow( + 'WARNING: Using embedded-postgres for local development is experimental and subject to change', + ), + ); + const { default: EmbeddedPostgres } = await import('embedded-postgres').catch( error => { throw new ForwardedError( From 13c5f97337d3f1f3d184329e69bf01ddcaa20b5e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Apr 2026 00:31:09 +0200 Subject: [PATCH 27/48] cli: clean up stale embedded-postgres temp directories on startup Uses a PID file to track which process owns each temp directory, so concurrent instances from different projects are not affected. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../src/lib/runner/startEmbeddedDb.ts | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts index 94b3d01b19..d667a43ae8 100644 --- a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts +++ b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts @@ -21,6 +21,37 @@ import { getPortPromise } from 'portfinder'; import { ForwardedError } from '@backstage/errors'; import chalk from 'chalk'; +const TEMP_DIR_PREFIX = 'backstage-dev-db-'; +const PID_FILE = 'backstage.pid'; + +function isProcessAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +async function cleanStaleDatabases() { + const tmpBase = os.tmpdir(); + const entries = (await fs.readdir(tmpBase)).filter(d => + d.startsWith(TEMP_DIR_PREFIX), + ); + await Promise.all( + entries.map(async d => { + const dir = resolvePath(tmpBase, d); + const raw = await fs + .readFile(resolvePath(dir, PID_FILE), 'utf8') + .catch(() => undefined); + const pid = raw ? Number(raw.trim()) : NaN; + if (!pid || !isProcessAlive(pid)) { + await fs.remove(dir); + } + }), + ); +} + export async function startEmbeddedDb() { console.warn( chalk.yellow( @@ -39,13 +70,16 @@ export async function startEmbeddedDb() { }, ); + await cleanStaleDatabases(); + const host = 'localhost'; const user = 'postgres'; const password = 'password'; const port = await getPortPromise(); - const tmpDir = await fs.mkdtemp( - resolvePath(os.tmpdir(), 'backstage-dev-db-'), - ); + const tmpDir = await fs.mkdtemp(resolvePath(os.tmpdir(), TEMP_DIR_PREFIX)); + + await fs.writeFile(resolvePath(tmpDir, PID_FILE), String(process.pid)); + const pg = new EmbeddedPostgres({ databaseDir: tmpDir, user, From c0908fe5b5d8e184fe6e6c5e0d31406deea963bb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Apr 2026 00:38:35 +0200 Subject: [PATCH 28/48] cli: add error handling for config close and startup failure cleanup Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../cli-module-build/src/lib/runner/runBackend.ts | 7 +++++-- .../src/lib/runner/startEmbeddedDb.ts | 13 ++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/cli-module-build/src/lib/runner/runBackend.ts b/packages/cli-module-build/src/lib/runner/runBackend.ts index 272eddd360..464ff10cec 100644 --- a/packages/cli-module-build/src/lib/runner/runBackend.ts +++ b/packages/cli-module-build/src/lib/runner/runBackend.ts @@ -232,6 +232,9 @@ async function readDatabaseClient( }); const config = await ConfigSources.toConfig(source); - config.close(); - return config.getOptionalString('backend.database.client'); + try { + return config.getOptionalString('backend.database.client'); + } finally { + config.close(); + } } diff --git a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts index d667a43ae8..c94a5a7f46 100644 --- a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts +++ b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts @@ -92,11 +92,14 @@ export async function startEmbeddedDb() { onLog() {}, }); - // Create the cluster config files - await pg.initialise(); - - // Start the server - await pg.start(); + try { + await pg.initialise(); + await pg.start(); + } catch (error) { + await pg.stop().catch(() => {}); + await fs.remove(tmpDir).catch(() => {}); + throw error; + } return { connection: { From ee6a133dd74c8c0ecd7c498b96d88920bbb7d4ef Mon Sep 17 00:00:00 2001 From: wpessers Date: Thu, 2 Apr 2026 14:46:53 +0200 Subject: [PATCH 29/48] update test naming Signed-off-by: wpessers --- plugins/catalog-backend-module-github/src/lib/github.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-backend-module-github/src/lib/github.test.ts b/plugins/catalog-backend-module-github/src/lib/github.test.ts index 81dc77ab5f..2b9c35be85 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.test.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.test.ts @@ -1010,7 +1010,7 @@ describe('github', () => { baseUrl, logger, }); - it('should return a graphql client with throttling', async () => { + it('should return a graphql client with throttling and retry', async () => { expect(client).toBeDefined(); expect(Octokit.plugin).toHaveBeenCalledWith(throttling, retry); }); From d5899c2362a94f27342148ad41fd2ebe8eccf9cf Mon Sep 17 00:00:00 2001 From: Riley Martine Date: Fri, 3 Apr 2026 06:34:45 -0600 Subject: [PATCH 30/48] Allow passing showArrowHeads to entity-card:catalog-graph/relations and /catalog-graph page (#33706) * Allow passing showArrowHeads to entity-card:catalog-graph/relations and /catalog-graph page Signed-off-by: Riley Martine * Update .changeset/nine-signs-end.md Signed-off-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> --------- Signed-off-by: Riley Martine Signed-off-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Co-authored-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> --- .changeset/nine-signs-end.md | 5 +++++ plugins/catalog-graph/README-alpha.md | 2 ++ plugins/catalog-graph/report-alpha.api.md | 4 ++++ plugins/catalog-graph/src/alpha.tsx | 2 ++ .../src/components/CatalogGraphCard/CatalogGraphCard.tsx | 2 ++ .../src/components/CatalogGraphPage/CatalogGraphPage.tsx | 3 ++- 6 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 .changeset/nine-signs-end.md diff --git a/.changeset/nine-signs-end.md b/.changeset/nine-signs-end.md new file mode 100644 index 0000000000..544b0bb231 --- /dev/null +++ b/.changeset/nine-signs-end.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-graph': patch +--- + +Support configuring `showArrowHeads` on `page:catalog-graph` and `entity-card:catalog-graph/relations`. diff --git a/plugins/catalog-graph/README-alpha.md b/plugins/catalog-graph/README-alpha.md index 90481caca6..b7cd5f65be 100644 --- a/plugins/catalog-graph/README-alpha.md +++ b/plugins/catalog-graph/README-alpha.md @@ -165,6 +165,7 @@ See below the complete list of available configs: | `maxDepth` | A maximum number of levels of relations to display in the graph. | `number` | yes | `1` | | `unidirectional` | Shows only relations that are from the source to the target entity. | `boolean` | yes | `true` | | `mergeRelations` | Merge the relations line into a single one. | `boolean` | yes | `true` | +| `showArrowHeads` | Show arrowheads on the relation lines | `boolean` | yes | `false` | | `direction` | Render direction of the graph. | `TB` \| `BT` \| `LR` \| `RL` | yes | `'LR'` | | `relationPairs` | A list of [pairs of entity relations](https://backstage.io/docs/features/software-catalog/well-known-relations#relations), used to define which relations are merged together and which the primary relation is. | `[string[], string[]]` | yes | Show all entity [relations](https://backstage.io/docs/features/software-catalog/well-known-relations#relations). | | `zoom` | Controls zoom behavior of graph. | `enabled` \| `disabled` \| `enable-on-click` | yes | `'enabled'` | @@ -265,6 +266,7 @@ See below the complete list of available configs: | `maxDepth` | A maximum number of levels of relations to display in the graph. | `number` | yes | `1` | | `unidirectional` | Shows only relations that are from the source to the target entity. | `boolean` | yes | `true` | | `mergeRelations` | Merge the relations line into a single one. | `boolean` | yes | `true` | +| `showArrowHeads` | Show arrowheads on the relation lines | `boolean` | yes | `false` | | `direction` | Render direction of the graph. | `TB` \| `BT` \| `LR` \| `RL` | yes | `'LR'` | | `relationPairs` | A list of [pairs of entity relations](https://backstage.io/docs/features/software-catalog/well-known-relations#relations), used to define which relations are merged together and which the primary relation is. | `[string[], string[]]` | yes | Show all entity [relations](https://backstage.io/docs/features/software-catalog/well-known-relations#relations). | | `zoom` | Controls zoom behavior of graph. | `enabled` \| `disabled` \| `enable-on-click` | yes | `'enabled'` | diff --git a/plugins/catalog-graph/report-alpha.api.md b/plugins/catalog-graph/report-alpha.api.md index 8bfbe17e9b..44a9bbfb61 100644 --- a/plugins/catalog-graph/report-alpha.api.md +++ b/plugins/catalog-graph/report-alpha.api.md @@ -88,6 +88,7 @@ const _default: OverridableFrontendPlugin< maxDepth: number | undefined; unidirectional: boolean | undefined; mergeRelations: boolean | undefined; + showArrowHeads: boolean | undefined; direction: 'TB' | 'BT' | 'LR' | 'RL' | undefined; relationPairs: [string, string][] | undefined; zoom: 'disabled' | 'enabled' | 'enable-on-click' | undefined; @@ -103,6 +104,7 @@ const _default: OverridableFrontendPlugin< direction?: 'TB' | 'BT' | 'LR' | 'RL' | undefined; zoom?: 'disabled' | 'enabled' | 'enable-on-click' | undefined; title?: string | undefined; + showArrowHeads?: boolean | undefined; relations?: string[] | undefined; maxDepth?: number | undefined; kinds?: string[] | undefined; @@ -152,6 +154,7 @@ const _default: OverridableFrontendPlugin< maxDepth: number | undefined; unidirectional: boolean | undefined; mergeRelations: boolean | undefined; + showArrowHeads: boolean | undefined; direction: 'TB' | 'BT' | 'LR' | 'RL' | undefined; showFilters: boolean | undefined; curve: 'curveStepBefore' | 'curveMonotoneX' | undefined; @@ -166,6 +169,7 @@ const _default: OverridableFrontendPlugin< curve?: 'curveStepBefore' | 'curveMonotoneX' | undefined; direction?: 'TB' | 'BT' | 'LR' | 'RL' | undefined; zoom?: 'disabled' | 'enabled' | 'enable-on-click' | undefined; + showArrowHeads?: boolean | undefined; relations?: string[] | undefined; maxDepth?: number | undefined; rootEntityRefs?: string[] | undefined; diff --git a/plugins/catalog-graph/src/alpha.tsx b/plugins/catalog-graph/src/alpha.tsx index f1b4dbc6d0..9d77c35020 100644 --- a/plugins/catalog-graph/src/alpha.tsx +++ b/plugins/catalog-graph/src/alpha.tsx @@ -37,6 +37,7 @@ const CatalogGraphEntityCard = EntityCardBlueprint.makeWithOverrides({ maxDepth: z => z.number().optional(), unidirectional: z => z.boolean().optional(), mergeRelations: z => z.boolean().optional(), + showArrowHeads: z => z.boolean().optional(), direction: z => z.nativeEnum(Direction).optional(), relationPairs: z => z.array(z.tuple([z.string(), z.string()])).optional(), zoom: z => z.enum(['enabled', 'disabled', 'enable-on-click']).optional(), @@ -66,6 +67,7 @@ const CatalogGraphPage = PageBlueprint.makeWithOverrides({ maxDepth: z => z.number().optional(), unidirectional: z => z.boolean().optional(), mergeRelations: z => z.boolean().optional(), + showArrowHeads: z => z.boolean().optional(), direction: z => z.nativeEnum(Direction).optional(), showFilters: z => z.boolean().optional(), curve: z => z.enum(['curveStepBefore', 'curveMonotoneX']).optional(), diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx index 20b4074f79..10ba73e2b6 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx @@ -68,6 +68,7 @@ export const CatalogGraphCard = ( maxDepth = 1, unidirectional = true, mergeRelations = true, + showArrowHeads, direction = Direction.LEFT_RIGHT, kinds, relations, @@ -147,6 +148,7 @@ export const CatalogGraphCard = ( relationPairs={relationPairs} entityFilter={entityFilter} zoom={zoom} + showArrowHeads={showArrowHeads} /> ); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx index 7e6420da1f..93bdc94860 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx @@ -131,7 +131,7 @@ export const CatalogGraphPage = ( }; } & Partial, ) => { - const { relationPairs, initialState, entityFilter } = props; + const { relationPairs, initialState, entityFilter, showArrowHeads } = props; const { t } = useTranslationRef(catalogGraphTranslationRef); const navigate = useNavigate(); const classes = useStyles(); @@ -260,6 +260,7 @@ export const CatalogGraphPage = ( } mergeRelations={mergeRelations} unidirectional={unidirectional} + showArrowHeads={showArrowHeads} onNodeClick={onNodeClick} direction={direction} relationPairs={relationPairs} From 308c672680503ffa04ad1cd337a229a4f3d48415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 3 Apr 2026 14:48:26 +0200 Subject: [PATCH 31/48] feat(backend-defaults): warn on localhost or invalid backend.baseUrl in HostDiscovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds startup warnings to HostDiscovery.fromConfig when backend.baseUrl is set to a localhost address in a production environment, or when the value is not a valid URL at all. Signed-off-by: Fredrik Adelöw Made-with: Cursor --- .changeset/host-discovery-baseurl-warnings.md | 5 ++ .../discovery/HostDiscovery.test.ts | 46 +++++++++++++++++++ .../entrypoints/discovery/HostDiscovery.ts | 21 +++++++++ 3 files changed, 72 insertions(+) create mode 100644 .changeset/host-discovery-baseurl-warnings.md diff --git a/.changeset/host-discovery-baseurl-warnings.md b/.changeset/host-discovery-baseurl-warnings.md new file mode 100644 index 0000000000..658b24bf0c --- /dev/null +++ b/.changeset/host-discovery-baseurl-warnings.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +`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. diff --git a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts index 1eb2d7d42b..cdfe8a79e1 100644 --- a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts +++ b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts @@ -413,6 +413,52 @@ describe('HostDiscovery', () => { ); }); + describe('backend.baseUrl warnings', () => { + const env = process.env as Record; + const originalNodeEnv = env.NODE_ENV; + + afterEach(() => { + env.NODE_ENV = originalNodeEnv; + }); + + 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 (http://localhost:7007) but 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..23cced476e 100644 --- a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts +++ b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts @@ -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 (${baseUrl}) but 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); From 57543abb7c876c66a632e81e2acf5b59888e1099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 3 Apr 2026 14:53:43 +0200 Subject: [PATCH 32/48] Update packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Fredrik Adelöw Signed-off-by: Fredrik Adelöw Made-with: Cursor Signed-off-by: Fredrik Adelöw Made-with: Cursor --- .../src/entrypoints/discovery/HostDiscovery.test.ts | 8 ++++++-- .../src/entrypoints/discovery/HostDiscovery.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts index cdfe8a79e1..85cbaaa73b 100644 --- a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts +++ b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.test.ts @@ -418,7 +418,11 @@ describe('HostDiscovery', () => { const originalNodeEnv = env.NODE_ENV; afterEach(() => { - env.NODE_ENV = originalNodeEnv; + 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', () => { @@ -436,7 +440,7 @@ describe('HostDiscovery', () => { ); expect(logger.warn).toHaveBeenCalledWith( - `backend.baseUrl is set to a localhost URL (http://localhost:7007) but 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.`, + `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.`, ); }); diff --git a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts index 23cced476e..7e25ba66c4 100644 --- a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts +++ b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts @@ -164,7 +164,7 @@ export class HostDiscovery implements DiscoveryService { hostname === '::'; if (isLocalhost && process.env.NODE_ENV === 'production') { options?.logger?.warn( - `backend.baseUrl is set to a localhost URL (${baseUrl}) but 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.`, + `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 { From a4dc401ac3f340de7ebc2b036c34785d1a2049d4 Mon Sep 17 00:00:00 2001 From: Rajib Quayum Date: Fri, 3 Apr 2026 09:04:59 -0400 Subject: [PATCH 33/48] chore: revert all previous changes Signed-off-by: Rajib Quayum --- .changeset/vast-jeans-boil.md | 22 ---------- packages/app-legacy/src/index.tsx | 1 - packages/app/src/index.tsx | 1 - .../default-app/packages/app/src/index.tsx | 1 - .../next-app/packages/app/src/index.tsx | 1 - packages/theme/package.json | 24 ++--------- .../theme/report-MuiClassNameSetup.api.md | 7 ---- .../theme/src/unified/MuiClassNameSetup.ts | 41 ------------------- .../src/unified/UnifiedThemeProvider.tsx | 11 ++++- 9 files changed, 14 insertions(+), 95 deletions(-) delete mode 100644 .changeset/vast-jeans-boil.md delete mode 100644 packages/theme/report-MuiClassNameSetup.api.md delete mode 100644 packages/theme/src/unified/MuiClassNameSetup.ts diff --git a/.changeset/vast-jeans-boil.md b/.changeset/vast-jeans-boil.md deleted file mode 100644 index 7fbc49cdfa..0000000000 --- a/.changeset/vast-jeans-boil.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@backstage/create-app': minor -'@backstage/theme': minor ---- - -Separates MUI 5 class name generator code into separate file and entry point that can be imported before any MUI 5 component loads. - -This addresses the problem where the elements contain the `v5-` prefix for MUI class names, but the static class names from the library do not. - -Import should be made as follows to ensure the prefix problem is addressed correctly: - -```diff -// packages/app/src/index.tsx - -+ import '@backstage/theme/MuiClassNameSetup'; // must be the very first import! -import '@backstage/cli/asset-types'; -import ReactDOM from 'react-dom/client'; -import app from './App'; -import '@backstage/ui/css/styles.css'; - -ReactDOM.createRoot(document.getElementById('root')!).render(app); -``` diff --git a/packages/app-legacy/src/index.tsx b/packages/app-legacy/src/index.tsx index d7699e3d9d..05dcc024bf 100644 --- a/packages/app-legacy/src/index.tsx +++ b/packages/app-legacy/src/index.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import '@backstage/theme/MuiClassNameSetup'; import '@backstage/cli/asset-types'; import ReactDOM from 'react-dom/client'; import App from './App'; diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx index 873670de0f..fd86261385 100644 --- a/packages/app/src/index.tsx +++ b/packages/app/src/index.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import '@backstage/theme/MuiClassNameSetup'; import '@backstage/cli/asset-types'; import ReactDOM from 'react-dom/client'; import app from './App'; diff --git a/packages/create-app/templates/default-app/packages/app/src/index.tsx b/packages/create-app/templates/default-app/packages/app/src/index.tsx index 3cc56c886c..46f31902f4 100644 --- a/packages/create-app/templates/default-app/packages/app/src/index.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/index.tsx @@ -1,4 +1,3 @@ -import '@backstage/theme/MuiClassNameSetup'; import '@backstage/cli/asset-types'; import ReactDOM from 'react-dom/client'; import App from './App'; diff --git a/packages/create-app/templates/next-app/packages/app/src/index.tsx b/packages/create-app/templates/next-app/packages/app/src/index.tsx index 6936ff50bc..ac9e52bdc1 100644 --- a/packages/create-app/templates/next-app/packages/app/src/index.tsx +++ b/packages/create-app/templates/next-app/packages/app/src/index.tsx @@ -1,4 +1,3 @@ -import '@backstage/theme/MuiClassNameSetup'; import '@backstage/cli/asset-types'; import ReactDOM from 'react-dom/client'; import App from './App'; diff --git a/packages/theme/package.json b/packages/theme/package.json index fe272bcf1c..e95ae20b36 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -6,7 +6,9 @@ "role": "web-library" }, "publishConfig": { - "access": "public" + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" }, "keywords": [ "backstage" @@ -18,25 +20,7 @@ "directory": "packages/theme" }, "license": "Apache-2.0", - "sideEffects": [ - "./src/unified/MuiClassNameSetup.ts", - "./dist/MuiClassNameSetup.*" - ], - "exports": { - ".": "./src/index.ts", - "./MuiClassNameSetup": "./src/unified/MuiClassNameSetup.ts", - "./package.json": "./package.json" - }, - "typesVersions": { - "*": { - "MuiClassNameSetup": [ - "src/unified/MuiClassNameSetup.ts" - ], - "package.json": [ - "package.json" - ] - } - }, + "sideEffects": false, "main": "src/index.ts", "types": "src/index.ts", "files": [ diff --git a/packages/theme/report-MuiClassNameSetup.api.md b/packages/theme/report-MuiClassNameSetup.api.md deleted file mode 100644 index c05c3c72a8..0000000000 --- a/packages/theme/report-MuiClassNameSetup.api.md +++ /dev/null @@ -1,7 +0,0 @@ -## API Report File for "@backstage/theme" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -// (No @packageDocumentation comment for this package) -``` diff --git a/packages/theme/src/unified/MuiClassNameSetup.ts b/packages/theme/src/unified/MuiClassNameSetup.ts deleted file mode 100644 index b069394838..0000000000 --- a/packages/theme/src/unified/MuiClassNameSetup.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2026 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { unstable_ClassNameGenerator as ClassNameGenerator } from '@mui/material/className'; - -/** - * This API is introduced in @mui/material (v5.0.5) as a replacement of deprecated createGenerateClassName & only affects v5 Material UI components from `@mui/*`. - * - * This needs to be configured before any MUI 5 component can possibly be imported. See: https://v5.mui.com/material-ui/experimental-api/classname-generator/#caveat - * - * ```diff - * // packages/app/src/index.tsx - * - * + import '@backstage/theme/MuiClassNameSetup'; // must be the very first import! - * import '@backstage/cli/asset-types'; - * import ReactDOM from 'react-dom/client'; - * import app from './App'; - * import '@backstage/ui/css/styles.css'; - * - * ReactDOM.createRoot(document.getElementById('root')!).render(app); - * ``` - */ -ClassNameGenerator.configure(componentName => { - return componentName.startsWith('v5-') - ? componentName - : `v5-${componentName}`; -}); - -export {}; diff --git a/packages/theme/src/unified/UnifiedThemeProvider.tsx b/packages/theme/src/unified/UnifiedThemeProvider.tsx index 678be82209..69d843ed3e 100644 --- a/packages/theme/src/unified/UnifiedThemeProvider.tsx +++ b/packages/theme/src/unified/UnifiedThemeProvider.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import './MuiClassNameSetup'; import { ReactNode } from 'react'; import { ThemeProvider, @@ -28,6 +27,7 @@ import { Theme as Mui5Theme, } from '@mui/material/styles'; import { UnifiedTheme } from './types'; +import { unstable_ClassNameGenerator as ClassNameGenerator } from '@mui/material/className'; /** * Props for {@link UnifiedThemeProvider}. @@ -41,6 +41,15 @@ export interface UnifiedThemeProviderProps { themeName?: string; } +/** + * This API is introduced in @mui/material (v5.0.5) as a replacement of deprecated createGenerateClassName & only affects v5 Material UI components from `@mui/*`. + * + * This call needs to be in the same module as the `UnifiedThemeProvider` to ensure that it doesn't get removed by tree shaking + */ +ClassNameGenerator.configure(componentName => { + return `v5-${componentName}`; +}); + // Background at https://mui.com/x/migration/migration-data-grid-v4/#using-mui-core-v4-with-v5 // Rather than disabling globals and custom seed, we instead only set a production prefix that // won't collide with Material UI 5 styles. We've already got the separate class name generator From 2c541a782be76d41c6dde057257c0ab15d58a104 Mon Sep 17 00:00:00 2001 From: Rajib Quayum Date: Fri, 3 Apr 2026 09:29:06 -0400 Subject: [PATCH 34/48] fix: prevent occasional duplication of the MUI v5 prefix Signed-off-by: Rajib Quayum --- packages/theme/src/unified/UnifiedThemeProvider.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/theme/src/unified/UnifiedThemeProvider.tsx b/packages/theme/src/unified/UnifiedThemeProvider.tsx index 69d843ed3e..f10320393d 100644 --- a/packages/theme/src/unified/UnifiedThemeProvider.tsx +++ b/packages/theme/src/unified/UnifiedThemeProvider.tsx @@ -47,6 +47,9 @@ export interface UnifiedThemeProviderProps { * This call needs to be in the same module as the `UnifiedThemeProvider` to ensure that it doesn't get removed by tree shaking */ ClassNameGenerator.configure(componentName => { + if ((componentName ?? '').startsWith('v5-')) { + return componentName; + } return `v5-${componentName}`; }); From e22fc0af9c7fc094cdc3797d7df7927d8118ce37 Mon Sep 17 00:00:00 2001 From: Rajib Quayum Date: Fri, 3 Apr 2026 10:01:25 -0400 Subject: [PATCH 35/48] chore: update docs for MUI v5 class name prefix issue Signed-off-by: Rajib Quayum --- docs/conf/user-interface/index.md | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/conf/user-interface/index.md b/docs/conf/user-interface/index.md index 107fde3d57..b9eda5151c 100644 --- a/docs/conf/user-interface/index.md +++ b/docs/conf/user-interface/index.md @@ -634,3 +634,53 @@ export const myTheme = createUnifiedTheme({ ``` + +
+ Missing v5 prefix for MUI 5 class names + +If you are using MUI 5 components in the main app, you may notice that the rendered elements have a `v5-` prefix in front of the MUI class names, but not when you try to use the class name props in code. + +Example: + +```html +