From f14df56222a1ed754b0608ad66e4aa2aaa48c213 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 30 Dec 2024 13:24:31 +0100 Subject: [PATCH 01/14] 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 02/14] 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 6537e5f8c1447d77d8071456bd8b0bec7f475719 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Apr 2026 15:40:31 +0200 Subject: [PATCH 03/14] 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 04/14] 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 05/14] 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 06/14] 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 07/14] 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 08/14] 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 09/14] 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 10/14] 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 11/14] 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 12/14] 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 13/14] 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 14/14] 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: {