From c985173cdfccfe0276e090e83145d6e3acf365e6 Mon Sep 17 00:00:00 2001 From: Danylo Hotvianskyi Date: Fri, 11 Jul 2025 17:40:41 +0200 Subject: [PATCH 001/233] feat(scafolder): scaffolder-backend-module-github implement github:issues:create Signed-off-by: Danylo Hotvianskyi --- .../actions/githubIssuesCreate.examples.ts | 58 ++++++ .../src/actions/githubIssuesCreate.test.ts | 144 +++++++++++++++ .../src/actions/githubIssuesCreate.ts | 166 ++++++++++++++++++ .../src/actions/index.ts | 1 + .../src/module.ts | 5 + 5 files changed, 374 insertions(+) create mode 100644 plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.examples.ts create mode 100644 plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.test.ts create mode 100644 plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.ts diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.examples.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.examples.ts new file mode 100644 index 0000000000..17f6c8078b --- /dev/null +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.examples.ts @@ -0,0 +1,58 @@ +import { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import * as yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Create a simple issue', + example: yaml.stringify({ + steps: [ + { + action: 'github:issues:create', + name: 'Create issue', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + title: 'Bug report', + body: 'Found a bug that needs to be fixed', + }, + }, + ], + }), + }, + { + description: 'Create an issue with labels and assignees', + example: yaml.stringify({ + steps: [ + { + action: 'github:issues:create', + name: 'Create issue with metadata', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + title: 'Feature request', + body: 'This is a new feature request', + labels: ['enhancement', 'needs-review'], + assignees: ['octocat'], + milestone: 1, + }, + }, + ], + }), + }, + { + description: 'Create an issue with specific token', + example: yaml.stringify({ + steps: [ + { + action: 'github:issues:create', + name: 'Create issue with token', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + title: 'Documentation update', + body: 'Update the documentation for the new API', + labels: ['documentation'], + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.test.ts new file mode 100644 index 0000000000..440df36915 --- /dev/null +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.test.ts @@ -0,0 +1,144 @@ +import { createGithubIssuesCreateAction } from './githubIssuesCreate'; +import { + ScmIntegrations, + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, +} from '@backstage/integration'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { getOctokitOptions } from '../util'; + +jest.mock('../util', () => { + return { + getOctokitOptions: jest.fn(), + }; +}); + +import { Octokit } from 'octokit'; + +const octokitMock = Octokit as unknown as jest.Mock; +const mockOctokit = { + rest: { + issues: { + create: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: jest.fn(), +})); + +describe('github:issues:create', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const getOctokitOptionsMock = getOctokitOptions as jest.Mock; + const integrations = ScmIntegrations.fromConfig(config); + let githubCredentialsProvider: GithubCredentialsProvider; + let action: TemplateAction; + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + title: 'Test Issue', + body: 'This is a test issue', + labels: ['bug', 'test'], + assignees: ['octocat'], + milestone: 1, + }, + }); + + beforeEach(() => { + jest.resetAllMocks(); + octokitMock.mockImplementation(() => mockOctokit); + mockOctokit.rest.issues.create.mockResolvedValue({ + data: { + html_url: 'https://github.com/owner/repo/issues/1', + number: 1, + }, + }); + githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + action = createGithubIssuesCreateAction({ + integrations, + githubCredentialsProvider, + }); + }); + + it('should pass context logger to Octokit client', async () => { + await action.handler(mockContext); + + expect(octokitMock).toHaveBeenCalledWith( + expect.objectContaining({ log: mockContext.logger }), + ); + }); + + it('should call the githubApi for creating issue', async () => { + await action.handler(mockContext); + expect(mockOctokit.rest.issues.create).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + title: 'Test Issue', + body: 'This is a test issue', + labels: ['bug', 'test'], + assignees: ['octocat'], + milestone: 1, + }); + expect(getOctokitOptionsMock.mock.calls[0][0].token).toBeUndefined(); + }); + + it('should call the githubApi for creating issue with token', async () => { + await action.handler({ + ...mockContext, + input: { ...mockContext.input, token: 'gph_YourGitHubToken' }, + }); + expect(mockOctokit.rest.issues.create).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + title: 'Test Issue', + body: 'This is a test issue', + labels: ['bug', 'test'], + assignees: ['octocat'], + milestone: 1, + }); + expect(getOctokitOptionsMock.mock.calls[0][0].token).toEqual( + 'gph_YourGitHubToken', + ); + }); + + it('should output issue URL and number', async () => { + await action.handler(mockContext); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://github.com/owner/repo/issues/1', + ); + expect(mockContext.output).toHaveBeenCalledWith('issueNumber', 1); + }); + + it('should create issue with minimal input', async () => { + const minimalContext = createMockActionContext({ + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + title: 'Simple Issue', + }, + }); + + await action.handler(minimalContext); + expect(mockOctokit.rest.issues.create).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + title: 'Simple Issue', + body: undefined, + labels: undefined, + assignees: undefined, + milestone: undefined, + }); + }); +}); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.ts new file mode 100644 index 0000000000..757e4ac675 --- /dev/null +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.ts @@ -0,0 +1,166 @@ +import { + GithubCredentialsProvider, + ScmIntegrationRegistry, +} from '@backstage/integration'; +import { + createTemplateAction, + parseRepoUrl, +} from '@backstage/plugin-scaffolder-node'; +import { assertError, InputError } from '@backstage/errors'; +import { Octokit } from 'octokit'; +import { getOctokitOptions } from '../util'; +import { examples } from './githubIssuesCreate.examples'; + +/** + * Creates an issue on GitHub + * @public + */ +export function createGithubIssuesCreateAction(options: { + integrations: ScmIntegrationRegistry; + githubCredentialsProvider?: GithubCredentialsProvider; +}) { + const { integrations, githubCredentialsProvider } = options; + + return createTemplateAction({ + id: 'github:issues:create', + description: 'Creates an issue on GitHub.', + examples, + supportsDryRun: true, + schema: { + input: { + repoUrl: z => + z.string({ + description: + 'Accepts the format `github.com?repo=reponame&owner=owner` where `reponame` is the repository name and `owner` is an organization or username', + }), + title: z => + z.string({ + description: 'The title of the issue', + }), + body: z => + z + .string({ + description: 'The contents of the issue', + }) + .optional(), + assignees: z => + z + .array(z.string(), { + description: + 'Logins for Users to assign to this issue. NOTE: Only users with push access can set assignees for new issues. Assignees are silently dropped otherwise.', + }) + .optional(), + milestone: z => + z + .union([z.string(), z.number()], { + description: + 'The number of the milestone to associate this issue with. NOTE: Only users with push access can set the milestone for new issues. The milestone is silently dropped otherwise.', + }) + .optional(), + labels: z => + z + .array(z.string(), { + description: + 'Labels to associate with this issue. NOTE: Only users with push access can set labels for new issues. Labels are silently dropped otherwise.', + }) + .optional(), + token: z => + z + .string({ + description: + 'The `GITHUB_TOKEN` to use for authorization to GitHub', + }) + .optional(), + }, + output: { + issueUrl: z => + z.string({ + description: 'The URL of the created issue', + }), + issueNumber: z => + z.number({ + description: 'The number of the created issue', + }), + }, + }, + async handler(ctx) { + const { + repoUrl, + title, + body, + assignees, + milestone, + labels, + token: providedToken, + } = ctx.input; + + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + ctx.logger.info(`Creating issue "${title}" on repo ${repo}`); + + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + host, + owner, + repo, + token: providedToken, + }); + + const client = new Octokit({ + ...octokitOptions, + log: ctx.logger, + }); + + if (ctx.isDryRun) { + ctx.logger.info(`Performing dry run of creating issue "${title}"`); + ctx.output('issueUrl', `https://github.com/${owner}/${repo}/issues/42`); + ctx.output('issueNumber', 42); + ctx.logger.info(`Dry run complete`); + return; + } + + try { + const issue = await ctx.checkpoint({ + key: `github.issues.create.${owner}.${repo}.${title}`, + fn: async () => { + const response = await client.rest.issues.create({ + owner, + repo, + title, + body, + assignees, + milestone, + labels, + }); + + return { + html_url: response.data.html_url, + number: response.data.number, + }; + }, + }); + + if (!issue) { + throw new Error('Failed to create issue'); + } + + ctx.output('issueUrl', issue.html_url); + ctx.output('issueNumber', issue.number); + + ctx.logger.info( + `Successfully created issue #${issue.number}: ${issue.html_url}`, + ); + } catch (e) { + assertError(e); + ctx.logger.warn( + `Failed: creating issue '${title}' on repo: '${repo}', ${e.message}`, + ); + throw e; + } + }, + }); +} diff --git a/plugins/scaffolder-backend-module-github/src/actions/index.ts b/plugins/scaffolder-backend-module-github/src/actions/index.ts index 7cf4109e6b..21732d7908 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/index.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/index.ts @@ -16,6 +16,7 @@ export { createGithubActionsDispatchAction } from './githubActionsDispatch'; export { createGithubIssuesLabelAction } from './githubIssuesLabel'; +export { createGithubIssuesCreateAction } from './githubIssuesCreate'; export { createGithubRepoCreateAction } from './githubRepoCreate'; export { createGithubRepoPushAction } from './githubRepoPush'; export { createGithubWebhookAction } from './githubWebhook'; diff --git a/plugins/scaffolder-backend-module-github/src/module.ts b/plugins/scaffolder-backend-module-github/src/module.ts index 5812a22560..67bda58fc4 100644 --- a/plugins/scaffolder-backend-module-github/src/module.ts +++ b/plugins/scaffolder-backend-module-github/src/module.ts @@ -27,6 +27,7 @@ import { createGithubDeployKeyAction, createGithubEnvironmentAction, createGithubIssuesLabelAction, + createGithubIssuesCreateAction, createGithubRepoCreateAction, createGithubRepoPushAction, createGithubWebhookAction, @@ -82,6 +83,10 @@ export const githubModule = createBackendModule({ integrations, githubCredentialsProvider, }), + createGithubIssuesCreateAction({ + integrations, + githubCredentialsProvider, + }), createGithubRepoCreateAction({ integrations, githubCredentialsProvider, From f0f06b43542dec7533ae5c4d9cdb7b9a654139bc Mon Sep 17 00:00:00 2001 From: Danylo Hotvianskyi Date: Fri, 11 Jul 2025 18:12:07 +0200 Subject: [PATCH 002/233] add changeset Signed-off-by: Danylo Hotvianskyi --- .changeset/stupid-pans-flash.md | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .changeset/stupid-pans-flash.md diff --git a/.changeset/stupid-pans-flash.md b/.changeset/stupid-pans-flash.md new file mode 100644 index 0000000000..bc857a243a --- /dev/null +++ b/.changeset/stupid-pans-flash.md @@ -0,0 +1,35 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': minor +--- + +Adding a new scaffolder action `github:issues:create` following the reference of `github:issues:label` with dryRun testing possibility + +It can be used like this + +``` + steps: + - id: create-simple-issue + name: Create Simple Issue + action: github:issues:create + input: + repoUrl: ${{ parameters.repoUrl }} + title: "[${{ parameters.projectName }}] Simple Bug Report" + body: | + ## Bug Description + This is a simple bug report created by the scaffolder template. + + ### Steps to Reproduce + 1. Run the application + 2. Navigate to the main page + 3. Click on the problematic button + + ### Expected Behavior + The button should work correctly. + + ### Actual Behavior + The button does not respond to clicks. + output: + links: + - title: Simple Issue + url: ${{ steps['create-simple-issue'].output.issueUrl }} +``` From 16ee5911ebc5d3051b25652695f241e99df12d89 Mon Sep 17 00:00:00 2001 From: Danylo Hotvianskyi Date: Fri, 11 Jul 2025 18:27:29 +0200 Subject: [PATCH 003/233] Add notices Signed-off-by: Danylo Hotvianskyi --- .../actions/githubIssuesCreate.examples.ts | 15 ++++++++++++++ .../src/actions/githubIssuesCreate.test.ts | 16 +++++++++++++++ .../src/actions/githubIssuesCreate.ts | 20 +++++++++++++++++-- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.examples.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.examples.ts index 17f6c8078b..7d767f0337 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.examples.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.examples.ts @@ -1,3 +1,18 @@ +/* + * Copyright 2023 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 { TemplateExample } from '@backstage/plugin-scaffolder-node'; import * as yaml from 'yaml'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.test.ts index 440df36915..474acf14d7 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.test.ts @@ -1,3 +1,19 @@ +/* + * Copyright 2021 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 { createGithubIssuesCreateAction } from './githubIssuesCreate'; import { ScmIntegrations, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.ts index 757e4ac675..5c5703e20d 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesCreate.ts @@ -1,3 +1,19 @@ +/* + * Copyright 2021 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 { GithubCredentialsProvider, ScmIntegrationRegistry, @@ -109,7 +125,7 @@ export function createGithubIssuesCreateAction(options: { repo, token: providedToken, }); - + const client = new Octokit({ ...octokitOptions, log: ctx.logger, @@ -157,7 +173,7 @@ export function createGithubIssuesCreateAction(options: { } catch (e) { assertError(e); ctx.logger.warn( - `Failed: creating issue '${title}' on repo: '${repo}', ${e.message}`, + `Failed: creating issue '${title}' on repo: '${repo}', ${e.message}`, ); throw e; } From d5f4dd1c9078a0903e8578649e30ad6024ddf629 Mon Sep 17 00:00:00 2001 From: Danylo Hotvianskyi Date: Fri, 11 Jul 2025 18:55:13 +0200 Subject: [PATCH 004/233] Changes in public API Signed-off-by: Danylo Hotvianskyi --- .../report.api.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index 2941be7702..a2dcc3ec4e 100644 --- a/plugins/scaffolder-backend-module-github/report.api.md +++ b/plugins/scaffolder-backend-module-github/report.api.md @@ -136,6 +136,27 @@ export function createGithubEnvironmentAction(options: { 'v2' >; +// @public +export function createGithubIssuesCreateAction(options: { + integrations: ScmIntegrationRegistry; + githubCredentialsProvider?: GithubCredentialsProvider; +}): TemplateAction< + { + repoUrl: string; + title: string; + body?: string | undefined; + assignees?: string[] | undefined; + milestone?: string | number | undefined; + labels?: string[] | undefined; + token?: string | undefined; + }, + { + issueUrl: string; + issueNumber: number; + }, + 'v2' +>; + // @public export function createGithubIssuesLabelAction(options: { integrations: ScmIntegrationRegistry; From 5ae6d9dd4bd709a44bc61bcc326585ccfff27ab2 Mon Sep 17 00:00:00 2001 From: JounQin Date: Tue, 29 Jul 2025 17:33:05 +0800 Subject: [PATCH 005/233] feat(core-app-api): support no en languages Signed-off-by: JounQin --- .changeset/tasty-yaks-kiss.md | 5 +++++ .../AppLanguageApi/AppLanguageSelector.test.ts | 17 ++++++++++++++++- .../AppLanguageApi/AppLanguageSelector.ts | 4 +--- 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 .changeset/tasty-yaks-kiss.md diff --git a/.changeset/tasty-yaks-kiss.md b/.changeset/tasty-yaks-kiss.md new file mode 100644 index 0000000000..125b77a870 --- /dev/null +++ b/.changeset/tasty-yaks-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': patch +--- + +feat: support no en languages diff --git a/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.test.ts b/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.test.ts index 702d5e642a..eeeb11634d 100644 --- a/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.test.ts +++ b/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.test.ts @@ -144,11 +144,26 @@ describe('AppLanguageSelector', () => { AppLanguageSelector.createWithStorage({ availableLanguages: ['de'], }), - ).toThrow("Supported languages must include 'en'"); + ).toThrow( + "Initial language must be one of the supported languages, got 'en'", + ); const selector = AppLanguageSelector.createWithStorage(baseOptions); expect(() => selector.setLanguage('sv')).toThrow( "Failed to change language to 'sv', available languages are 'en', 'de'", ); }); + + it('should support no en languages', () => { + const selector = AppLanguageSelector.createWithStorage({ + availableLanguages: ['de'], + defaultLanguage: 'de', + }); + + expect(selector.getLanguage()).toEqual({ language: 'de' }); + + expect(() => selector.setLanguage('en')).toThrow( + "Failed to change language to 'en', available languages are 'de'", + ); + }); }); diff --git a/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.ts b/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.ts index 93f3d6517c..04dcfe9ebb 100644 --- a/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.ts +++ b/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.ts @@ -44,11 +44,9 @@ export class AppLanguageSelector implements AppLanguageApi { )}'`, ); } - if (!languages.includes(DEFAULT_LANGUAGE)) { - throw new Error(`Supported languages must include '${DEFAULT_LANGUAGE}'`); - } const initialLanguage = options?.defaultLanguage ?? DEFAULT_LANGUAGE; + if (!languages.includes(initialLanguage)) { throw new Error( `Initial language must be one of the supported languages, got '${initialLanguage}'`, From 4316c11c3d105b0e8f2d0066e967efbea40769d4 Mon Sep 17 00:00:00 2001 From: mario ma Date: Wed, 30 Jul 2025 17:55:39 +0800 Subject: [PATCH 006/233] feat: Catalog table columns support i18n Signed-off-by: mario ma --- .changeset/lovely-frogs-share.md | 6 +++ plugins/catalog-react/report-alpha.api.md | 46 ++++++++++++++++++ plugins/catalog-react/report.api.md | 4 +- plugins/catalog-react/src/alpha/index.ts | 1 + .../EntityTable/TitleColumn.test.tsx | 25 ++++++++++ .../components/EntityTable/TitleColumn.tsx | 47 +++++++++++++++++++ .../src/components/EntityTable/columns.tsx | 19 ++++---- plugins/catalog-react/src/translation.ts | 14 ++++++ .../src/components/CatalogTable/columns.tsx | 25 +++++----- 9 files changed, 164 insertions(+), 23 deletions(-) create mode 100644 .changeset/lovely-frogs-share.md create mode 100644 plugins/catalog-react/src/components/EntityTable/TitleColumn.test.tsx create mode 100644 plugins/catalog-react/src/components/EntityTable/TitleColumn.tsx diff --git a/.changeset/lovely-frogs-share.md b/.changeset/lovely-frogs-share.md new file mode 100644 index 0000000000..5c630f301e --- /dev/null +++ b/.changeset/lovely-frogs-share.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-catalog': patch +--- + +Catalog table columns support i18n diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 1db7a8e5e4..ebdf9f0e79 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -86,6 +86,18 @@ export const catalogReactTranslationRef: TranslationRef< readonly 'userListPicker.personalFilter.title': 'Personal'; readonly 'userListPicker.personalFilter.ownedLabel': 'Owned'; readonly 'userListPicker.personalFilter.starredLabel': 'Starred'; + readonly 'entityTableColumnTitle.name': 'Name'; + readonly 'entityTableColumnTitle.type': 'Type'; + readonly 'entityTableColumnTitle.label': 'Label'; + readonly 'entityTableColumnTitle.title': 'Title'; + readonly 'entityTableColumnTitle.description': 'Description'; + readonly 'entityTableColumnTitle.domain': 'Domain'; + readonly 'entityTableColumnTitle.namespace': 'Namespace'; + readonly 'entityTableColumnTitle.lifecycle': 'Lifecycle'; + readonly 'entityTableColumnTitle.owner': 'Owner'; + readonly 'entityTableColumnTitle.system': 'System'; + readonly 'entityTableColumnTitle.targets': 'Targets'; + readonly 'entityTableColumnTitle.tags': 'Tags'; } >; @@ -517,6 +529,40 @@ export type EntityPredicateValue = $contains: EntityPredicateExpression; }; +// @alpha (undocumented) +export const EntityTableColumnTitle: ({ + translationKey, +}: EntityTableColumnTitleProps) => + | 'Domain' + | 'System' + | 'Name' + | 'Description' + | 'Lifecycle' + | 'Namespace' + | 'Owner' + | 'Tags' + | 'Type' + | 'Targets' + | 'Title' + | 'Label'; + +// @alpha (undocumented) +export type EntityTableColumnTitleProps = { + translationKey: + | 'name' + | 'system' + | 'owner' + | 'type' + | 'lifecycle' + | 'namespace' + | 'description' + | 'tags' + | 'targets' + | 'title' + | 'label' + | 'domain'; +}; + // @alpha export function isOwnerOf(owner: Entity, entity: Entity): boolean; diff --git a/plugins/catalog-react/report.api.md b/plugins/catalog-react/report.api.md index bfebb2b13a..df32c51416 100644 --- a/plugins/catalog-react/report.api.md +++ b/plugins/catalog-react/report.api.md @@ -171,7 +171,7 @@ export const columnFactories: Readonly<{ defaultKind?: string; }): TableColumn; createEntityRelationColumn(options: { - title: string; + title: string | JSX.Element; relation: string; defaultKind?: string; filter?: { @@ -575,7 +575,7 @@ export const EntityTable: { defaultKind?: string; }): TableColumn; createEntityRelationColumn(options: { - title: string; + title: string | JSX.Element; relation: string; defaultKind?: string; filter?: { diff --git a/plugins/catalog-react/src/alpha/index.ts b/plugins/catalog-react/src/alpha/index.ts index 4ff4dbf0dd..f5909c9aa2 100644 --- a/plugins/catalog-react/src/alpha/index.ts +++ b/plugins/catalog-react/src/alpha/index.ts @@ -20,3 +20,4 @@ export * from './predicates'; export { catalogReactTranslationRef } from '../translation'; export { isOwnerOf } from '../utils/isOwnerOf'; export { useEntityPermission } from '../hooks/useEntityPermission'; +export * from '../components/EntityTable/TitleColumn'; diff --git a/plugins/catalog-react/src/components/EntityTable/TitleColumn.test.tsx b/plugins/catalog-react/src/components/EntityTable/TitleColumn.test.tsx new file mode 100644 index 0000000000..e08741d274 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityTable/TitleColumn.test.tsx @@ -0,0 +1,25 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { screen } from '@testing-library/react'; +import { EntityTableColumnTitle } from './TitleColumn'; +import { renderInTestApp } from '@backstage/test-utils'; + +describe('', () => { + it('renders the translated title for the given key', async () => { + await renderInTestApp(); + expect(screen.getByText('Name')).toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog-react/src/components/EntityTable/TitleColumn.tsx b/plugins/catalog-react/src/components/EntityTable/TitleColumn.tsx new file mode 100644 index 0000000000..36bce84882 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityTable/TitleColumn.tsx @@ -0,0 +1,47 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useTranslationRef } from '@backstage/frontend-plugin-api'; +import { catalogReactTranslationRef } from '../../translation'; + +/** + * @alpha + */ +export type EntityTableColumnTitleProps = { + translationKey: + | 'name' + | 'system' + | 'owner' + | 'type' + | 'lifecycle' + | 'namespace' + | 'description' + | 'tags' + | 'targets' + | 'title' + | 'label' + | 'domain'; +}; + +/** + * @alpha + */ +export const EntityTableColumnTitle = ({ + translationKey, +}: EntityTableColumnTitleProps) => { + const { t } = useTranslationRef(catalogReactTranslationRef); + return t(`entityTableColumnTitle.${translationKey}`); +}; diff --git a/plugins/catalog-react/src/components/EntityTable/columns.tsx b/plugins/catalog-react/src/components/EntityTable/columns.tsx index 00e1e5ad43..94b1141057 100644 --- a/plugins/catalog-react/src/components/EntityTable/columns.tsx +++ b/plugins/catalog-react/src/components/EntityTable/columns.tsx @@ -27,8 +27,7 @@ import { EntityRefLinks, humanizeEntityRef, } from '../EntityRefLink'; - -// TODO: column title support i18n +import { EntityTableColumnTitle } from './TitleColumn'; /** @public */ export const columnFactories = Object.freeze({ @@ -46,7 +45,7 @@ export const columnFactories = Object.freeze({ } return { - title: 'Name', + title: , highlight: true, customFilterAndSearch(filter, entity) { // TODO: We could implement this more efficiently, like searching over @@ -72,7 +71,7 @@ export const columnFactories = Object.freeze({ }; }, createEntityRelationColumn(options: { - title: string; + title: string | JSX.Element; relation: string; defaultKind?: string; filter?: { kind: string }; @@ -109,14 +108,14 @@ export const columnFactories = Object.freeze({ }, createOwnerColumn(): TableColumn { return this.createEntityRelationColumn({ - title: 'Owner', + title: , relation: RELATION_OWNED_BY, defaultKind: 'group', }); }, createDomainColumn(): TableColumn { return this.createEntityRelationColumn({ - title: 'Domain', + title: , relation: RELATION_PART_OF, defaultKind: 'domain', filter: { @@ -126,7 +125,7 @@ export const columnFactories = Object.freeze({ }, createSystemColumn(): TableColumn { return this.createEntityRelationColumn({ - title: 'System', + title: , relation: RELATION_PART_OF, defaultKind: 'system', filter: { @@ -136,7 +135,7 @@ export const columnFactories = Object.freeze({ }, createMetadataDescriptionColumn(): TableColumn { return { - title: 'Description', + title: , field: 'metadata.description', render: entity => ( (): TableColumn { return { - title: 'Lifecycle', + title: , field: 'spec.lifecycle', }; }, createSpecTypeColumn(): TableColumn { return { - title: 'Type', + title: , field: 'spec.type', }; }, diff --git a/plugins/catalog-react/src/translation.ts b/plugins/catalog-react/src/translation.ts index 7f9661380f..6e831886cd 100644 --- a/plugins/catalog-react/src/translation.ts +++ b/plugins/catalog-react/src/translation.ts @@ -124,5 +124,19 @@ export const catalogReactTranslationRef = createTranslationRef({ }, orgFilterAllLabel: 'All', }, + entityTableColumnTitle: { + name: 'Name', + system: 'System', + owner: 'Owner', + type: 'Type', + lifecycle: 'Lifecycle', + namespace: 'Namespace', + description: 'Description', + tags: 'Tags', + targets: 'Targets', + title: 'Title', + label: 'Label', + domain: 'Domain', + }, }, }); diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx index 05674f3e1b..65094bc568 100644 --- a/plugins/catalog/src/components/CatalogTable/columns.tsx +++ b/plugins/catalog/src/components/CatalogTable/columns.tsx @@ -23,6 +23,7 @@ import { CatalogTableRow } from './types'; import { OverflowTooltip, TableColumn } from '@backstage/core-components'; import { Entity } from '@backstage/catalog-model'; import { JsonArray } from '@backstage/types'; +import { EntityTableColumnTitle } from '@backstage/plugin-catalog-react/alpha'; // The columnFactories symbol is not directly exported, but through the // CatalogTable.columns field. @@ -41,7 +42,7 @@ export const columnFactories = Object.freeze({ } return { - title: 'Name', + title: , field: 'resolved.entityRef', highlight: true, customSort({ entity: entity1 }, { entity: entity2 }) { @@ -59,7 +60,7 @@ export const columnFactories = Object.freeze({ }, createSystemColumn(): TableColumn { return { - title: 'System', + title: , field: 'resolved.partOfSystemRelationTitle', customFilterAndSearch: (query, row) => { if (!row.resolved.partOfSystemRelations) { @@ -83,7 +84,7 @@ export const columnFactories = Object.freeze({ }, createOwnerColumn(): TableColumn { return { - title: 'Owner', + title: , field: 'resolved.ownedByRelationsTitle', render: ({ resolved }) => ( { return { - title: 'Targets', + title: , field: 'entity.spec.targets', customFilterAndSearch: (query, row) => { let targets: JsonArray = []; @@ -132,7 +133,7 @@ export const columnFactories = Object.freeze({ } = { hidden: false }, ): TableColumn { return { - title: 'Type', + title: , field: 'entity.spec.type', hidden: options.hidden, width: 'auto', @@ -140,13 +141,13 @@ export const columnFactories = Object.freeze({ }, createSpecLifecycleColumn(): TableColumn { return { - title: 'Lifecycle', + title: , field: 'entity.spec.lifecycle', }; }, createMetadataDescriptionColumn(): TableColumn { return { - title: 'Description', + title: , field: 'entity.metadata.description', render: ({ entity }) => ( { return { - title: 'Tags', + title: , field: 'entity.metadata.tags', cellStyle: { padding: '0px 16px 0px 20px', @@ -185,7 +186,7 @@ export const columnFactories = Object.freeze({ hidden?: boolean; }): TableColumn { return { - title: 'Title', + title: , field: 'entity.metadata.title', hidden: options?.hidden, searchable: true, @@ -202,7 +203,9 @@ export const columnFactories = Object.freeze({ } return { - title: options?.title || 'Label', + title: options?.title || ( + + ), field: 'entity.metadata.labels', cellStyle: { padding: '0px 16px 0px 20px', @@ -235,7 +238,7 @@ export const columnFactories = Object.freeze({ }, createNamespaceColumn(): TableColumn { return { - title: 'Namespace', + title: , field: 'entity.metadata.namespace', width: 'auto', }; From 88225a8cf3ede83dc9b932ae560bb9cf111e3707 Mon Sep 17 00:00:00 2001 From: mario ma Date: Fri, 1 Aug 2025 15:27:21 +0800 Subject: [PATCH 007/233] fix: api report Signed-off-by: mario ma --- plugins/catalog-react/report-alpha.api.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index ebdf9f0e79..132cf5eac7 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -533,17 +533,17 @@ export type EntityPredicateValue = export const EntityTableColumnTitle: ({ translationKey, }: EntityTableColumnTitleProps) => + | 'Title' | 'Domain' | 'System' - | 'Name' - | 'Description' | 'Lifecycle' | 'Namespace' | 'Owner' | 'Tags' | 'Type' + | 'Name' + | 'Description' | 'Targets' - | 'Title' | 'Label'; // @alpha (undocumented) From ea80e76e7c997ecacd57b551ce01fbc9b390e6cb Mon Sep 17 00:00:00 2001 From: Patrick McDonnell Date: Fri, 1 Aug 2025 13:35:47 +0200 Subject: [PATCH 008/233] perf: use GitLab API simple=true parameter when not filtering forks Reduces response size by conditionally using simple=true in GitLab list projects API calls. Only applied when skipForkedRepos is false since fork detection requires the forked_from_project field excluded by simple. Signed-off-by: Patrick McDonnell --- .changeset/cuddly-lemons-cheat.md | 5 + .../src/GitLabDiscoveryProcessor.test.ts | 107 +++++++++++++++ .../src/GitLabDiscoveryProcessor.ts | 5 + .../src/lib/client.test.ts | 49 +++++++ .../src/lib/client.ts | 1 + .../GitlabDiscoveryEntityProvider.test.ts | 127 ++++++++++++++++++ .../GitlabDiscoveryEntityProvider.ts | 5 + 7 files changed, 299 insertions(+) create mode 100644 .changeset/cuddly-lemons-cheat.md diff --git a/.changeset/cuddly-lemons-cheat.md b/.changeset/cuddly-lemons-cheat.md new file mode 100644 index 0000000000..aa4ee786ff --- /dev/null +++ b/.changeset/cuddly-lemons-cheat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': patch +--- + +When possible, requests a more limited set of results from the Gitlab projects API, which can reduce the amount of network traffic required to sync with Gitlab. diff --git a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts index d75d0e224e..21cbd24232 100644 --- a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts @@ -59,6 +59,7 @@ function setupFakeServer( page: number; include_subgroups: boolean; archived: boolean; + simple?: boolean; }) => { data: GitLabProject[]; nextPage?: number; @@ -78,10 +79,12 @@ function setupFakeServer( const page = req.url.searchParams.get('page'); const include_subgroups = req.url.searchParams.get('include_subgroups'); const archived = req.url.searchParams.get('archived'); + const simple = req.url.searchParams.get('simple'); const response = listProjectsCallback({ page: parseInt(page!, 10), include_subgroups: include_subgroups === 'true', archived: archived === 'true', + simple: simple === 'true', }); // Filter the fake results based on the `last_activity_after` parameter @@ -471,6 +474,110 @@ describe('GitlabDiscoveryProcessor', () => { }); expect(result2).toHaveLength(1); }); + + it('sets simple=true when skipForkedRepos is false', async () => { + const processor = getProcessor({ + options: { skipForkedRepos: false }, + }); + setupFakeServer( + PROJECTS_URL, + request => { + return { + data: [ + { + id: 1, + archived: false, + default_branch: 'main', + last_activity_at: '2021-08-05T11:03:05.774Z', + web_url: 'https://gitlab.fake/1', + path_with_namespace: '1', + }, + ], + }; + }, + request => { + // Verify that simple=true is set in the request + expect(request.url.searchParams.get('simple')).toBe('true'); + }, + ); + + const result: any[] = []; + await processor.readLocation(PROJECT_LOCATION, false, e => { + result.push(e); + }); + expect(result).toHaveLength(1); + }); + + it('does not set simple when skipForkedRepos is true', async () => { + const processor = getProcessor({ + options: { skipForkedRepos: true }, + }); + setupFakeServer( + PROJECTS_URL, + request => { + return { + data: [ + { + id: 1, + archived: false, + default_branch: 'main', + last_activity_at: '2021-08-05T11:03:05.774Z', + web_url: 'https://gitlab.fake/1', + path_with_namespace: '1', + // Include forked_from_project to test fork filtering + forked_from_project: { + id: 100, + name: 'original-project', + }, + }, + ], + }; + }, + request => { + // Verify that simple parameter is not set + expect(request.url.searchParams.get('simple')).toBeNull(); + }, + ); + + const result: any[] = []; + await processor.readLocation(PROJECT_LOCATION, false, e => { + result.push(e); + }); + // Should be empty because forked repo is skipped + expect(result).toHaveLength(0); + }); + + it('sets default parameters correctly (archived=false, simple=true)', async () => { + const processor = getProcessor(); // Uses defaults: skipForkedRepos=false, includeArchivedRepos=false + setupFakeServer( + PROJECTS_URL, + request => { + return { + data: [ + { + id: 1, + archived: false, + default_branch: 'main', + last_activity_at: '2021-08-05T11:03:05.774Z', + web_url: 'https://gitlab.fake/1', + path_with_namespace: '1', + }, + ], + }; + }, + request => { + // Verify default parameters: archived=false and simple=true + expect(request.url.searchParams.get('archived')).toBe('false'); + expect(request.url.searchParams.get('simple')).toBe('true'); + }, + ); + + const result: any[] = []; + await processor.readLocation(PROJECT_LOCATION, false, e => { + result.push(e); + }); + expect(result).toHaveLength(1); + }); }); describe('handles failure', () => { diff --git a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts index ab31a44bbc..126bb07f03 100644 --- a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts @@ -115,6 +115,11 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { // that the options doesn't include the key so that the API doesn't receive an empty query parameter. ...(lastActivity && { last_activity_after: lastActivity }), ...(!this.includeArchivedRepos && { archived: false }), + // Only use simple=true when we don't need to skip forked repos. + // The simple=true parameter reduces response size by returning fewer fields, + // but it excludes the 'forked_from_project' field which is required for fork detection. + // Therefore, we can only optimize with simple=true when skipForkedRepos is false. + ...(!this.skipForkedRepos && { simple: true }), }; const projects = paginated(options => client.listProjects(options), opts); diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts index daea71060f..9b3bac6399 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts @@ -187,6 +187,55 @@ describe('GitLabClient', () => { expect(allProjects).toHaveLength(mock.all_projects_response.length); }); + + it('should pass simple parameter to API when provided', async () => { + const client = new GitLabClient({ + config: readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), + logger: mockServices.logger.mock(), + }); + + // Mock the pagedRequest method to verify parameters + const mockPagedRequest = jest.fn().mockResolvedValue({ + items: [], + nextPage: undefined, + }); + (client as any).pagedRequest = mockPagedRequest; + + await client.listProjects({ simple: true }); + + expect(mockPagedRequest).toHaveBeenCalledWith('/projects', { + simple: true, + }); + }); + + it('should pass simple parameter to group projects API when provided', async () => { + const client = new GitLabClient({ + config: readGitLabIntegrationConfig( + new ConfigReader(mock.config_self_managed), + ), + logger: mockServices.logger.mock(), + }); + + // Mock the pagedRequest method to verify parameters + const mockPagedRequest = jest.fn().mockResolvedValue({ + items: [], + nextPage: undefined, + }); + (client as any).pagedRequest = mockPagedRequest; + + await client.listProjects({ group: 'test-group', simple: true }); + + expect(mockPagedRequest).toHaveBeenCalledWith( + '/groups/test-group/projects', + { + group: 'test-group', + simple: true, + include_subgroups: true, + }, + ); + }); }); describe('listUsers', () => { diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts index e58227839e..3178e2c0d1 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts @@ -43,6 +43,7 @@ interface ListProjectOptions extends CommonListOptions { group?: string; membership?: boolean; topics?: string; + simple?: boolean; } interface UserListOptions extends CommonListOptions { diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts index dd7a4f0d6d..b03f2c4b5b 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts @@ -147,6 +147,21 @@ describe('GitlabDiscoveryEntityProvider - refresh', () => { 'GitlabDiscoveryEntityProvider:test-id', ); + // Mock the GitLabClient listProjects method to verify default parameters + const originalListProjects = (provider as any).gitLabClient.listProjects; + const mockListProjects = jest.fn().mockImplementation(async options => { + // Verify default parameters: archived=false and simple=true (since skipForkedRepos=false by default) + expect(options).toMatchObject({ + group: 'group1', + per_page: 50, + archived: false, + simple: true, // Should be set since skipForkedRepos defaults to false + }); + // Call the original method to maintain test behavior + return originalListProjects.call((provider as any).gitLabClient, options); + }); + (provider as any).gitLabClient.listProjects = mockListProjects; + await provider.connect(entityProviderConnection); const taskDef = schedule.getTasks()[0]; @@ -559,3 +574,115 @@ describe('GitlabDiscoveryEntityProvider - events', () => { }); // EventSupportChange: stop add tests >>> }); + +describe('GitlabDiscoveryEntityProvider - simple parameter', () => { + it('should pass simple=true when skipForkedRepos is false', async () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'example.com', + apiBaseUrl: 'https://example.com/api/v4', + token: 'test-token', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'example.com', + group: 'test-group', + skipForkedRepos: false, + }, + }, + }, + }, + }); + + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + + const provider = GitlabDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + + // Mock the GitLabClient listProjects method to verify parameters + const mockListProjects = jest.fn().mockResolvedValue({ + items: [], + nextPage: undefined, + }); + + (provider as any).gitLabClient.listProjects = mockListProjects; + + await provider.connect(entityProviderConnection); + await provider.refresh(logger); + + expect(mockListProjects).toHaveBeenCalledWith({ + group: 'test-group', + page: undefined, + per_page: 50, + archived: false, + simple: true, // Should be set when skipForkedRepos is false + }); + }); + + it('should not pass simple when skipForkedRepos is true', async () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'example.com', + apiBaseUrl: 'https://example.com/api/v4', + token: 'test-token', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'example.com', + group: 'test-group', + skipForkedRepos: true, + }, + }, + }, + }, + }); + + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + + const provider = GitlabDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + + // Mock the GitLabClient listProjects method to verify parameters + const mockListProjects = jest.fn().mockResolvedValue({ + items: [], + nextPage: undefined, + }); + + (provider as any).gitLabClient.listProjects = mockListProjects; + + await provider.connect(entityProviderConnection); + await provider.refresh(logger); + + expect(mockListProjects).toHaveBeenCalledWith({ + group: 'test-group', + page: undefined, + per_page: 50, + archived: false, + // simple should not be present when skipForkedRepos is true + }); + }); +}); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index 8bf749c275..243ffd5287 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -216,6 +216,11 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { ...(!this.config.includeArchivedRepos && { archived: false }), ...(this.config.membership && { membership: true }), ...(this.config.topics && { topics: this.config.topics }), + // Only use simple=true when we don't need to skip forked repos. + // The simple=true parameter reduces response size by returning fewer fields, + // but it excludes the 'forked_from_project' field which is required for fork detection. + // Therefore, we can only optimize with simple=true when skipForkedRepos is false. + ...(!this.config.skipForkedRepos && { simple: true }), }, ); From f8db72be543bf81956822b2d0f19101aae5f647b Mon Sep 17 00:00:00 2001 From: Patrick McDonnell Date: Fri, 1 Aug 2025 13:46:50 +0200 Subject: [PATCH 009/233] fix linter error Signed-off-by: Patrick McDonnell --- .../src/GitLabDiscoveryProcessor.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts index 21cbd24232..774da46553 100644 --- a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts @@ -481,7 +481,7 @@ describe('GitlabDiscoveryProcessor', () => { }); setupFakeServer( PROJECTS_URL, - request => { + _ => { return { data: [ { @@ -514,7 +514,7 @@ describe('GitlabDiscoveryProcessor', () => { }); setupFakeServer( PROJECTS_URL, - request => { + _ => { return { data: [ { @@ -551,7 +551,7 @@ describe('GitlabDiscoveryProcessor', () => { const processor = getProcessor(); // Uses defaults: skipForkedRepos=false, includeArchivedRepos=false setupFakeServer( PROJECTS_URL, - request => { + _ => { return { data: [ { From a73f4958401c2f6e6a46331b53dea6403e28a9e1 Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Fri, 1 Aug 2025 15:45:22 +0300 Subject: [PATCH 010/233] feat(config): allow specifying env specific config files this change allows to specify loading of environment specific config files based on `BACKSTAGE_ENVIRONMENT` environment variable. relates to #30716 Signed-off-by: Hellgren Heikki --- .changeset/better-eagles-tickle.md | 5 +++++ docs/conf/index.md | 21 +++++++++++++++---- .../src/sources/ConfigSources.test.ts | 12 +++++++++++ .../src/sources/ConfigSources.ts | 14 +++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 .changeset/better-eagles-tickle.md diff --git a/.changeset/better-eagles-tickle.md b/.changeset/better-eagles-tickle.md new file mode 100644 index 0000000000..b199ae21d7 --- /dev/null +++ b/.changeset/better-eagles-tickle.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Allow using `BACKSTAGE_ENVIRONMENT` for loading environment specific config files diff --git a/docs/conf/index.md b/docs/conf/index.md index 659ffccfe3..7c5be174e5 100644 --- a/docs/conf/index.md +++ b/docs/conf/index.md @@ -16,10 +16,23 @@ allowing for customization. ## Supplying Configuration Configuration is stored in YAML files where the defaults are `app-config.yaml` -and `app-config.local.yaml` for local overrides. Other sets of files can by -loaded by passing `--config ` flags. The configuration files themselves -contain plain YAML, but with support for loading in data and secrets from -various sources using for example `$env` and `$file` keys. +and `app-config.local.yaml` for local overrides. Additionally, it is possible +to define environment based configuration files with `BACKSTAGE_ENVIRONMENT` +environment variable, which will load `app-config..yaml`. + +Loading order of these files is as follows: + +1. `app-config.yaml` +2. `app-config..yaml` +3. `app-config.local.yaml` + +Other sets of files can by loaded by passing `--config ` flags. +Read more about the configuration loading order in the +[Configuration Files](./writing.md#configuration-files) section. + +The configuration files themselves contain plain YAML, but with support for +loading in data and secrets from various sources using for example +`$env` and `$file` keys. It is also possible to supply configuration through environment variables, for example `APP_CONFIG_app_baseUrl=https://staging.example.com`. However these diff --git a/packages/config-loader/src/sources/ConfigSources.test.ts b/packages/config-loader/src/sources/ConfigSources.test.ts index e19ac1d51f..9b2932e439 100644 --- a/packages/config-loader/src/sources/ConfigSources.test.ts +++ b/packages/config-loader/src/sources/ConfigSources.test.ts @@ -89,6 +89,18 @@ describe('ConfigSources', () => { { name: 'FileConfigSource', path: `${root}app-config.yaml` }, { name: 'FileConfigSource', path: `${root}app-config.local.yaml` }, ]); + + process.env = Object.assign(process.env, { BACKSTAGE_ENVIRONMENT: 'test' }); + expect( + mergeSources( + ConfigSources.defaultForTargets({ rootDir: '/', targets: [] }), + ), + ).toEqual([ + { name: 'FileConfigSource', path: `${root}app-config.yaml` }, + { name: 'FileConfigSource', path: `${root}app-config.test.yaml` }, + { name: 'FileConfigSource', path: `${root}app-config.local.yaml` }, + ]); + fsSpy.mockRestore(); expect( diff --git a/packages/config-loader/src/sources/ConfigSources.ts b/packages/config-loader/src/sources/ConfigSources.ts index 37c1c6b087..0d958bb81d 100644 --- a/packages/config-loader/src/sources/ConfigSources.ts +++ b/packages/config-loader/src/sources/ConfigSources.ts @@ -182,6 +182,10 @@ export class ConfigSources { if (argSources.length === 0) { const defaultPath = resolvePath(rootDir, 'app-config.yaml'); const localPath = resolvePath(rootDir, 'app-config.local.yaml'); + const envPath = resolvePath( + rootDir, + `app-config.${process.env.BACKSTAGE_ENVIRONMENT}.yaml`, + ); const alwaysIncludeDefaultConfigSource = !options.allowMissingDefaultConfig; @@ -195,6 +199,16 @@ export class ConfigSources { ); } + if (process.env.BACKSTAGE_ENVIRONMENT && fs.pathExistsSync(envPath)) { + argSources.push( + FileConfigSource.create({ + watch: options.watch, + path: envPath, + substitutionFunc: options.substitutionFunc, + }), + ); + } + if (fs.pathExistsSync(localPath)) { argSources.push( FileConfigSource.create({ From 4ce58318da0bd4b64939d9ed348102a418b2bf40 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Fri, 1 Aug 2025 14:47:56 +0200 Subject: [PATCH 011/233] fix(techdocs): support dompurify 3.2.6 Element.tagName is uppercased, see https://developer.mozilla.org/fr/docs/Web/API/Element/tagName Signed-off-by: Gabriel Dugny --- .changeset/stupid-areas-share.md | 5 +++++ plugins/techdocs/src/reader/transformers/html/transformer.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/stupid-areas-share.md diff --git a/.changeset/stupid-areas-share.md b/.changeset/stupid-areas-share.md new file mode 100644 index 0000000000..2238f9d942 --- /dev/null +++ b/.changeset/stupid-areas-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Support Techdocs redirect with dompurify 3.2.6+ diff --git a/plugins/techdocs/src/reader/transformers/html/transformer.ts b/plugins/techdocs/src/reader/transformers/html/transformer.ts index 7c341c783e..d4b31a34e5 100644 --- a/plugins/techdocs/src/reader/transformers/html/transformer.ts +++ b/plugins/techdocs/src/reader/transformers/html/transformer.ts @@ -65,7 +65,7 @@ export const useSanitizerTransformer = (): Transformer => { // Only allow http-equiv and content attributes on meta tags. They are required for the redirect feature. DOMPurify.addHook('uponSanitizeAttribute', (currNode, data) => { - if (currNode.tagName !== 'meta') { + if (currNode.tagName !== 'META') { if (data.attrName === 'http-equiv' || data.attrName === 'content') { currNode.removeAttribute(data.attrName); } From 067fdcd0a5b6b63a689f9fe4fa322c690ed263fb Mon Sep 17 00:00:00 2001 From: Joshua Rogers Date: Tue, 5 Aug 2025 11:18:09 +0200 Subject: [PATCH 012/233] techdocs: Disallow javascript URLs Signed-off-by: Joshua Rogers --- .changeset/great-adults-stare.md | 5 ++++ .../transformers/rewriteDocLinks.test.ts | 27 +++++++++++++++++++ .../reader/transformers/rewriteDocLinks.ts | 11 ++++++++ 3 files changed, 43 insertions(+) create mode 100644 .changeset/great-adults-stare.md diff --git a/.changeset/great-adults-stare.md b/.changeset/great-adults-stare.md new file mode 100644 index 0000000000..05d196ac62 --- /dev/null +++ b/.changeset/great-adults-stare.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': minor +--- + +Ensure that techdocs rewritten URLs do not contain potentially dangerous javascript: URLs. diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts index 66fc5f29c1..864129164c 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts @@ -71,6 +71,33 @@ describe('rewriteDocLinks', () => { expect(getSample(shadowDom, 'a', 'href')).toEqual([]); expect(shadowDom.innerHTML).toContain(expectedText); }); + + it('should rewrite javascript hrefs as text', async () => { + const samples: Array<[string, string]> = [ + // eslint-disable-next-line no-script-url + ['javascript:alert(1)', 'JS 1'], + [' javascript:alert(2)', 'JS 2 (leading space)'], + ['\n\tjavascript:alert(3)', 'JS 3 (whitespace)'], + // eslint-disable-next-line no-script-url + ['JaVaScRiPt:alert(4)', 'JS 4 (mixed case)'], + ['javascript:alert(5)', 'JS 5 (entity-encoded colon)'], + ]; + + const html = samples + .map(([href, text]) => `${text}`) + .join('\n'); + + const shadowDom = await createTestShadowDom(html, { + preTransformers: [rewriteDocLinks()], + postTransformers: [], + }); + + // There should be no tags, but the link text should remain. + expect(getSample(shadowDom, 'a', 'href')).toEqual([]); + for (const [, text] of samples) { + expect(shadowDom.innerHTML).toContain(text); + } + }); }); describe('normalizeUrl', () => { diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts index e43dda1815..a1005fe1d6 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts @@ -16,6 +16,11 @@ import type { Transformer } from './transformer'; +// See https://github.com/facebook/react/blob/f0cf832e1d0c8544c36aa8b310960885a11a847c/packages/react-dom-bindings/src/shared/sanitizeURL.js +const scriptProtocolPattern = + // eslint-disable-next-line no-control-regex + /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i; + export const rewriteDocLinks = (): Transformer => { return dom => { const updateDom = ( @@ -33,6 +38,12 @@ export const rewriteDocLinks = (): Transformer => { } try { + if (scriptProtocolPattern.test(elemAttribute)) { + throw new TypeError( + `Invalid location ref '${elemAttribute}', target is a javascript: URL`, + ); + } + const normalizedWindowLocation = normalizeUrl( window.location.href, ); From b321dd90406223c4b8a05f462bab9b02c560c3fd Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Mon, 11 Aug 2025 14:46:51 +0300 Subject: [PATCH 013/233] feat: add support for local env specific config files Signed-off-by: Hellgren Heikki --- docs/conf/index.md | 1 + .../src/sources/ConfigSources.test.ts | 1 + .../config-loader/src/sources/ConfigSources.ts | 17 +++++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/docs/conf/index.md b/docs/conf/index.md index 7c5be174e5..0b2240e2de 100644 --- a/docs/conf/index.md +++ b/docs/conf/index.md @@ -25,6 +25,7 @@ Loading order of these files is as follows: 1. `app-config.yaml` 2. `app-config..yaml` 3. `app-config.local.yaml` +4. `app-config..local.yaml` Other sets of files can by loaded by passing `--config ` flags. Read more about the configuration loading order in the diff --git a/packages/config-loader/src/sources/ConfigSources.test.ts b/packages/config-loader/src/sources/ConfigSources.test.ts index 9b2932e439..da56f5ccf9 100644 --- a/packages/config-loader/src/sources/ConfigSources.test.ts +++ b/packages/config-loader/src/sources/ConfigSources.test.ts @@ -99,6 +99,7 @@ describe('ConfigSources', () => { { name: 'FileConfigSource', path: `${root}app-config.yaml` }, { name: 'FileConfigSource', path: `${root}app-config.test.yaml` }, { name: 'FileConfigSource', path: `${root}app-config.local.yaml` }, + { name: 'FileConfigSource', path: `${root}app-config.test.local.yaml` }, ]); fsSpy.mockRestore(); diff --git a/packages/config-loader/src/sources/ConfigSources.ts b/packages/config-loader/src/sources/ConfigSources.ts index 0d958bb81d..1dacdc4752 100644 --- a/packages/config-loader/src/sources/ConfigSources.ts +++ b/packages/config-loader/src/sources/ConfigSources.ts @@ -186,6 +186,10 @@ export class ConfigSources { rootDir, `app-config.${process.env.BACKSTAGE_ENVIRONMENT}.yaml`, ); + const envLocalPath = resolvePath( + rootDir, + `app-config.${process.env.BACKSTAGE_ENVIRONMENT}.local.yaml`, + ); const alwaysIncludeDefaultConfigSource = !options.allowMissingDefaultConfig; @@ -218,6 +222,19 @@ export class ConfigSources { }), ); } + + if ( + process.env.BACKSTAGE_ENVIRONMENT && + fs.pathExistsSync(envLocalPath) + ) { + argSources.push( + FileConfigSource.create({ + watch: options.watch, + path: envLocalPath, + substitutionFunc: options.substitutionFunc, + }), + ); + } } return this.merge(argSources); From f244e61d206088e732b9c0a91812197b36e4ad10 Mon Sep 17 00:00:00 2001 From: Thomas Cardonne Date: Sat, 2 Aug 2025 01:08:02 +0200 Subject: [PATCH 014/233] feat(backend-defaults): add backend.logger configuration options Signed-off-by: Thomas Cardonne --- .changeset/silly-horses-share.md | 9 ++ .../core-services/root-logger.md | 45 +++++++++ packages/backend-defaults/config.d.ts | 62 ++++++++++++- .../backend-defaults/report-rootLogger.api.md | 19 ++++ .../rootLogger/WinstonLogger.test.ts | 52 +++++++++++ .../entrypoints/rootLogger/WinstonLogger.ts | 60 +++++++++++- .../src/entrypoints/rootLogger/config.test.ts | 75 +++++++++++++++ .../src/entrypoints/rootLogger/config.ts | 52 +++++++++++ .../src/entrypoints/rootLogger/index.ts | 4 + .../rootLoggerServiceFactory.test.ts | 72 +++++++++++++++ .../rootLogger/rootLoggerServiceFactory.ts | 17 +++- .../src/entrypoints/rootLogger/types.ts | 40 ++++++++ .../src/entrypoints/rootLogger/utils.test.ts | 92 +++++++++++++++++++ .../src/entrypoints/rootLogger/utils.ts | 78 ++++++++++++++++ 14 files changed, 670 insertions(+), 7 deletions(-) create mode 100644 .changeset/silly-horses-share.md create mode 100644 packages/backend-defaults/src/entrypoints/rootLogger/config.test.ts create mode 100644 packages/backend-defaults/src/entrypoints/rootLogger/config.ts create mode 100644 packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.test.ts create mode 100644 packages/backend-defaults/src/entrypoints/rootLogger/types.ts create mode 100644 packages/backend-defaults/src/entrypoints/rootLogger/utils.test.ts create mode 100644 packages/backend-defaults/src/entrypoints/rootLogger/utils.ts diff --git a/.changeset/silly-horses-share.md b/.changeset/silly-horses-share.md new file mode 100644 index 0000000000..969b337d6b --- /dev/null +++ b/.changeset/silly-horses-share.md @@ -0,0 +1,9 @@ +--- +'@backstage/backend-defaults': patch +--- + +Add `backend.logger` config options to configure the `RootLoggerService`. + +Read more about the new configuration options in the +[Root Logger Service](https://backstage.io/docs/backend-system/core-services/root-logger/) +documentation. diff --git a/docs/backend-system/core-services/root-logger.md b/docs/backend-system/core-services/root-logger.md index 36bb276920..9ab4473244 100644 --- a/docs/backend-system/core-services/root-logger.md +++ b/docs/backend-system/core-services/root-logger.md @@ -13,6 +13,51 @@ If you want to override the implementation for logging across all of the backend ## Configuring the service +The Root Logger Service can be configured with the `backend.logger` section of your `app-config.yaml`. + +The following parameters are available: + +- `level` (string, optional): Sets the global log level. Possible values are 'debug', 'info', 'warn', or 'error'. Only messages at or above this level will be logged. This can also be set via the `LOG_LEVEL` environment variable, which takes precedence. Defaults to 'info'. + +- `meta` (object, optional): Additional metadata to include with every log entry. + +- `overrides` (array, optional): Allows to specify logger overrides for specific plugins or messages. Each override can match on plugin names, message patterns, or any field contained in the log, and set a custom log level for those matches. + +Log level overrides are useful for controlling the volume of logs generated in Backstage. +They allow you to apply a global log level, `info` for example, while setting a stricter level, such as `warn`, for specific verbose plugins. + +The reverse is also possible: you can set a global log level of `warn` while enabling a more detailed level, such as `debug`, for certain logs. + +Example: + +```yaml +backend: + logger: + meta: + env: prod # Every log message will have `env="prod"` + + level: info # Set the global log level to info (the default) + + overrides: + # Set the log level to 'debug' for the catalog plugin logs + - matchers: + plugin: catalog + level: debug + + # Ignore 'info' incoming HTTP requests logs from the rootHttpRouter service + - matchers: + service: rootHttpRouter + type: incomingRequest + level: warn + + # Ignore logs starting with "Task worker starting", unless they're warnings or errors + - matchers: + message: ['/^Task worker starting/'] + level: warn +``` + +## Overriding the service + The following example is how you can override the root logger service to add additional metadata to all log lines. ```ts diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts index 167a8935f9..76975662e3 100644 --- a/packages/backend-defaults/config.d.ts +++ b/packages/backend-defaults/config.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { HumanDuration } from '@backstage/types'; +import { HumanDuration, JsonObject } from '@backstage/types'; export interface Config { app: { @@ -790,6 +790,66 @@ export interface Config { headers?: { [name: string]: string }; }; + /** + * Options to configure the default RootLoggerService. + */ + logger?: { + /** + * Configures the global log level for messages. + * + * This can also be configured using the LOG_LEVEL environment variable, which + * takes precedence over this configuration. + * + * Defaults to 'info'. + */ + level?: 'debug' | 'info' | 'warn' | 'error'; + + /** + * Additional metadata to include with every log entry. + */ + meta?: JsonObject; + + /** + * List of logger overrides. + * + * Can be used to configure a different level for logs matching certain criterias. + * For example, it can be used to ignore 'info' logs of given plugins. + * + * @example + * + * ```yaml + * logger: + * level: info + * overrides: + * # For catalog and auth plugins, messages less important than 'warn' will be ignored. + * - matchers: + * plugin: [catalog, auth] + * level: warn + * # Ignore all messages that starts with 'Forget' + * - matchers: + * message: '/^Forget/' + * level: warn + * ``` + */ + overrides?: Array<{ + /** + * Conditions that must be met to override the log level. + * + * A matcher can be: + * + * - A string (exact match or regex pattern delimited by slashes, e.g. `/pattern/`) + * - A non-string value (compared by strict equality) + * - An array of matchers (returns true if any matcher matches) + */ + matchers: JsonObject; + + /** + * Log level to use for matched entries. + */ + level: 'debug' | 'info' | 'warn' | 'error'; + }>; + }; + /** * Rate limiting options. Defining this as `true` will enable rate limiting with default values. */ diff --git a/packages/backend-defaults/report-rootLogger.api.md b/packages/backend-defaults/report-rootLogger.api.md index b353039c03..aa27a366e3 100644 --- a/packages/backend-defaults/report-rootLogger.api.md +++ b/packages/backend-defaults/report-rootLogger.api.md @@ -3,8 +3,10 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { config } from 'winston'; import { Format } from 'logform'; import { JsonObject } from '@backstage/types'; +import { JsonValue } from '@backstage/types'; import { LoggerService } from '@backstage/backend-plugin-api'; import { RootLoggerService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; @@ -31,14 +33,31 @@ export class WinstonLogger implements RootLoggerService { error(message: string, meta?: JsonObject): void; // (undocumented) info(message: string, meta?: JsonObject): void; + static logLevelFilter(defaultLogLevel: keyof config.NpmConfigSetLevels): { + format: Format; + setOverrides: (overrides: WinstonLoggerLevelOverride[]) => void; + }; static redacter(): { format: Format; add: (redactions: Iterable) => void; }; // (undocumented) + setLevelOverrides(overrides: WinstonLoggerLevelOverride[]): void; + // (undocumented) warn(message: string, meta?: JsonObject): void; } +// @public (undocumented) +export type WinstonLoggerLevelOverride = { + matchers: WinstonLoggerLevelOverrideMatchers; + level: string; +}; + +// @public (undocumented) +export type WinstonLoggerLevelOverrideMatchers = { + [key: string]: JsonValue | undefined; +}; + // @public (undocumented) export interface WinstonLoggerOptions { // (undocumented) diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.test.ts b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.test.ts index 7208c22d39..8fc35a6e6f 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.test.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.test.ts @@ -118,4 +118,56 @@ describe('WinstonLogger', () => { add([null as any, undefined as any, 'valid-secret']); }).not.toThrow(); }); + + it('should filter logs below the default log level', () => { + const mockTransport = new Transport({ + log: jest.fn(), + logv: jest.fn(), + }); + + const logger = WinstonLogger.create({ + level: 'warn', + format: format.json(), + transports: [mockTransport], + }); + + logger.debug('debug log'); + + expect(mockTransport.log).not.toHaveBeenCalled(); + }); + + it('should not filter logs below the default log level with an override', () => { + const mockTransport = new Transport({ + log: jest.fn(), + logv: jest.fn(), + }); + + const logger = WinstonLogger.create({ + level: 'warn', + format: format.json(), + transports: [mockTransport], + }); + + logger.setLevelOverrides([ + { + matchers: { + plugin: 'catalog', + }, + level: 'debug', + }, + ]); + + logger.debug('debug log', { plugin: 'catalog' }); + + expect(mockTransport.log).toHaveBeenCalledWith( + expect.objectContaining({ + [MESSAGE]: JSON.stringify({ + level: 'debug', + message: 'debug log', + plugin: 'catalog', + }), + }), + expect.any(Function), + ); + }); }); diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts index 1b6b9db900..90f3042e33 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts @@ -26,9 +26,12 @@ import { createLogger, transports, transport as Transport, + config as winstonConfig, } from 'winston'; import { MESSAGE } from 'triple-beam'; import { escapeRegExp } from '../../lib/escapeRegExp'; +import { winstonLevels, WinstonLoggerLevelOverride } from './types'; +import { isLogMatching } from './utils'; /** * @public @@ -48,20 +51,27 @@ export interface WinstonLoggerOptions { export class WinstonLogger implements RootLoggerService { #winston: Logger; #addRedactions?: (redactions: Iterable) => void; + #setLevelOverrides?: (overrides: WinstonLoggerLevelOverride[]) => void; /** * Creates a {@link WinstonLogger} instance. */ static create(options: WinstonLoggerOptions): WinstonLogger { + const defaultLogLevel = process.env.LOG_LEVEL || options.level || 'info'; + const redacter = WinstonLogger.redacter(); + const logLevelFilter = WinstonLogger.logLevelFilter(defaultLogLevel); + const defaultFormatter = process.env.NODE_ENV === 'production' ? format.json() : WinstonLogger.colorFormat(); let logger = createLogger({ - level: process.env.LOG_LEVEL || options.level || 'info', + // Lowest level possible as we let the logLevelFilter do the filtering + level: 'silly', format: format.combine( + logLevelFilter.format, options.format ?? defaultFormatter, redacter.format, ), @@ -72,7 +82,7 @@ export class WinstonLogger implements RootLoggerService { logger = logger.child(options.meta); } - return new WinstonLogger(logger, redacter.add); + return new WinstonLogger(logger, redacter.add, logLevelFilter.setOverrides); } /** @@ -169,12 +179,54 @@ export class WinstonLogger implements RootLoggerService { ); } + /** + * Formatter that filters log levels using overrides, falling back to the default level when no criteria match. + */ + static logLevelFilter( + defaultLogLevel: keyof winstonConfig.NpmConfigSetLevels, + ): { + format: Format; + setOverrides: (overrides: WinstonLoggerLevelOverride[]) => void; + } { + const overrides: WinstonLoggerLevelOverride[] = []; + + return { + format: format(log => { + for (const override of overrides) { + if (isLogMatching(log, override.matchers)) { + // Discard the log if the log level is below the override + // eg, if the override level is 'warn' (1) and the log is 'debug' (5) + if (winstonLevels[log.level] > winstonLevels[override.level]) { + return false; + } + + return log; + } + } + + // Ignore logs that are below the global level + // eg, if the global level is 'warn' (1) and the log level is 'debug' (5) + if (winstonLevels[log.level] > winstonLevels[defaultLogLevel]) { + return false; + } + + return log; + })(), + setOverrides: newOverrides => { + // Replace the content while preserving the reference + overrides.splice(0, overrides.length, ...newOverrides); + }, + }; + } + private constructor( winston: Logger, addRedactions?: (redactions: Iterable) => void, + setLevelOverrides?: (overrides: WinstonLoggerLevelOverride[]) => void, ) { this.#winston = winston; this.#addRedactions = addRedactions; + this.#setLevelOverrides = setLevelOverrides; } error(message: string, meta?: JsonObject): void { @@ -200,4 +252,8 @@ export class WinstonLogger implements RootLoggerService { addRedactions(redactions: Iterable) { this.#addRedactions?.(redactions); } + + setLevelOverrides(overrides: WinstonLoggerLevelOverride[]) { + this.#setLevelOverrides?.(overrides); + } } diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/config.test.ts b/packages/backend-defaults/src/entrypoints/rootLogger/config.test.ts new file mode 100644 index 0000000000..6fde1d3ec2 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLogger/config.test.ts @@ -0,0 +1,75 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { mockServices } from '@backstage/backend-test-utils'; +import { getRootLoggerConfig } from './config'; + +describe('getRootLoggerConfig', () => { + it('should load the configuration without throwing', () => { + const config = { + backend: { + logger: { + level: 'info', + meta: { + env: 'prod', + }, + overrides: [ + { + matchers: { + plugin: 'catalog', + }, + level: 'warn', + }, + ], + }, + }, + }; + + expect(() => + getRootLoggerConfig( + mockServices.rootConfig({ + data: config, + }), + ), + ).not.toThrow(); + }); + + it('should throw if an override is using an invalid level', () => { + const config = { + backend: { + logger: { + level: 'info', + meta: { + env: 'prod', + }, + overrides: [ + { + matchers: { + plugin: 'catalog', + }, + level: 'invalid', + }, + ], + }, + }, + }; + + expect(() => + getRootLoggerConfig(mockServices.rootConfig({ data: config })), + ).toThrow( + "Invalid config at backend.logger.overrides[0].level, 'invalid' is not a valid Winston npm logging level", + ); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/config.ts b/packages/backend-defaults/src/entrypoints/rootLogger/config.ts new file mode 100644 index 0000000000..4a1f9e1446 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLogger/config.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { RootConfigService } from '@backstage/backend-plugin-api'; +import { JsonObject } from '@backstage/types'; +import { RootLoggerConfig, winstonLevels } from './types'; + +export const getRootLoggerConfig = ( + config: RootConfigService, +): RootLoggerConfig => { + const level = config.getOptionalString('backend.logger.level'); + const meta = config + .getOptionalConfig('backend.logger.meta') + ?.get(); + + const overridesConfig = config.getOptionalConfigArray( + 'backend.logger.overrides', + ); + const overrides = overridesConfig?.map((override, i) => { + const overrideLevel = override.getString('level'); + if (winstonLevels[overrideLevel] === undefined) { + throw new Error( + `Invalid config at backend.logger.overrides[${i}].level, '${overrideLevel}' is not a valid Winston npm logging level`, + ); + } + + const matchers = override.getConfig('matchers').get(); + + return { + matchers, + level: overrideLevel, + }; + }); + + return { + meta, + level, + overrides, + }; +}; diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/index.ts b/packages/backend-defaults/src/entrypoints/rootLogger/index.ts index 96ed402520..676c1f5398 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/index.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/index.ts @@ -16,3 +16,7 @@ export { rootLoggerServiceFactory } from './rootLoggerServiceFactory'; export { WinstonLogger, type WinstonLoggerOptions } from './WinstonLogger'; +export { + type WinstonLoggerLevelOverride, + type WinstonLoggerLevelOverrideMatchers, +} from './types'; diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.test.ts new file mode 100644 index 0000000000..4b807aafc8 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + mockServices, + ServiceFactoryTester, +} from '@backstage/backend-test-utils'; +import { rootLoggerServiceFactory } from './rootLoggerServiceFactory'; + +import { WinstonLogger } from './WinstonLogger'; + +describe('rootLoggerServiceFactory', () => { + beforeEach(() => { + jest.spyOn(WinstonLogger, 'create'); + }); + + it('should create WinstonLogger with defaults', async () => { + await ServiceFactoryTester.from(rootLoggerServiceFactory, { + dependencies: [mockServices.rootConfig.factory()], + }).getSubject(); + + expect(WinstonLogger.create).toHaveBeenCalledWith({ + level: 'info', + meta: { + service: 'backstage', + }, + format: expect.anything(), + transports: expect.anything(), + }); + }); + + it('should create WinstonLogger from config', async () => { + await ServiceFactoryTester.from(rootLoggerServiceFactory, { + dependencies: [ + mockServices.rootConfig.factory({ + data: { + backend: { + logger: { + meta: { + env: 'test', + }, + level: 'warn', + }, + }, + }, + }), + ], + }).getSubject(); + + expect(WinstonLogger.create).toHaveBeenCalledWith({ + level: 'warn', + meta: { + service: 'backstage', + env: 'test', + }, + format: expect.anything(), + transports: expect.anything(), + }); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.ts index 5a4427cf5f..abd81e575b 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.ts @@ -15,12 +15,13 @@ */ import { - createServiceFactory, coreServices, + createServiceFactory, } from '@backstage/backend-plugin-api'; -import { transports, format } from 'winston'; -import { WinstonLogger } from '../rootLogger/WinstonLogger'; +import { format, transports } from 'winston'; import { createConfigSecretEnumerator } from '../rootConfig/createConfigSecretEnumerator'; +import { WinstonLogger } from '../rootLogger/WinstonLogger'; +import { getRootLoggerConfig } from './config'; /** * Root-level logging. @@ -37,11 +38,14 @@ export const rootLoggerServiceFactory = createServiceFactory({ config: coreServices.rootConfig, }, async factory({ config }) { + const rootLoggerConfig = getRootLoggerConfig(config); + const logger = WinstonLogger.create({ meta: { service: 'backstage', + ...rootLoggerConfig.meta, }, - level: process.env.LOG_LEVEL || 'info', + level: process.env.LOG_LEVEL || rootLoggerConfig.level || 'info', format: process.env.NODE_ENV === 'production' ? format.json() @@ -53,6 +57,11 @@ export const rootLoggerServiceFactory = createServiceFactory({ logger.addRedactions(secretEnumerator(config)); config.subscribe?.(() => logger.addRedactions(secretEnumerator(config))); + logger.setLevelOverrides(rootLoggerConfig.overrides ?? []); + config.subscribe?.(() => + logger.setLevelOverrides(getRootLoggerConfig(config).overrides ?? []), + ); + return logger; }, }); diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/types.ts b/packages/backend-defaults/src/entrypoints/rootLogger/types.ts new file mode 100644 index 0000000000..b1f6fcc5b3 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLogger/types.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { JsonObject, JsonValue } from '@backstage/types'; +import { config as winstonConfig } from 'winston'; + +/** + * @public + */ +export type WinstonLoggerLevelOverrideMatchers = { + [key: string]: JsonValue | undefined; +}; + +/** + * @public + */ +export type WinstonLoggerLevelOverride = { + matchers: WinstonLoggerLevelOverrideMatchers; + level: string; +}; + +export type RootLoggerConfig = { + level?: string; + meta?: JsonObject; + overrides?: WinstonLoggerLevelOverride[]; +}; + +export const winstonLevels = winstonConfig.npm.levels; diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/utils.test.ts b/packages/backend-defaults/src/entrypoints/rootLogger/utils.test.ts new file mode 100644 index 0000000000..c60414cfc3 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLogger/utils.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { isLogMatching } from './utils'; + +describe('isLogMatching', () => { + const log = { + level: 'info', + message: 'This is a simple log from the catalog plugin', + plugin: 'catalog', + status: 200, + action: 'read', + }; + + it('should match with a simple matcher', () => { + expect( + isLogMatching(log, { + plugin: 'catalog', + }), + ).toEqual(true); + }); + + it('should not match with a simple matcher', () => { + expect( + isLogMatching(log, { + plugin: 'search', + }), + ).toEqual(false); + }); + + it('should match with an AND matcher', () => { + expect( + isLogMatching(log, { + plugin: 'catalog', + action: 'read', + }), + ).toEqual(true); + }); + + it('should not match log with an AND matcher', () => { + expect( + isLogMatching(log, { + plugin: 'catalog', + action: 'write', + }), + ).toEqual(false); + }); + + it('should match with an OR matcher', () => { + expect( + isLogMatching(log, { + plugin: ['auth', 'catalog'], + }), + ).toEqual(true); + }); + + it('should not match log with an OR matcher', () => { + expect( + isLogMatching(log, { + plugin: ['auth', 'search'], + }), + ).toEqual(false); + }); + + it('should match log with a regex matcher', () => { + expect( + isLogMatching(log, { + message: '/This is a simple log/', + }), + ).toEqual(true); + }); + + it('should not match log with a regex matcher', () => { + expect( + isLogMatching(log, { + message: '/^simple log/', + }), + ).toEqual(false); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/utils.ts b/packages/backend-defaults/src/entrypoints/rootLogger/utils.ts new file mode 100644 index 0000000000..205a1b1520 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLogger/utils.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TransformableInfo } from 'logform'; +import { WinstonLoggerLevelOverrideMatchers } from './types'; + +/** + * Determines if a given log field matches a specified matcher. + * + * The matcher can be: + * - A string (exact match or regex pattern delimited by slashes, e.g. `/pattern/`) + * - A non-string value (compared by strict equality) + * - An array of matchers (returns true if any matcher matches) + * + * @param logField - The log field value to test for a match. + * @param matcher - The matcher or array of matchers to compare against the log field. + * @returns `true` if the log field matches the matcher, otherwise `false`. + */ +const isLogFieldMatching = ( + logField: unknown, + matcher: WinstonLoggerLevelOverrideMatchers[0], +): boolean => { + if (Array.isArray(matcher)) { + return matcher.some(m => isLogFieldMatching(logField, m)); + } + + if (typeof matcher !== 'string') { + return logField === matcher; + } + + if ( + matcher.startsWith('/') && + matcher.endsWith('/') && + typeof logField === 'string' + ) { + const regex = new RegExp(matcher.slice(1, -1)); + return regex.test(logField); + } + + return logField === matcher; +}; + +/** + * Determines whether a log entry matches all specified override matchers. + * + * Iterates over each key-matcher pair in the provided `matchers` object, + * retrieves the corresponding field from the `log` object, and checks if + * the field matches the matcher using `isLogFieldMatching`. Returns `true` + * only if all matchers are satisfied. + * + * @param log - The log entry to be checked, typically containing various log fields. + * @param matchers - An object where each key corresponds to a log field and each value is a matcher to test against that field. + * @returns `true` if the log entry matches all provided matchers, otherwise `false`. + */ +export const isLogMatching = ( + log: TransformableInfo, + matchers: WinstonLoggerLevelOverrideMatchers, +): boolean => { + const matched = Object.entries(matchers).every(([key, matcher]) => { + const logField = log[key]; + return isLogFieldMatching(logField, matcher); + }); + + return matched; +}; From ae6b606b72e7d591286ebe7a0369a46cdd63c3e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Wed, 13 Aug 2025 19:42:00 +0100 Subject: [PATCH 015/233] Add support for customizing the catalog graph relations using an API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- .changeset/dull-eagles-sin.md | 5 + packages/app/src/App.tsx | 24 ---- plugins/catalog-graph/README.md | 52 +++++-- plugins/catalog-graph/report.api.md | 50 +++++++ .../catalog-graph/src/api/CatalogGraphApi.ts | 58 ++++++++ .../src/api/DefaultCatalogGraphApi.test.ts | 61 +++++++++ .../src/api/DefaultCatalogGraphApi.ts | 74 ++++++++++ plugins/catalog-graph/src/api/index.ts | 18 +++ .../CatalogGraphCard.test.tsx | 2 + .../CatalogGraphCard/CatalogGraphCard.tsx | 3 +- .../CatalogGraphPage/CatalogGraphPage.tsx | 8 +- .../SelectedRelationsFilter.test.tsx | 80 +++++++---- .../SelectedRelationsFilter.tsx | 28 ++-- .../EntityRelationsGraph.tsx | 4 +- .../components/EntityRelationsGraph/index.ts | 2 - ...est.ts => useEntityRelationGraph.test.tsx} | 121 +++++++++++------ .../useEntityRelationGraph.ts | 6 +- ...> useEntityRelationNodesAndEdges.test.tsx} | 127 +++++++++++------- .../useEntityRelationNodesAndEdges.ts | 14 +- plugins/catalog-graph/src/hooks/index.ts | 18 +++ .../catalog-graph/src/hooks/useRelations.ts | 71 ++++++++++ plugins/catalog-graph/src/index.ts | 3 + plugins/catalog-graph/src/plugin.ts | 10 +- plugins/catalog-graph/src/types/index.ts | 18 +++ plugins/catalog-graph/src/types/relations.ts | 77 +++++++++++ 25 files changed, 750 insertions(+), 184 deletions(-) create mode 100644 .changeset/dull-eagles-sin.md create mode 100644 plugins/catalog-graph/src/api/CatalogGraphApi.ts create mode 100644 plugins/catalog-graph/src/api/DefaultCatalogGraphApi.test.ts create mode 100644 plugins/catalog-graph/src/api/DefaultCatalogGraphApi.ts create mode 100644 plugins/catalog-graph/src/api/index.ts rename plugins/catalog-graph/src/components/EntityRelationsGraph/{useEntityRelationGraph.test.ts => useEntityRelationGraph.test.tsx} (78%) rename plugins/catalog-graph/src/components/EntityRelationsGraph/{useEntityRelationNodesAndEdges.test.ts => useEntityRelationNodesAndEdges.test.tsx} (87%) create mode 100644 plugins/catalog-graph/src/hooks/index.ts create mode 100644 plugins/catalog-graph/src/hooks/useRelations.ts create mode 100644 plugins/catalog-graph/src/types/index.ts create mode 100644 plugins/catalog-graph/src/types/relations.ts diff --git a/.changeset/dull-eagles-sin.md b/.changeset/dull-eagles-sin.md new file mode 100644 index 0000000000..a527a9757d --- /dev/null +++ b/.changeset/dull-eagles-sin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-graph': patch +--- + +Support custom relations by using an API diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 5ac9052d2a..ad525420a7 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -14,18 +14,6 @@ * limitations under the License. */ -import { - RELATION_API_CONSUMED_BY, - RELATION_API_PROVIDED_BY, - RELATION_CONSUMES_API, - RELATION_DEPENDENCY_OF, - RELATION_DEPENDS_ON, - RELATION_HAS_PART, - RELATION_OWNED_BY, - RELATION_OWNER_OF, - RELATION_PART_OF, - RELATION_PROVIDES_API, -} from '@backstage/catalog-model'; import { createApp } from '@backstage/app-defaults'; import { AppRouter, FeatureFlagged, FlatRoutes } from '@backstage/core-app-api'; import { @@ -158,18 +146,6 @@ const routes = ( } diff --git a/plugins/catalog-graph/README.md b/plugins/catalog-graph/README.md index b9400e8e8b..b411d4d868 100644 --- a/plugins/catalog-graph/README.md +++ b/plugins/catalog-graph/README.md @@ -50,18 +50,6 @@ To use the catalog graph plugin, you have to add some things to your Backstage a } @@ -88,7 +76,7 @@ To use the catalog graph plugin, you have to add some things to your Backstage a ``` -### Customization +### Customizing the UI Copy the default implementation `DefaultRenderNode.tsx` and add more classes to the styles: @@ -151,6 +139,44 @@ Once you have your custom implementation, you can follow these steps to modify t ``` +### Custom relations + +Implementers with added custom relations can add them to the catalog graph plugin by overriding the default API. This also allows some relations to not be selected by default. + +In `packages/app/src/apis.ts`, import the api ref and create the API as: + +```ts +import { + ALL_RELATIONS, + ALL_RELATION_PAIRS, + catalogGraphApiRef, + DefaultCatalogGraphApi, +} from '@backstage/plugin-catalog-graph'; + +// ... + + createApiFactory({ + api: catalogGraphApiRef, + deps: {}, + factory: () => + new DefaultCatalogGraphApi({ + // The relations to support + knownRelations: [...ALL_RELATIONS, 'myRelationOf', 'myRelationFor'], + // The relation pairs to support + knownRelationPairs: [ + ...ALL_RELATION_PAIRS, + ['myRelationOf', 'myRelationFor'], + ], + // Select what relations to be shown by default, either by including them, + // or excluding some from all known relations: + defaultRelationTypes: { + // Don't show/select these by default + exclude: ['myRelationOf', 'myRelationFor'], + }, + }), + }), +``` + ## Development Run `yarn` in the root of this plugin to install all dependencies and then `yarn start` to run a [development version](./dev/index.tsx) of this plugin. diff --git a/plugins/catalog-graph/report.api.md b/plugins/catalog-graph/report.api.md index 25479bd9b7..fd1e403473 100644 --- a/plugins/catalog-graph/report.api.md +++ b/plugins/catalog-graph/report.api.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { DependencyGraphTypes } from '@backstage/core-components'; @@ -19,6 +20,19 @@ import { RouteRef } from '@backstage/core-plugin-api'; // @public export const ALL_RELATION_PAIRS: RelationPairs; +// @public +export const ALL_RELATIONS: string[]; + +// @public +export interface CatalogGraphApi { + readonly defaultRelations: string[]; + readonly knownRelationPairs: [string, string][]; + readonly knownRelations: string[]; +} + +// @public +export const catalogGraphApiRef: ApiRef; + // @public export const CatalogGraphPage: ( props: { @@ -62,6 +76,42 @@ export type CustomLabelClassKey = 'text' | 'secondary'; // @public (undocumented) export type CustomNodeClassKey = 'node' | 'text' | 'clickable'; +// @public +export class DefaultCatalogGraphApi implements CatalogGraphApi { + constructor(options?: DefaultCatalogGraphApiOptions); + // (undocumented) + readonly defaultRelations: string[]; + // (undocumented) + readonly knownRelationPairs: [string, string][]; + // (undocumented) + readonly knownRelations: string[]; +} + +// @public +export interface DefaultCatalogGraphApiOptions { + // (undocumented) + readonly defaultRelationTypes?: DefaultRelations; + // (undocumented) + readonly knownRelationPairs?: RelationPairs; + // (undocumented) + readonly knownRelations?: string[]; +} + +// @public +export type DefaultRelations = + | DefaultRelationsInclude + | DefaultRelationsExclude; + +// @public (undocumented) +export type DefaultRelationsExclude = { + exclude: string[]; +}; + +// @public (undocumented) +export type DefaultRelationsInclude = { + include: string[]; +}; + // @public export enum Direction { BOTTOM_TOP = 'BT', diff --git a/plugins/catalog-graph/src/api/CatalogGraphApi.ts b/plugins/catalog-graph/src/api/CatalogGraphApi.ts new file mode 100644 index 0000000000..33d994e0c7 --- /dev/null +++ b/plugins/catalog-graph/src/api/CatalogGraphApi.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApiRef } from '@backstage/core-plugin-api'; + +/** + * Utility API reference for the {@link CatalogGraphApi}. + * + * @public + */ +export const catalogGraphApiRef = createApiRef({ + id: 'plugin.catalog-graph.service', +}); + +/** @public */ +export type DefaultRelationsInclude = { include: string[] }; + +/** @public */ +export type DefaultRelationsExclude = { exclude: string[] }; + +/** + * Default relations. Can either be a list of relations to use by default, or + * a list of relations to _not_ use, i.e. include all other relations. + * + * @public + */ +export type DefaultRelations = + | DefaultRelationsInclude + | DefaultRelationsExclude; + +/** + * API for driving catalog imports. + * + * @public + */ +export interface CatalogGraphApi { + /** All known relations */ + readonly knownRelations: string[]; + + /** All known relation pairs */ + readonly knownRelationPairs: [string, string][]; + + /** The default relations to show in the graph */ + readonly defaultRelations: string[]; +} diff --git a/plugins/catalog-graph/src/api/DefaultCatalogGraphApi.test.ts b/plugins/catalog-graph/src/api/DefaultCatalogGraphApi.test.ts new file mode 100644 index 0000000000..63186868fd --- /dev/null +++ b/plugins/catalog-graph/src/api/DefaultCatalogGraphApi.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DefaultCatalogGraphApi } from './DefaultCatalogGraphApi'; + +describe('DefaultCatalogGraphApi', () => { + it('default config', async () => { + const { defaultRelations } = new DefaultCatalogGraphApi(); + + expect(defaultRelations.includes('')).toBe(false); + expect(defaultRelations.includes('fooRelation')).toBe(false); + expect(defaultRelations.includes('ownedBy')).toBe(true); + expect(defaultRelations.includes('hasPart')).toBe(true); + }); + + it('empty include config', async () => { + const { defaultRelations } = new DefaultCatalogGraphApi({ + defaultRelationTypes: { include: [] }, + }); + + expect(defaultRelations.includes('')).toBe(false); + expect(defaultRelations.includes('fooRelation')).toBe(false); + expect(defaultRelations.includes('ownedBy')).toBe(false); + expect(defaultRelations.includes('hasPart')).toBe(false); + }); + + it('include config', async () => { + const { defaultRelations } = new DefaultCatalogGraphApi({ + defaultRelationTypes: { include: ['ownedBy'] }, + }); + + expect(defaultRelations.includes('')).toBe(false); + expect(defaultRelations.includes('fooRelation')).toBe(false); + expect(defaultRelations.includes('ownedBy')).toBe(true); + expect(defaultRelations.includes('hasPart')).toBe(false); + }); + + it('exclude config', async () => { + const { defaultRelations } = new DefaultCatalogGraphApi({ + defaultRelationTypes: { exclude: ['ownedBy', 'ownerOf'] }, + }); + + expect(defaultRelations.includes('')).toBe(false); + expect(defaultRelations.includes('fooRelation')).toBe(false); + expect(defaultRelations.includes('ownedBy')).toBe(false); + expect(defaultRelations.includes('hasPart')).toBe(true); + }); +}); diff --git a/plugins/catalog-graph/src/api/DefaultCatalogGraphApi.ts b/plugins/catalog-graph/src/api/DefaultCatalogGraphApi.ts new file mode 100644 index 0000000000..f64dffb09b --- /dev/null +++ b/plugins/catalog-graph/src/api/DefaultCatalogGraphApi.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ALL_RELATION_PAIRS, ALL_RELATIONS, RelationPairs } from '../types'; +import { + CatalogGraphApi, + DefaultRelations, + DefaultRelationsExclude, + DefaultRelationsInclude, +} from './CatalogGraphApi'; + +/** + * Options for the {@link DefaultCatalogGraphApi}. + * + * @public + */ +export interface DefaultCatalogGraphApiOptions { + readonly knownRelations?: string[]; + readonly knownRelationPairs?: RelationPairs; + readonly defaultRelationTypes?: DefaultRelations; +} + +/** + * The default implementation of the {@link CatalogGraphApi}. + * + * @public + */ +export class DefaultCatalogGraphApi implements CatalogGraphApi { + readonly knownRelations: string[]; + readonly knownRelationPairs: [string, string][]; + readonly defaultRelations: string[]; + + constructor( + options: DefaultCatalogGraphApiOptions = { + knownRelations: ALL_RELATIONS, + knownRelationPairs: ALL_RELATION_PAIRS, + defaultRelationTypes: { exclude: [] }, + }, + ) { + this.knownRelations = options.knownRelations ?? ALL_RELATIONS; + this.knownRelationPairs = options.knownRelationPairs ?? ALL_RELATION_PAIRS; + + const defaultRelations = options.defaultRelationTypes; + + if (Array.isArray((defaultRelations as DefaultRelationsInclude).include)) { + const defaultRelationsInclude = + defaultRelations as DefaultRelationsInclude; + + this.defaultRelations = this.knownRelations.filter(rel => + defaultRelationsInclude.include.includes(rel), + ); + } else { + const defaultRelationsExclude = + defaultRelations as DefaultRelationsExclude; + + this.defaultRelations = this.knownRelations.filter( + rel => !defaultRelationsExclude.exclude.includes(rel), + ); + } + } +} diff --git a/plugins/catalog-graph/src/api/index.ts b/plugins/catalog-graph/src/api/index.ts new file mode 100644 index 0000000000..5bca33ce1a --- /dev/null +++ b/plugins/catalog-graph/src/api/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './CatalogGraphApi'; +export * from './DefaultCatalogGraphApi'; diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx index ed8069a8f4..3ae8310280 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx @@ -35,6 +35,7 @@ import { catalogGraphRouteRef } from '../../routes'; import { CatalogGraphCard } from './CatalogGraphCard'; import Button from '@material-ui/core/Button'; import { translationApiRef } from '@backstage/core-plugin-api/alpha'; +import { catalogGraphApiRef, DefaultCatalogGraphApi } from '../../api'; describe('', () => { let entity: Entity; @@ -56,6 +57,7 @@ describe('', () => { apis = TestApiRegistry.from( [catalogApiRef, catalog], [translationApiRef, mockApis.translation()], + [catalogGraphApiRef, new DefaultCatalogGraphApi()], ); wrapper = ( diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx index 733f209990..d0524668aa 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx @@ -32,7 +32,6 @@ import { MouseEvent, ReactNode, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { catalogGraphRouteRef } from '../../routes'; import { - ALL_RELATION_PAIRS, Direction, EntityNode, EntityRelationsGraph, @@ -71,7 +70,7 @@ export const CatalogGraphCard = ( const { t } = useTranslationRef(catalogGraphTranslationRef); const { variant = 'gridItem', - relationPairs = ALL_RELATION_PAIRS, + relationPairs, maxDepth = 1, unidirectional = true, mergeRelations = true, diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx index 69cb04bf3a..27d619c9c5 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx @@ -37,7 +37,6 @@ import ToggleButton from '@material-ui/lab/ToggleButton'; import { MouseEvent, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { - ALL_RELATION_PAIRS, Direction, EntityNode, EntityRelationsGraph, @@ -133,11 +132,7 @@ export const CatalogGraphPage = ( }; } & Partial, ) => { - const { - relationPairs = ALL_RELATION_PAIRS, - initialState, - entityFilter, - } = props; + const { relationPairs, initialState, entityFilter } = props; const { t } = useTranslationRef(catalogGraphTranslationRef); const navigate = useNavigate(); const classes = useStyles(); @@ -224,7 +219,6 @@ export const CatalogGraphPage = ( diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx index 057842bcdc..e97b817915 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx @@ -14,6 +14,8 @@ * limitations under the License. */ +import { PropsWithChildren } from 'react'; +import { ApiProvider } from '@backstage/core-app-api'; import { RELATION_CHILD_OF, RELATION_HAS_MEMBER, @@ -21,18 +23,34 @@ import { } from '@backstage/catalog-model'; import { waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { ALL_RELATION_PAIRS } from '../EntityRelationsGraph'; +import { ALL_RELATIONS } from '../../types'; import { SelectedRelationsFilter } from './SelectedRelationsFilter'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; +import { catalogGraphApiRef, DefaultCatalogGraphApi } from '../../api'; + +function GraphContext(props: PropsWithChildren<{}>) { + return ( + + {props.children} + + ); +} describe('', () => { test('should render current value', async () => { await renderInTestApp( - {}} - />, + + {}} + /> + , ); expect(screen.getByText(RELATION_OWNED_BY)).toBeInTheDocument(); @@ -42,11 +60,13 @@ describe('', () => { test('should select value', async () => { const onChange = jest.fn(); await renderInTestApp( - , + + + , ); await userEvent.click(screen.getByLabelText('Open')); @@ -66,16 +86,16 @@ describe('', () => { }); }); - test('should return undefined if all values are selected', async () => { + test('should return all known relations if all values are selected', async () => { const onChange = jest.fn(); await renderInTestApp( - p).filter( - r => r !== RELATION_HAS_MEMBER, - )} - onChange={onChange} - />, + + r !== RELATION_HAS_MEMBER)} + onChange={onChange} + /> + , ); await userEvent.click(screen.getByLabelText('Open')); @@ -87,18 +107,26 @@ describe('', () => { await userEvent.click(screen.getByText(RELATION_HAS_MEMBER)); await waitFor(() => { - expect(onChange).toHaveBeenCalledWith(undefined); + // Same as ALL_RELATIONS but with RELATION_HAS_MEMBER at the end + const allRelationsOrdered = [ + ...ALL_RELATIONS.filter(rel => rel !== RELATION_HAS_MEMBER), + RELATION_HAS_MEMBER, + ]; + + expect(onChange).toHaveBeenCalledWith(allRelationsOrdered); }); }); test('should return all values when cleared', async () => { const onChange = jest.fn(); await renderInTestApp( - , + + + , ); await userEvent.click(screen.getByRole('combobox')); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.tsx index 9ded3def03..aa5fa026d4 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.tsx @@ -24,9 +24,9 @@ import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import Autocomplete from '@material-ui/lab/Autocomplete'; import { useCallback, useMemo } from 'react'; -import { RelationPairs } from '../EntityRelationsGraph'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { catalogGraphTranslationRef } from '../../translation'; +import { useRelations } from '../../hooks'; /** @public */ export type SelectedRelationsFilterClassKey = 'formControl'; @@ -41,25 +41,31 @@ const useStyles = makeStyles( ); export type Props = { - relationPairs: RelationPairs; + relations?: string[]; value: string[] | undefined; onChange: (value: string[] | undefined) => void; }; -export const SelectedRelationsFilter = ({ - relationPairs, - value, - onChange, -}: Props) => { +export const SelectedRelationsFilter = (props: Props) => { + const { relations: incomingRelations, value, onChange } = props; + const classes = useStyles(); - const relations = useMemo(() => relationPairs.flat(), [relationPairs]); + + const { relations, includeRelation } = useRelations({ + relations: incomingRelations, + }); + + const defaultValue = useMemo( + () => relations.filter(rel => includeRelation(rel)), + [relations, includeRelation], + ); const { t } = useTranslationRef(catalogGraphTranslationRef); const handleChange = useCallback( (_: unknown, v: string[]) => { - onChange(relations.every(r => v.includes(r)) ? undefined : v); + onChange(v); }, - [relations, onChange], + [onChange], ); const handleEmpty = useCallback(() => { @@ -78,7 +84,7 @@ export const SelectedRelationsFilter = ({ disableCloseOnSelect aria-label={t('catalogGraphPage.selectedRelationsFilter.title')} options={relations} - value={value ?? relations} + value={value ?? defaultValue} onChange={handleChange} onBlur={handleEmpty} renderOption={(option, { selected }) => ( diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx index 933aa230be..13bebbabb7 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx @@ -30,7 +30,7 @@ import classNames from 'classnames'; import { MouseEvent, useEffect, useMemo } from 'react'; import { DefaultRenderLabel } from './DefaultRenderLabel'; import { DefaultRenderNode } from './DefaultRenderNode'; -import { ALL_RELATION_PAIRS, RelationPairs } from './relations'; +import { RelationPairs } from '../../types'; import { Direction, EntityEdge, EntityNode } from './types'; import { useEntityRelationNodesAndEdges } from './useEntityRelationNodesAndEdges'; @@ -109,7 +109,7 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => { entityFilter, direction = Direction.LEFT_RIGHT, onNodeClick, - relationPairs = ALL_RELATION_PAIRS, + relationPairs, className, zoom = 'enabled', renderNode, diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts index 7477d22f87..1c61e4abc9 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts @@ -18,8 +18,6 @@ export type { EntityRelationsGraphProps, EntityRelationsGraphClassKey, } from './EntityRelationsGraph'; -export { ALL_RELATION_PAIRS } from './relations'; -export type { RelationPairs } from './relations'; export { Direction } from './types'; export type { EntityEdgeData, diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.tsx similarity index 78% rename from plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.ts rename to plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.tsx index b2d9083180..26feb6f597 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.tsx @@ -13,16 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { PropsWithChildren } from 'react'; +import { ApiProvider } from '@backstage/core-app-api'; import { RELATION_HAS_PART, RELATION_OWNED_BY, RELATION_OWNER_OF, RELATION_PART_OF, } from '@backstage/catalog-model'; +import { TestApiRegistry } from '@backstage/test-utils'; import { renderHook } from '@testing-library/react'; import { pick } from 'lodash'; import { useEntityRelationGraph } from './useEntityRelationGraph'; import { useEntityStore as useEntityStoreMocked } from './useEntityStore'; +import { catalogGraphApiRef, DefaultCatalogGraphApi } from '../../api'; jest.mock('./useEntityStore'); @@ -30,6 +34,19 @@ const useEntityStore = useEntityStoreMocked as jest.Mock< ReturnType >; +function GraphContext(props: PropsWithChildren<{}>) { + return ( + + {props.children} + + ); +} + describe('useEntityRelationGraph', () => { const requestEntities = jest.fn(); @@ -171,8 +188,9 @@ describe('useEntityRelationGraph', () => { requestEntities, }); - const { result } = renderHook(() => - useEntityRelationGraph({ rootEntityRefs: [] }), + const { result } = renderHook( + () => useEntityRelationGraph({ rootEntityRefs: [] }), + { wrapper: GraphContext }, ); const { entities, loading, error } = result.current; @@ -190,8 +208,9 @@ describe('useEntityRelationGraph', () => { requestEntities, }); - const { result } = renderHook(() => - useEntityRelationGraph({ rootEntityRefs: [] }), + const { result } = renderHook( + () => useEntityRelationGraph({ rootEntityRefs: [] }), + { wrapper: GraphContext }, ); const { entities, loading, error } = result.current; @@ -210,8 +229,9 @@ describe('useEntityRelationGraph', () => { requestEntities, }); - const { result } = renderHook(() => - useEntityRelationGraph({ rootEntityRefs: [] }), + const { result } = renderHook( + () => useEntityRelationGraph({ rootEntityRefs: [] }), + { wrapper: GraphContext }, ); const { entities, loading, error } = result.current; @@ -222,8 +242,9 @@ describe('useEntityRelationGraph', () => { }); test('should walk relation tree', async () => { - const { result, rerender } = renderHook(() => - useEntityRelationGraph({ rootEntityRefs: ['b:d/c'] }), + const { result, rerender } = renderHook( + () => useEntityRelationGraph({ rootEntityRefs: ['b:d/c'] }), + { wrapper: GraphContext }, ); // Simulate rerendering as this is triggered automatically due to the mock @@ -256,11 +277,13 @@ describe('useEntityRelationGraph', () => { }); test('should limit max depth', async () => { - const { result, rerender } = renderHook(() => - useEntityRelationGraph({ - rootEntityRefs: ['b:d/c'], - filter: { maxDepth: 1 }, - }), + const { result, rerender } = renderHook( + () => + useEntityRelationGraph({ + rootEntityRefs: ['b:d/c'], + filter: { maxDepth: 1 }, + }), + { wrapper: GraphContext }, ); // Simulate rerendering as this is triggered automatically due to the mock @@ -277,11 +300,13 @@ describe('useEntityRelationGraph', () => { test('should update on filter change', async () => { let maxDepth: number = Number.POSITIVE_INFINITY; - const { result, rerender } = renderHook(() => - useEntityRelationGraph({ - rootEntityRefs: ['b:d/c'], - filter: { maxDepth }, - }), + const { result, rerender } = renderHook( + () => + useEntityRelationGraph({ + rootEntityRefs: ['b:d/c'], + filter: { maxDepth }, + }), + { wrapper: GraphContext }, ); // Simulate rerendering as this is triggered automatically due to the mock @@ -310,13 +335,15 @@ describe('useEntityRelationGraph', () => { }); test('should filter by relation', async () => { - const { result, rerender } = renderHook(() => - useEntityRelationGraph({ - rootEntityRefs: ['b:d/c'], - filter: { - relations: [RELATION_HAS_PART, RELATION_PART_OF], - }, - }), + const { result, rerender } = renderHook( + () => + useEntityRelationGraph({ + rootEntityRefs: ['b:d/c'], + filter: { + relations: [RELATION_HAS_PART, RELATION_PART_OF], + }, + }), + { wrapper: GraphContext }, ); // Simulate rerendering as this is triggered automatically due to the mock @@ -332,14 +359,16 @@ describe('useEntityRelationGraph', () => { }); test('should filter by kind', async () => { - const { result, rerender } = renderHook(() => - useEntityRelationGraph({ - rootEntityRefs: ['b:d/c'], - filter: { - relations: [RELATION_OWNED_BY, RELATION_OWNER_OF], - kinds: ['k'], - }, - }), + const { result, rerender } = renderHook( + () => + useEntityRelationGraph({ + rootEntityRefs: ['b:d/c'], + filter: { + relations: [RELATION_OWNED_BY, RELATION_OWNER_OF], + kinds: ['k'], + }, + }), + { wrapper: GraphContext }, ); // Simulate rerendering as this is triggered automatically due to the mock @@ -354,13 +383,15 @@ describe('useEntityRelationGraph', () => { }); test('should filter by func', async () => { - const { result, rerender } = renderHook(() => - useEntityRelationGraph({ - rootEntityRefs: ['b:d/c'], - filter: { - entityFilter: e => e.metadata.name !== 'c2', - }, - }), + const { result, rerender } = renderHook( + () => + useEntityRelationGraph({ + rootEntityRefs: ['b:d/c'], + filter: { + entityFilter: e => e.metadata.name !== 'c2', + }, + }), + { wrapper: GraphContext }, ); // Simulate rerendering as this is triggered automatically due to the mock @@ -376,10 +407,12 @@ describe('useEntityRelationGraph', () => { }); test('should support multiple roots by kind', async () => { - const { result, rerender } = renderHook(() => - useEntityRelationGraph({ - rootEntityRefs: ['b:d/c', 'b:d/c2'], - }), + const { result, rerender } = renderHook( + () => + useEntityRelationGraph({ + rootEntityRefs: ['b:d/c', 'b:d/c2'], + }), + { wrapper: GraphContext }, ); // Simulate rerendering as this is triggered automatically due to the mock diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts index 07e3085f1f..f52ac03f7e 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts @@ -17,6 +17,7 @@ import { Entity } from '@backstage/catalog-model'; import { useEffect, useMemo } from 'react'; import { useEntityStore } from './useEntityStore'; import { pickBy } from 'lodash'; +import { useRelations } from '../../hooks/useRelations'; /** * Discover the graph of entities connected by relations, starting from a set of @@ -45,6 +46,7 @@ export function useEntityRelationGraph({ error?: Error; } { const { entities, loading, error, requestEntities } = useEntityStore(); + const { includeRelation } = useRelations({ relations }); useEffect(() => { const expectedEntities = new Set([...rootEntityRefs]); @@ -74,7 +76,7 @@ export function useEntityRelationGraph({ } for (const rel of entity.relations) { if ( - (!relations || relations.includes(rel.type)) && + includeRelation(rel.type) && (!kinds || kinds.some(kind => rel.targetRef.startsWith( @@ -98,7 +100,7 @@ export function useEntityRelationGraph({ entities, rootEntityRefs, maxDepth, - relations, + includeRelation, kinds, entityFilter, requestEntities, diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.tsx similarity index 87% rename from plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.ts rename to plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.tsx index 08b718d789..4dfb612646 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { PropsWithChildren } from 'react'; import { DEFAULT_NAMESPACE, Entity, @@ -22,11 +23,14 @@ import { RELATION_PART_OF, stringifyEntityRef, } from '@backstage/catalog-model'; +import { ApiProvider } from '@backstage/core-app-api'; +import { TestApiRegistry } from '@backstage/test-utils'; import { renderHook, waitFor } from '@testing-library/react'; import { filter, keyBy } from 'lodash'; import { useEntityRelationGraph as useEntityRelationGraphMocked } from './useEntityRelationGraph'; import { useEntityRelationNodesAndEdges } from './useEntityRelationNodesAndEdges'; import { EntityNode } from './types'; +import { catalogGraphApiRef, DefaultCatalogGraphApi } from '../../api'; jest.mock('./useEntityRelationGraph'); @@ -129,6 +133,19 @@ function deprecatedProperties(entity: Entity): Partial { }; } +function GraphContext(props: PropsWithChildren<{}>) { + return ( + + {props.children} + + ); +} + describe('useEntityRelationNodesAndEdges', () => { beforeEach(() => { useEntityRelationGraph.mockImplementation(({ filter: { kinds } }) => ({ @@ -149,10 +166,12 @@ describe('useEntityRelationNodesAndEdges', () => { loading: true, }); - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c'], - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c'], + }), + { wrapper: GraphContext }, ); const { nodes, edges, loading, error } = result.current; @@ -170,10 +189,12 @@ describe('useEntityRelationNodesAndEdges', () => { error: returnError, }); - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c'], - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c'], + }), + { wrapper: GraphContext }, ); const { nodes, edges, loading, error } = result.current; @@ -185,12 +206,14 @@ describe('useEntityRelationNodesAndEdges', () => { }); test('should generate unidirectional graph with merged relations', async () => { - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c'], - unidirectional: true, - mergeRelations: true, - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c'], + unidirectional: true, + mergeRelations: true, + }), + { wrapper: GraphContext }, ); await waitFor(() => { @@ -260,12 +283,14 @@ describe('useEntityRelationNodesAndEdges', () => { }); test('should generate unidirectional graph', async () => { - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c'], - unidirectional: true, - mergeRelations: false, - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c'], + unidirectional: true, + mergeRelations: false, + }), + { wrapper: GraphContext }, ); await waitFor(() => { @@ -359,12 +384,14 @@ describe('useEntityRelationNodesAndEdges', () => { }); test('should generate bidirectional graph with merged relations', async () => { - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c'], - unidirectional: false, - mergeRelations: true, - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c'], + unidirectional: false, + mergeRelations: true, + }), + { wrapper: GraphContext }, ); await waitFor(() => { @@ -434,12 +461,14 @@ describe('useEntityRelationNodesAndEdges', () => { }); test('should generate bidirectional graph with all relations', async () => { - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c'], - unidirectional: false, - mergeRelations: false, - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c'], + unidirectional: false, + mergeRelations: false, + }), + { wrapper: GraphContext }, ); await waitFor(() => { @@ -533,10 +562,12 @@ describe('useEntityRelationNodesAndEdges', () => { }); test('should generate graph with multiple root nodes', async () => { - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c', 'b:d/c2'], - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c', 'b:d/c2'], + }), + { wrapper: GraphContext }, ); await waitFor(() => { @@ -606,11 +637,13 @@ describe('useEntityRelationNodesAndEdges', () => { }); test('should filter by relation', async () => { - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c'], - relations: [RELATION_OWNER_OF], - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c'], + relations: [RELATION_OWNER_OF], + }), + { wrapper: GraphContext }, ); await waitFor(() => { @@ -662,11 +695,13 @@ describe('useEntityRelationNodesAndEdges', () => { }); test('should filter by kind', async () => { - const { result } = renderHook(() => - useEntityRelationNodesAndEdges({ - rootEntityRefs: ['b:d/c'], - kinds: ['b'], - }), + const { result } = renderHook( + () => + useEntityRelationNodesAndEdges({ + rootEntityRefs: ['b:d/c'], + kinds: ['b'], + }), + { wrapper: GraphContext }, ); await waitFor(() => { diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts index 19a6dbe566..20b33ba14e 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts @@ -15,10 +15,11 @@ */ import { MouseEvent, useState } from 'react'; import useDebounce from 'react-use/esm/useDebounce'; -import { RelationPairs, ALL_RELATION_PAIRS } from './relations'; +import { RelationPairs } from '../../types'; import { EntityEdge, EntityNode } from './types'; import { useEntityRelationGraph } from './useEntityRelationGraph'; import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { useRelations } from '../../hooks/useRelations'; /** * Generate nodes and edges to render the entity graph. @@ -32,7 +33,7 @@ export function useEntityRelationNodesAndEdges({ relations, entityFilter, onNodeClick, - relationPairs = ALL_RELATION_PAIRS, + relationPairs: incomingRelationPairs, }: { rootEntityRefs: string[]; maxDepth?: number; @@ -63,6 +64,11 @@ export function useEntityRelationNodesAndEdges({ }, }); + const { relationPairs, includeRelation } = useRelations({ + relations, + relationPairs: incomingRelationPairs, + }); + useDebounce( () => { if (!entities || Object.keys(entities).length === 0) { @@ -124,7 +130,7 @@ export function useEntityRelationNodesAndEdges({ return; } - if (relations && !relations.includes(rel.type)) { + if (!includeRelation(rel.type)) { return; } @@ -230,7 +236,7 @@ export function useEntityRelationNodesAndEdges({ entities, rootEntityRefs, kinds, - relations, + includeRelation, unidirectional, mergeRelations, onNodeClick, diff --git a/plugins/catalog-graph/src/hooks/index.ts b/plugins/catalog-graph/src/hooks/index.ts new file mode 100644 index 0000000000..6cc875631d --- /dev/null +++ b/plugins/catalog-graph/src/hooks/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { UseRelationsOptions, UseRelationsResult } from './useRelations'; +export { useRelations } from './useRelations'; diff --git a/plugins/catalog-graph/src/hooks/useRelations.ts b/plugins/catalog-graph/src/hooks/useRelations.ts new file mode 100644 index 0000000000..976f17735b --- /dev/null +++ b/plugins/catalog-graph/src/hooks/useRelations.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useCallback, useMemo } from 'react'; + +import { useApi } from '@backstage/core-plugin-api'; + +import { catalogGraphApiRef } from '../api'; +import { RelationPairs } from '../types'; + +export interface UseRelationsOptions { + relations?: string[]; + relationPairs?: RelationPairs; +} + +export interface UseRelationsResult { + relations: string[]; + relationPairs: RelationPairs; + defaultRelations: string[]; + includeRelation: (type: string) => boolean; +} + +/** + * Given an optional list of specific relations and/or relation pairs, + * this hook returns the relations, relation pairs and default relations (to + * show/select), falling back to the configured setting. + * It also returns a function `includeRelation` to filter whether a relation + * should be included or not. + */ +export function useRelations( + opts: UseRelationsOptions = {}, +): UseRelationsResult { + const { knownRelations, knownRelationPairs, defaultRelations } = + useApi(catalogGraphApiRef); + + const relations = opts.relations ?? knownRelations; + const relationPairs = opts.relationPairs ?? knownRelationPairs; + + const includeRelation = useCallback( + (type: string) => { + if (opts.relations) { + return opts.relations.includes(type); + } + return defaultRelations.includes(type); + }, + [opts.relations, defaultRelations], + ); + + return useMemo( + () => ({ + relations, + relationPairs, + defaultRelations, + includeRelation, + }), + [relations, relationPairs, defaultRelations, includeRelation], + ); +} diff --git a/plugins/catalog-graph/src/index.ts b/plugins/catalog-graph/src/index.ts index af81b076f2..fb12cda14e 100644 --- a/plugins/catalog-graph/src/index.ts +++ b/plugins/catalog-graph/src/index.ts @@ -23,5 +23,8 @@ export * from './components'; export { CatalogGraphPage, EntityCatalogGraphCard } from './extensions'; +export * from './api'; export { catalogGraphPlugin } from './plugin'; export { catalogGraphRouteRef } from './routes'; +export { ALL_RELATIONS, ALL_RELATION_PAIRS } from './types'; +export type { RelationPairs } from './types'; diff --git a/plugins/catalog-graph/src/plugin.ts b/plugins/catalog-graph/src/plugin.ts index 7629caabee..570283c952 100644 --- a/plugins/catalog-graph/src/plugin.ts +++ b/plugins/catalog-graph/src/plugin.ts @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createPlugin } from '@backstage/core-plugin-api'; +import { createApiFactory, createPlugin } from '@backstage/core-plugin-api'; import { catalogEntityRouteRef, catalogGraphRouteRef } from './routes'; +import { catalogGraphApiRef, DefaultCatalogGraphApi } from './api'; /** * Catalog Graph Plugin instance. @@ -28,4 +29,11 @@ export const catalogGraphPlugin = createPlugin({ externalRoutes: { catalogEntity: catalogEntityRouteRef, }, + apis: [ + createApiFactory({ + api: catalogGraphApiRef, + deps: {}, + factory: () => new DefaultCatalogGraphApi(), + }), + ], }); diff --git a/plugins/catalog-graph/src/types/index.ts b/plugins/catalog-graph/src/types/index.ts new file mode 100644 index 0000000000..63dfb5e3f2 --- /dev/null +++ b/plugins/catalog-graph/src/types/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ALL_RELATIONS, ALL_RELATION_PAIRS } from './relations'; +export type { RelationPairs } from './relations'; diff --git a/plugins/catalog-graph/src/types/relations.ts b/plugins/catalog-graph/src/types/relations.ts new file mode 100644 index 0000000000..d6a683cf4f --- /dev/null +++ b/plugins/catalog-graph/src/types/relations.ts @@ -0,0 +1,77 @@ +/* + * Copyright 2021 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 { + RELATION_API_CONSUMED_BY, + RELATION_API_PROVIDED_BY, + RELATION_CHILD_OF, + RELATION_CONSUMES_API, + RELATION_DEPENDENCY_OF, + RELATION_DEPENDS_ON, + RELATION_HAS_MEMBER, + RELATION_HAS_PART, + RELATION_MEMBER_OF, + RELATION_OWNED_BY, + RELATION_OWNER_OF, + RELATION_PARENT_OF, + RELATION_PART_OF, + RELATION_PROVIDES_API, +} from '@backstage/catalog-model'; + +/** + * A pair of two relations that describe the opposite of each other. The first + * relation is considered as the primary relation. + * + * @public + */ +export type RelationPairs = [string, string][]; + +/** + * A list of built-in relations + * + * @public + */ +export const ALL_RELATIONS: string[] = [ + RELATION_API_CONSUMED_BY, + RELATION_API_PROVIDED_BY, + RELATION_CHILD_OF, + RELATION_CONSUMES_API, + RELATION_DEPENDENCY_OF, + RELATION_DEPENDS_ON, + RELATION_HAS_MEMBER, + RELATION_HAS_PART, + RELATION_MEMBER_OF, + RELATION_OWNED_BY, + RELATION_OWNER_OF, + RELATION_PARENT_OF, + RELATION_PART_OF, + RELATION_PROVIDES_API, +]; + +/** + * A list of pairs of entity relations, used to define which relations are + * merged together and which the primary relation is. + * + * @public + */ +export const ALL_RELATION_PAIRS: RelationPairs = [ + [RELATION_OWNER_OF, RELATION_OWNED_BY], + [RELATION_CONSUMES_API, RELATION_API_CONSUMED_BY], + [RELATION_API_PROVIDED_BY, RELATION_PROVIDES_API], + [RELATION_HAS_PART, RELATION_PART_OF], + [RELATION_PARENT_OF, RELATION_CHILD_OF], + [RELATION_HAS_MEMBER, RELATION_MEMBER_OF], + [RELATION_DEPENDS_ON, RELATION_DEPENDENCY_OF], +]; From 58fc10884612869139adaa87cb2f6ceb66c7eaac Mon Sep 17 00:00:00 2001 From: Stephen Glass Date: Wed, 20 Aug 2025 23:16:24 -0400 Subject: [PATCH 016/233] Set a minimum height for scaffolder task log stream Signed-off-by: Stephen Glass --- .changeset/tangy-squids-film.md | 5 +++++ .../src/next/components/TaskLogStream/TaskLogStream.tsx | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/tangy-squids-film.md diff --git a/.changeset/tangy-squids-film.md b/.changeset/tangy-squids-film.md new file mode 100644 index 0000000000..acb5546031 --- /dev/null +++ b/.changeset/tangy-squids-film.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Fix scaffolder task log stream not having a minimum height diff --git a/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.tsx b/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.tsx index a0c4ce94cd..b88d96510e 100644 --- a/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.tsx +++ b/plugins/scaffolder-react/src/next/components/TaskLogStream/TaskLogStream.tsx @@ -21,6 +21,7 @@ const useStyles = makeStyles({ width: '100%', height: '100%', position: 'relative', + minHeight: 240, }, }); From 85c5e045c9778623644e86716dd0aa76cf670ee6 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Thu, 21 Aug 2025 16:22:44 +0100 Subject: [PATCH 017/233] catalog: fix incorrect defaultTarget in createComponentRouteRef The previous value doesn't exist in the scaffolder. I believe the logical place for this to point to is the root of the scaffolder plugin, which renders the list of templates. Signed-off-by: MT Lewis --- .changeset/warm-emus-itch.md | 5 +++++ packages/app-next/app-config.yaml | 1 - plugins/catalog/src/routes.ts | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 .changeset/warm-emus-itch.md diff --git a/.changeset/warm-emus-itch.md b/.changeset/warm-emus-itch.md new file mode 100644 index 0000000000..3275098425 --- /dev/null +++ b/.changeset/warm-emus-itch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Fix incorrect defaultTarget on `createComponentRouteRef`. diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index f83e4d4ba0..35380c961f 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -4,7 +4,6 @@ app: routes: bindings: catalog.viewTechDoc: techdocs.docRoot - catalog.createComponent: catalog-import.importPage org.catalogIndex: catalog.catalogIndex pluginOverrides: diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index 4f7f0427b1..6043ecd1a1 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -22,7 +22,7 @@ import { export const createComponentRouteRef = createExternalRouteRef({ id: 'create-component', optional: true, - defaultTarget: 'scaffolder.createComponent', + defaultTarget: 'scaffolder.root', }); export const viewTechDocRouteRef = createExternalRouteRef({ From ddf1de03f85b98be124cf7ed95e50fc5fbcfba8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 25 Aug 2025 11:10:14 +0200 Subject: [PATCH 018/233] Update .changeset/stupid-pans-flash.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/stupid-pans-flash.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/stupid-pans-flash.md b/.changeset/stupid-pans-flash.md index bc857a243a..3e8c2333dd 100644 --- a/.changeset/stupid-pans-flash.md +++ b/.changeset/stupid-pans-flash.md @@ -2,7 +2,7 @@ '@backstage/plugin-scaffolder-backend-module-github': minor --- -Adding a new scaffolder action `github:issues:create` following the reference of `github:issues:label` with dryRun testing possibility +Adding a new scaffolder action `github:issues:create` following the reference of `github:issues:label` with `dryRun` testing possibility It can be used like this From c3405dbc91b7795013091e6507819af56bc639c5 Mon Sep 17 00:00:00 2001 From: Adam Letizia Date: Mon, 25 Aug 2025 09:35:48 -0500 Subject: [PATCH 019/233] fix(scaffolder-backend): sets router max upload size to 10MB Signed-off-by: Adam Letizia --- .changeset/silent-results-stick.md | 5 +++ .../src/service/router.test.ts | 42 +++++++------------ .../scaffolder-backend/src/service/router.ts | 9 ++-- 3 files changed, 27 insertions(+), 29 deletions(-) create mode 100644 .changeset/silent-results-stick.md diff --git a/.changeset/silent-results-stick.md b/.changeset/silent-results-stick.md new file mode 100644 index 0000000000..6cb501027c --- /dev/null +++ b/.changeset/silent-results-stick.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fixed a regression that prevented uploads greater than 100KB. Uploads up to 10MB are supported again. diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 467b590bed..4144c552fb 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -1385,34 +1385,24 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ expect.anything(), ); }); - it('disallows users from seeing tasks they do not own', async () => { - const { permissions, router, taskBroker } = await createTestRouter(); - jest - .spyOn(permissions, 'authorizeConditional') - .mockImplementationOnce(async () => [ - { - conditions: { - resourceType: 'scaffolder-task', - rule: 'IS_TASK_OWNER', - params: { createdBy: ['user'] }, - }, - pluginId: 'scaffolder', - resourceType: 'scaffolder-task', - result: AuthorizeResult.CONDITIONAL, + it('allows payloads up to 10MB', async () => { + const { unwrappedRouter } = await createTestRouter(); + const mockToken = mockCredentials.user.token(); + const mockTemplate = generateMockTemplate(); + + const response = await request(unwrappedRouter) + .post('/v2/dry-run') + .set('Authorization', `Bearer ${mockToken}`) + .send({ + template: mockTemplate, + values: { + requiredParameter1: 'A'.repeat(9 * 1024 * 1024), // ~9MB + requiredParameter2: 'required-value-2', }, - ]); - const response = await request(router).get( - `/v2/tasks?createdBy=not-user`, - ); - expect(taskBroker.list).toHaveBeenCalledWith({ - filters: { createdBy: ['not-user'], status: undefined }, - order: undefined, - pagination: { limit: undefined, offset: undefined }, - permissionFilters: { key: 'created_by', values: ['user'] }, - }); + directoryContents: [], + }); + expect(response.status).toBe(200); - expect(response.body.totalTasks).toBe(0); - expect(response.body.tasks).toEqual([]); }); }); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 9315e8ccb2..8296a6003f 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -186,9 +186,12 @@ const readDuration = ( export async function createRouter( options: RouterOptions, ): Promise { - const router = await createOpenApiRouter(); - // Be generous in upload size to support a wide range of templates in dry-run mode. - router.use(express.json({ limit: '10MB' })); + const router = await createOpenApiRouter({ + middleware: [ + // Be generous in upload size to support a wide range of templates in dry-run mode. + express.json({ limit: '10MB' }), + ], + }); const { logger: parentLogger, From a0b604cb6a50765450c3675405994f94137e38fd Mon Sep 17 00:00:00 2001 From: Owen Shartle Date: Mon, 25 Aug 2025 15:39:29 -0400 Subject: [PATCH 020/233] Adding handling which checks if the current entity (the catalog entity being loaded) has an annotation for an external entity's TechDocs. If it does then we will redirect there rather than allowing a 404 (mic drop). This helps keep older URLs routing to the updated locations. Adding changesets. Adding test coverage for external TechDocs entitiy redirect. Signed-off-by: Owen Shartle --- .changeset/eighty-numbers-act.md | 5 + .changeset/hungry-carrots-grow.md | 5 + .changeset/lovely-actors-love.md | 5 + .../well-known-annotations.md | 2 +- docs/features/techdocs/FAQ.md | 4 + .../documented-component/catalog-info.yaml | 14 ++ .../docs/inner-component-docs/index.md | 5 + .../examples/documented-component/mkdocs.yml | 2 + plugins/techdocs-react/src/helpers.ts | 4 +- .../TechDocsReaderPage.test.tsx | 131 +++++++++++++++++- .../TechDocsReaderPage/TechDocsReaderPage.tsx | 98 ++++++++++--- 11 files changed, 255 insertions(+), 20 deletions(-) create mode 100644 .changeset/eighty-numbers-act.md create mode 100644 .changeset/hungry-carrots-grow.md create mode 100644 .changeset/lovely-actors-love.md create mode 100644 plugins/techdocs-backend/examples/documented-component/docs/inner-component-docs/index.md diff --git a/.changeset/eighty-numbers-act.md b/.changeset/eighty-numbers-act.md new file mode 100644 index 0000000000..183d857d92 --- /dev/null +++ b/.changeset/eighty-numbers-act.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Update to documentation regarding TechDocs redirects. diff --git a/.changeset/hungry-carrots-grow.md b/.changeset/hungry-carrots-grow.md new file mode 100644 index 0000000000..748ace8d94 --- /dev/null +++ b/.changeset/hungry-carrots-grow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': minor +--- + +Adding redirect handling for TechDocs URLs that reference entities that now reference an external entity for TechDocs. Including tests and documentation. diff --git a/.changeset/lovely-actors-love.md b/.changeset/lovely-actors-love.md new file mode 100644 index 0000000000..f645cc5b3f --- /dev/null +++ b/.changeset/lovely-actors-love.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': minor +--- + +Adding new entity that specifies an external entity in the techdocs-entity annotation. diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index cdd93b50ac..d8a2e1d232 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -126,7 +126,7 @@ metadata: The value of this annotation informs of the path to this component's TechDocs within an external entity that owns the TechDocs. In conjunction with [backstage.io/techdocs-entity](#backstageiotechdocs-entity) this allows for deep linking into the TechDocs of -another entity, not just linking to the root of another entities TechDocs. +another entity, not just linking to the root of another entity's TechDocs. ### backstage.io/view-url, backstage.io/edit-url diff --git a/docs/features/techdocs/FAQ.md b/docs/features/techdocs/FAQ.md index 117471f749..323489ddab 100644 --- a/docs/features/techdocs/FAQ.md +++ b/docs/features/techdocs/FAQ.md @@ -57,3 +57,7 @@ your `mkdocs.yml` files per If the host name of your source code hosting URL does not include `github` or `gitlab`, an `integrations` entry in your `app-config.yaml` pointed at your source code provider is also needed (only the `host` key is necessary). + +#### What happens when you navigate to a TechDocs URL for an entity uses the `backstage.io/techdocs-entity` annotation? + +If you navigate to a TechDocs URL in the format `docs/{namespace}/{kind}/{name}` for an entity that has the `backstage.io/techdocs-entity` annotation (instead of the `backstage.io/techdocs-ref` annotation), then Backstage will redirect to the TechDocs page of the entity referenced in the value of that annotation. diff --git a/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml b/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml index 6b30213a60..4d3b71eb2a 100644 --- a/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml +++ b/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml @@ -10,3 +10,17 @@ spec: type: service lifecycle: experimental owner: user:guest +--- +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: techdocs-entity-documented-component + title: Example Entity Documented By TechDocs Entity Annotation + description: A Service with TechDocs documentation via the `backstage.io/techdocs-entity` annotation. + annotations: + backstage.io/techdocs-entity: component:default/documented-component + backstage.io/techdocs-entity-path: /inner-component-docs +spec: + type: service + lifecycle: experimental + owner: user:guest \ No newline at end of file diff --git a/plugins/techdocs-backend/examples/documented-component/docs/inner-component-docs/index.md b/plugins/techdocs-backend/examples/documented-component/docs/inner-component-docs/index.md new file mode 100644 index 0000000000..8cb6498c55 --- /dev/null +++ b/plugins/techdocs-backend/examples/documented-component/docs/inner-component-docs/index.md @@ -0,0 +1,5 @@ +# Inner Component Docs + +This is a basic example of documentation within a larger suite of TechDocs that can be referenced by whatever entities necessary. It is intended as a showcase of the `backstage.io/techdocs-entity-path` annotation for linking to a "subpage" in TechDocs that are declared by another entity. + +Please review the [How-To Guides - Deep Linking Into TechDocs](../../../../../../docs/features/techdocs/how-to-guides.md#deep-linking-into-techdocs) section for more information and you can view the example usage on the "Example Entity Documented By TechDocs Entity Annotation" component in this [catalog-info.yaml](../../catalog-info.yaml) file. diff --git a/plugins/techdocs-backend/examples/documented-component/mkdocs.yml b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml index 1a159a4d12..ab97326913 100644 --- a/plugins/techdocs-backend/examples/documented-component/mkdocs.yml +++ b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml @@ -7,6 +7,8 @@ nav: - Subpage: sub-page.md - 'Code Sample': code/code-sample.md - Extensions: extensions.md + - 'Inner Component Docs': inner-component-docs/index.md plugins: - techdocs-core + \ No newline at end of file diff --git a/plugins/techdocs-react/src/helpers.ts b/plugins/techdocs-react/src/helpers.ts index e6daf99b26..aebb0b3e52 100644 --- a/plugins/techdocs-react/src/helpers.ts +++ b/plugins/techdocs-react/src/helpers.ts @@ -64,7 +64,7 @@ export function getEntityRootTechDocsPath(entity: Entity): string { /** * Build the TechDocs URL for the given entity. This helper should be used anywhere there - * is a link to an entities TechDocs. + * is a link to an entity's TechDocs. * * @public */ @@ -104,7 +104,7 @@ export const buildTechDocsURL = ( }); // Add on the external entity path to the url if one exists. This allows deep linking into another - // entities TechDocs. + // entity's TechDocs. const path = getEntityRootTechDocsPath(entity); return `${url}${path}`; diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx index 2af4915ef5..93f24e9f97 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx @@ -18,6 +18,7 @@ import { ReactNode } from 'react'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { + catalogApiRef, entityPresentationApiRef, entityRouteRef, } from '@backstage/plugin-catalog-react'; @@ -30,9 +31,10 @@ import { import { techdocsApiRef, techdocsStorageApiRef } from '../../../api'; import { rootRouteRef, rootDocsRouteRef } from '../../../routes'; +import { TECHDOCS_EXTERNAL_ANNOTATION } from '@backstage/plugin-techdocs-common'; import { TechDocsReaderPage } from './TechDocsReaderPage'; -import { Route, useParams } from 'react-router-dom'; +import { Route, useNavigate, useParams } from 'react-router-dom'; import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; import { FlatRoutes } from '@backstage/core-app-api'; @@ -94,6 +96,10 @@ const entityPresentationApiMock: jest.Mocked< }), }; +const catalogApiMock = { + getEntityByRef: jest.fn().mockResolvedValue(mockEntityMetadata), +}; + const fetchApiMock = { fetch: jest.fn().mockResolvedValue({ ok: true, @@ -114,6 +120,11 @@ jest.mock('@backstage/core-components', () => ({ Page: jest.fn(), })); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: jest.fn(), +})); + const configApi = mockApis.config({ data: { app: { baseUrl: 'http://localhost:3000' } }, }); @@ -129,6 +140,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => { [techdocsApiRef, techdocsApiMock], [techdocsStorageApiRef, techdocsStorageApiMock], [entityPresentationApiRef, entityPresentationApiMock], + [catalogApiRef, catalogApiMock], ]} > {children} @@ -143,6 +155,8 @@ const mountedRoutes = { }; describe('', () => { + const mockNavigate = jest.fn(); + beforeEach(() => { getEntityMetadata.mockResolvedValue(mockEntityMetadata); getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata); @@ -150,6 +164,8 @@ describe('', () => { // Expires in 10 minutes expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString(), }); + + (useNavigate as jest.Mock).mockReturnValue(mockNavigate); }); afterEach(() => { @@ -285,4 +301,117 @@ describe('', () => { expect(text).toHaveStyle('fontFamily: Comic Sans MS'); }); + + describe('external TechDocs redirect', () => { + beforeEach(() => { + mockNavigate.mockClear(); + catalogApiMock.getEntityByRef.mockReset(); + catalogApiMock.getEntityByRef.mockResolvedValue(mockEntityMetadata); + }); + + it('should navigate to external URL when entity has external techdocs annotation', async () => { + const mockEntityWithExternalAnnotation = { + ...mockEntityMetadata, + metadata: { + ...mockEntityMetadata.metadata, + annotations: { + [TECHDOCS_EXTERNAL_ANNOTATION]: + 'component:external-namespace/external-docs', + }, + }, + }; + + catalogApiMock.getEntityByRef.mockResolvedValue( + mockEntityWithExternalAnnotation, + ); + + await renderInTestApp( + + + , + { + mountedRoutes, + }, + ); + + expect(mockNavigate).toHaveBeenCalledWith( + '/docs/external-namespace/component/external-docs', + { replace: true }, + ); + }); + + it('should render normally when entity has no external techdocs annotation', async () => { + const mockEntityWithoutExternalAnnotation = { + ...mockEntityMetadata, + metadata: { + ...mockEntityMetadata.metadata, + annotations: undefined, + }, + }; + + catalogApiMock.getEntityByRef.mockResolvedValue( + mockEntityWithoutExternalAnnotation, + ); + + const rendered = await renderInTestApp( + + + , + { + mountedRoutes, + }, + ); + + expect(rendered.container.querySelector('header')).toBeInTheDocument(); + expect(rendered.container.querySelector('article')).toBeInTheDocument(); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + + it('should render normally when entity has external annotation but no value', async () => { + const mockEntityWithEmptyExternalAnnotation = { + ...mockEntityMetadata, + metadata: { + ...mockEntityMetadata.metadata, + annotations: { + [TECHDOCS_EXTERNAL_ANNOTATION]: '', + }, + }, + }; + + catalogApiMock.getEntityByRef.mockResolvedValue( + mockEntityWithEmptyExternalAnnotation, + ); + + const rendered = await renderInTestApp( + + + , + { + mountedRoutes, + }, + ); + + expect(rendered.container.querySelector('header')).toBeInTheDocument(); + expect(rendered.container.querySelector('article')).toBeInTheDocument(); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx index 80545151d9..9b4999899d 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx @@ -14,19 +14,26 @@ * limitations under the License. */ -import { Children, ReactElement, ReactNode } from 'react'; -import { useOutlet } from 'react-router-dom'; - +import { + Children, + ReactElement, + ReactNode, + useEffect, + useMemo, + useCallback, +} from 'react'; +import { useOutlet, useNavigate } from 'react-router-dom'; import { Page } from '@backstage/core-components'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { TECHDOCS_ADDONS_KEY, TECHDOCS_ADDONS_WRAPPER_KEY, TechDocsReaderPageProvider, + buildTechDocsURL, } from '@backstage/plugin-techdocs-react'; - +import { TECHDOCS_EXTERNAL_ANNOTATION } from '@backstage/plugin-techdocs-common'; +import useAsync from 'react-use/esm/useAsync'; import { TechDocsReaderPageRenderFunction } from '../../../types'; - import { TechDocsReaderPageContent } from '../TechDocsReaderPageContent'; import { TechDocsReaderPageHeader } from '../TechDocsReaderPageHeader'; import { TechDocsReaderPageSubheader } from '../TechDocsReaderPageSubheader'; @@ -34,9 +41,11 @@ import { rootDocsRouteRef } from '../../../routes'; import { getComponentData, useRouteRefParams, + useApi, + useRouteRef, } from '@backstage/core-plugin-api'; - import { CookieAuthRefreshProvider } from '@backstage/plugin-auth-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { createTheme, styled, @@ -44,6 +53,7 @@ import { ThemeProvider, useTheme, } from '@material-ui/core/styles'; +import { Progress } from '@backstage/core-components'; /* An explanation for the multiple ways of customizing the TechDocs reader page @@ -177,44 +187,100 @@ const StyledPage = styled(Page)({ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => { const currentTheme = useTheme(); - const readerPageTheme = createTheme({ - ...currentTheme, - ...(props.overrideThemeOptions || {}), - }); + const readerPageTheme = useMemo( + () => + createTheme({ + ...currentTheme, + ...(props.overrideThemeOptions || {}), + }), + [currentTheme, props.overrideThemeOptions], + ); + const { kind, name, namespace } = useRouteRefParams(rootDocsRouteRef); const { children, entityRef = { kind, name, namespace } } = props; const outlet = useOutlet(); - if (!children) { + const catalogApi = useApi(catalogApiRef); + const navigate = useNavigate(); + const viewTechdocLink = useRouteRef(rootDocsRouteRef); + + const memoizedEntityRef = useMemo( + () => ({ + kind: entityRef.kind, + name: entityRef.name, + namespace: entityRef.namespace, + }), + [entityRef.kind, entityRef.name, entityRef.namespace], + ); + + const externalEntityTechDocsUrl = useAsync(async () => { + const catalogEntity = await catalogApi.getEntityByRef(memoizedEntityRef); + + if (catalogEntity?.metadata?.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION]) { + return buildTechDocsURL(catalogEntity, viewTechdocLink); + } + + return undefined; + }, [memoizedEntityRef, catalogApi, viewTechdocLink]); + + const handleNavigation = useCallback( + (url: string) => { + navigate(url, { replace: true }); + }, + [navigate], + ); + + useEffect(() => { + if (!externalEntityTechDocsUrl.loading && externalEntityTechDocsUrl.value) { + handleNavigation(externalEntityTechDocsUrl.value); + } + }, [ + externalEntityTechDocsUrl.loading, + externalEntityTechDocsUrl.value, + handleNavigation, + ]); + + const page: ReactNode = useMemo(() => { + if (children) { + return null; + } + const childrenList = outlet ? Children.toArray(outlet.props.children) : []; const grandChildren = childrenList.flatMap( child => (child as ReactElement)?.props?.children ?? [], ); - const page: ReactNode = grandChildren.find( + return grandChildren.find( grandChild => !getComponentData(grandChild, TECHDOCS_ADDONS_WRAPPER_KEY) && !getComponentData(grandChild, TECHDOCS_ADDONS_KEY), ); + }, [children, outlet]); - // As explained above, "page" is configuration 4 and is 1 + if (externalEntityTechDocsUrl.loading || externalEntityTechDocsUrl.value) { + return ; + } + + // As explained above, "page" is configuration 4 and is 1 + if (!children) { return ( - + {(page as JSX.Element) || } ); } + // As explained above, a render function is configuration 3 and React element is 2 return ( - + {({ metadata, entityMetadata, onReady }) => ( { > {children instanceof Function ? children({ - entityRef, + entityRef: memoizedEntityRef, techdocsMetadataValue: metadata.value, entityMetadataValue: entityMetadata.value, onReady, From dcdb03c7f491179188ab53c8f26958051dba92ca Mon Sep 17 00:00:00 2001 From: Owen Shartle Date: Mon, 25 Aug 2025 15:49:41 -0400 Subject: [PATCH 021/233] Correcting chagesets -- had duplicate changeset. Signed-off-by: Owen Shartle --- .changeset/eighty-numbers-act.md | 2 +- .changeset/lovely-actors-love.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/eighty-numbers-act.md b/.changeset/eighty-numbers-act.md index 183d857d92..dc7d504adc 100644 --- a/.changeset/eighty-numbers-act.md +++ b/.changeset/eighty-numbers-act.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-techdocs-react': patch --- Update to documentation regarding TechDocs redirects. diff --git a/.changeset/lovely-actors-love.md b/.changeset/lovely-actors-love.md index f645cc5b3f..b428a11619 100644 --- a/.changeset/lovely-actors-love.md +++ b/.changeset/lovely-actors-love.md @@ -2,4 +2,4 @@ '@backstage/plugin-techdocs-backend': minor --- -Adding new entity that specifies an external entity in the techdocs-entity annotation. +Adding new entity that specifies an external entity in the techdocs-entity annotation and updates to documentation regarding TechDocs redirects. From 40fe543447f190cdaa6465abd4916cefed3117ff Mon Sep 17 00:00:00 2001 From: Owen Shartle Date: Mon, 25 Aug 2025 16:43:58 -0400 Subject: [PATCH 022/233] Running prettier on files. Signed-off-by: Owen Shartle --- .../examples/documented-component/catalog-info.yaml | 2 +- .../techdocs-backend/examples/documented-component/mkdocs.yml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml b/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml index 4d3b71eb2a..5b90693e23 100644 --- a/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml +++ b/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml @@ -23,4 +23,4 @@ metadata: spec: type: service lifecycle: experimental - owner: user:guest \ No newline at end of file + owner: user:guest diff --git a/plugins/techdocs-backend/examples/documented-component/mkdocs.yml b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml index ab97326913..fd954225a3 100644 --- a/plugins/techdocs-backend/examples/documented-component/mkdocs.yml +++ b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml @@ -11,4 +11,3 @@ nav: plugins: - techdocs-core - \ No newline at end of file From 8eb950ff649b18430d29364ff0a11a5593e1515d Mon Sep 17 00:00:00 2001 From: Owen Shartle Date: Mon, 25 Aug 2025 21:36:57 -0400 Subject: [PATCH 023/233] Adding a try-catch around the usage of the catalog API in the TechDocsReaderPage as it could still attempt to load a standard TechDocs page. Signed-off-by: Owen Shartle --- .../TechDocsReaderPage.test.tsx | 25 +++++++++++++++++++ .../TechDocsReaderPage/TechDocsReaderPage.tsx | 12 ++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx index 93f24e9f97..c645d1db83 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx @@ -413,5 +413,30 @@ describe('', () => { expect(rendered.container.querySelector('article')).toBeInTheDocument(); expect(mockNavigate).not.toHaveBeenCalled(); }); + + it('should render normally when catalog API throws an error', async () => { + catalogApiMock.getEntityByRef.mockRejectedValue( + new Error('Catalog API error'), + ); + + const rendered = await renderInTestApp( + + + , + { + mountedRoutes, + }, + ); + + expect(rendered.container.querySelector('header')).toBeInTheDocument(); + expect(rendered.container.querySelector('article')).toBeInTheDocument(); + expect(mockNavigate).not.toHaveBeenCalled(); + }); }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx index 9b4999899d..c67660fd79 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx @@ -215,10 +215,16 @@ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => { ); const externalEntityTechDocsUrl = useAsync(async () => { - const catalogEntity = await catalogApi.getEntityByRef(memoizedEntityRef); + try { + const catalogEntity = await catalogApi.getEntityByRef(memoizedEntityRef); - if (catalogEntity?.metadata?.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION]) { - return buildTechDocsURL(catalogEntity, viewTechdocLink); + if ( + catalogEntity?.metadata?.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION] + ) { + return buildTechDocsURL(catalogEntity, viewTechdocLink); + } + } catch (error) { + // Ignore error and allow an attempt at loading the current entity's TechDocs when unable to fetch an external entity from the catalog. } return undefined; From def6247bb6cc42437a77c5c9bbb3b303fa195d02 Mon Sep 17 00:00:00 2001 From: Owen Shartle Date: Mon, 25 Aug 2025 22:00:22 -0400 Subject: [PATCH 024/233] Adding catalogApiRef to test-utils.tsx. Signed-off-by: Owen Shartle --- plugins/techdocs-addons-test-utils/src/test-utils.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/techdocs-addons-test-utils/src/test-utils.tsx b/plugins/techdocs-addons-test-utils/src/test-utils.tsx index 91b85e7885..2d7420ec62 100644 --- a/plugins/techdocs-addons-test-utils/src/test-utils.tsx +++ b/plugins/techdocs-addons-test-utils/src/test-utils.tsx @@ -39,6 +39,7 @@ import { } from '@backstage/plugin-techdocs-react'; import { TechDocsReaderPage, techdocsPlugin } from '@backstage/plugin-techdocs'; import { + catalogApiRef, EntityPresentationApi, entityPresentationApiRef, entityRouteRef, @@ -235,8 +236,16 @@ export class TechDocsAddonTester { }), }; + const catalogApi = { + getEntityByRef: jest.fn().mockResolvedValue({ + kind: 'Component', + metadata: { namespace: 'default', name: 'docs' }, + }), + }; + const apis: TechdocsAddonTesterApis = [ [fetchApiRef, fetchApi], + [catalogApiRef, catalogApi], [entityPresentationApiRef, entityPresentationApi], [discoveryApiRef, discoveryApi], [techdocsApiRef, techdocsApi], From 72543e92b8bae988f7ebfcf6fa08930ca5e27c8e Mon Sep 17 00:00:00 2001 From: Owen Shartle Date: Mon, 25 Aug 2025 22:05:59 -0400 Subject: [PATCH 025/233] Adding catalogApiRef to test-utils to support catalog API usage by TechDocs reader page. Signed-off-by: Owen Shartle --- .changeset/tiny-spoons-mix.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tiny-spoons-mix.md diff --git a/.changeset/tiny-spoons-mix.md b/.changeset/tiny-spoons-mix.md new file mode 100644 index 0000000000..bb607aca3f --- /dev/null +++ b/.changeset/tiny-spoons-mix.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-addons-test-utils': minor +--- + +Adding catalogApiRef to test-utils to support catalog API usage by TechDocs reader page. From 0d415ae01409b022b4d2fe8457cc8d301d2fadcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Fankh=C3=A4nel?= Date: Tue, 26 Aug 2025 15:08:55 +0200 Subject: [PATCH 026/233] fix(scaffolder): render TechDocs link on Template List page for TechDocs annotations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show “View TechDocs” link when template has backstage.io/techdocs-ref or backstage.io/techdocs-entity. Append backstage.io/techdocs-entity-path when set. Use buildTechDocsURL from @backstage/plugin-techdocs-react. Add tests covering both annotations and path handling. Update dependencies to include @backstage/plugin-techdocs-common and @backstage/plugin-techdocs-react. Add sample TechDocs scaffolding to notifications-demo template. Closes #29076. Signed-off-by: David Fankhänel --- .changeset/itchy-moons-start.md | 5 + plugins/scaffolder/package.json | 3 + .../TemplateListPage.test.tsx | 129 +++++++++++++++++- .../TemplateListPage/TemplateListPage.tsx | 17 ++- yarn.lock | 3 + 5 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 .changeset/itchy-moons-start.md diff --git a/.changeset/itchy-moons-start.md b/.changeset/itchy-moons-start.md new file mode 100644 index 0000000000..e5768b58b2 --- /dev/null +++ b/.changeset/itchy-moons-start.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Render a TechDocs link on the Scaffolder Template List page when templates include either `backstage.io/techdocs-ref` or `backstage.io/techdocs-entity` annotations, using the shared `buildTechDocsURL` helper. Also adds tests to verify both annotations and optional `backstage.io/techdocs-entity-path` are respected. diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index ff56a19175..df75a65656 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -72,6 +72,8 @@ "@backstage/plugin-permission-react": "workspace:^", "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/plugin-scaffolder-react": "workspace:^", + "@backstage/plugin-techdocs-common": "workspace:^", + "@backstage/plugin-techdocs-react": "workspace:^", "@backstage/types": "workspace:^", "@codemirror/language": "^6.0.0", "@codemirror/legacy-modes": "^6.1.0", @@ -109,6 +111,7 @@ "@backstage/dev-utils": "workspace:^", "@backstage/plugin-catalog": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", + "@backstage/plugin-techdocs": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/dom": "^10.0.0", "@testing-library/jest-dom": "^6.0.0", diff --git a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx index a16474a4cf..da544cb2c0 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx @@ -27,13 +27,19 @@ import { TestApiProvider, mockApis, } from '@backstage/test-utils'; -import { rootRouteRef } from '../../../routes'; +import { rootRouteRef, viewTechDocRouteRef } from '../../../routes'; import { TemplateListPage } from './TemplateListPage'; +import { + TECHDOCS_ANNOTATION, + TECHDOCS_EXTERNAL_ANNOTATION, + TECHDOCS_EXTERNAL_PATH_ANNOTATION, +} from '@backstage/plugin-techdocs-common'; const mountedRoutes = { mountedRoutes: { '/': rootRouteRef, '/catalog/:namespace/:kind/:name': entityRouteRef, + '/docs/:namespace/:kind/:name': viewTechDocRouteRef, }, }; @@ -51,6 +57,127 @@ describe('TemplateListPage', () => { ], }); + describe('TechDocs link rendering', () => { + it('shows TechDocs link when template has backstage.io/techdocs-ref', async () => { + const mockCatalogApiWithDocs = catalogApiMock({ + entities: [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 'tmpl-a', + annotations: { [TECHDOCS_ANNOTATION]: 'dir:.' }, + }, + spec: { type: 'service' }, + }, + ], + }); + + const { findByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + expect(await findByText('View TechDocs')).toBeInTheDocument(); + }); + + it('shows TechDocs link when template has backstage.io/techdocs-entity', async () => { + const mockCatalogApiWithExternal = catalogApiMock({ + entities: [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 'tmpl-b', + annotations: { + [TECHDOCS_EXTERNAL_ANNOTATION]: 'component:default/other', + }, + }, + spec: { type: 'service' }, + }, + ], + }); + + const { findByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + expect(await findByText('View TechDocs')).toBeInTheDocument(); + }); + + it('appends path when backstage.io/techdocs-entity-path is set', async () => { + const mockCatalogApiWithPath = catalogApiMock({ + entities: [ + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + name: 'tmpl-c', + annotations: { + [TECHDOCS_EXTERNAL_ANNOTATION]: 'component:default/other', + [TECHDOCS_EXTERNAL_PATH_ANNOTATION]: '/guides/start', + }, + }, + spec: { type: 'service' }, + }, + ], + }); + + const { findByText } = await renderInTestApp( + + + , + mountedRoutes, + ); + + const link = (await findByText('View TechDocs')).closest('a')!; + expect(link).toHaveAttribute( + 'href', + expect.stringMatching( + /\/docs\/default\/component\/other\/?(index\.html)?#?\/guides\/start|\/docs\/default\/component\/other\/guides\/start/, + ), + ); + }); + }); + it('should render the search bar for templates', async () => { const { getByPlaceholderText } = await renderInTestApp( { const additionalLinksForEntity = useCallback( (template: TemplateEntityV1beta3) => { - const { kind, namespace, name } = parseEntityRef( - stringifyEntityRef(template), - ); - return template.metadata.annotations?.['backstage.io/techdocs-ref'] && - viewTechDocsLink + const hasTechDocs = + !!template.metadata.annotations?.[TECHDOCS_ANNOTATION] || + !!template.metadata.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION]; + + return hasTechDocs && viewTechDocsLink ? [ { icon: app.getSystemIcon('docs') ?? DocsIcon, text: t( 'templateListPage.additionalLinksForEntity.viewTechDocsTitle', ), - url: viewTechDocsLink({ kind, namespace, name }), + url: buildTechDocsURL(template, viewTechDocsLink), }, ] : []; diff --git a/yarn.lock b/yarn.lock index 5487f23662..aae8a0ef0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6573,6 +6573,9 @@ __metadata: "@backstage/plugin-permission-react": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/plugin-scaffolder-react": "workspace:^" + "@backstage/plugin-techdocs": "workspace:^" + "@backstage/plugin-techdocs-common": "workspace:^" + "@backstage/plugin-techdocs-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@codemirror/language": "npm:^6.0.0" From 4d0de036ed708231b1043a91d87c09ccbc5ead4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Wed, 27 Aug 2025 08:38:55 +0200 Subject: [PATCH 027/233] Minor change, not patch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- .changeset/dull-eagles-sin.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/dull-eagles-sin.md b/.changeset/dull-eagles-sin.md index a527a9757d..864f38f582 100644 --- a/.changeset/dull-eagles-sin.md +++ b/.changeset/dull-eagles-sin.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-catalog-graph': minor --- -Support custom relations by using an API +Support custom relations by using an API to define known relations and which to show by default From 5b6416938412da34b1459a484d9d6827c92c8800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Fankh=C3=A4nel?= Date: Wed, 27 Aug 2025 10:28:28 +0200 Subject: [PATCH 028/233] fix(TemplateListPage): fix tsc error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: David Fankhänel --- .../TemplateListPage/TemplateListPage.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.tsx b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.tsx index 0a7a3df888..37a50ed904 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.tsx @@ -149,18 +149,25 @@ export const TemplateListPage = (props: TemplateListPageProps) => { const additionalLinksForEntity = useCallback( (template: TemplateEntityV1beta3) => { - const hasTechDocs = - !!template.metadata.annotations?.[TECHDOCS_ANNOTATION] || - !!template.metadata.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION]; + if ( + !( + template.metadata.annotations?.[TECHDOCS_ANNOTATION] || + template.metadata.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION] + ) || + !viewTechDocsLink + ) { + return []; + } - return hasTechDocs && viewTechDocsLink + const url = buildTechDocsURL(template, viewTechDocsLink); + return url ? [ { icon: app.getSystemIcon('docs') ?? DocsIcon, text: t( 'templateListPage.additionalLinksForEntity.viewTechDocsTitle', ), - url: buildTechDocsURL(template, viewTechDocsLink), + url, }, ] : []; From 6dbf53531f3dd3ad39d6f443b57c1c1f2fac5925 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 27 Aug 2025 13:17:25 -0400 Subject: [PATCH 029/233] docs: Update docker.md - node:20 --> node:22 for base images Signed-off-by: Mathieu Benoit --- docs/deployment/docker.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md index 83f8cff6ea..04b72da445 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -53,7 +53,7 @@ Once the host build is complete, we are ready to build our image. The following `Dockerfile` is included when creating a new app with `@backstage/create-app`: ```dockerfile -FROM node:20-bookworm-slim +FROM node:22-bookworm-slim # Set Python interpreter for `node-gyp` to use ENV PYTHON=/usr/bin/python3 @@ -178,7 +178,7 @@ the repo root: ```dockerfile # Stage 1 - Create yarn install skeleton layer -FROM node:20-bookworm-slim AS packages +FROM node:22-bookworm-slim AS packages WORKDIR /app COPY backstage.json package.json yarn.lock ./ @@ -193,7 +193,7 @@ COPY plugins plugins RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -exec rm -rf {} \+ # Stage 2 - Install dependencies and build packages -FROM node:20-bookworm-slim AS build +FROM node:22-bookworm-slim AS build # Set Python interpreter for `node-gyp` to use ENV PYTHON=/usr/bin/python3 @@ -231,7 +231,7 @@ RUN mkdir packages/backend/dist/skeleton packages/backend/dist/bundle \ && tar xzf packages/backend/dist/bundle.tar.gz -C packages/backend/dist/bundle # Stage 3 - Build the actual backend image and install production dependencies -FROM node:20-bookworm-slim +FROM node:22-bookworm-slim # Set Python interpreter for `node-gyp` to use ENV PYTHON=/usr/bin/python3 From bf02e9649e19674ed8d66abc381ca73b65a4c8ce Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 27 Aug 2025 16:14:49 -0400 Subject: [PATCH 030/233] feat: Update backend/Dockerfile - node:20 --> node:22 Signed-off-by: Mathieu Benoit --- .../create-app/templates/next-app/packages/backend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/templates/next-app/packages/backend/Dockerfile b/packages/create-app/templates/next-app/packages/backend/Dockerfile index 68d6c2fd58..ee80245d8b 100644 --- a/packages/create-app/templates/next-app/packages/backend/Dockerfile +++ b/packages/create-app/templates/next-app/packages/backend/Dockerfile @@ -12,7 +12,7 @@ # Alternatively, there is also a multi-stage Dockerfile documented here: # https://backstage.io/docs/deployment/docker#multi-stage-build -FROM node:20-bookworm-slim +FROM node:22-bookworm-slim # Set Python interpreter for `node-gyp` to use ENV PYTHON=/usr/bin/python3 From 7cfea1aac81c96e4ee21b0f3257308e1e1ec904a Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 27 Aug 2025 16:19:32 -0400 Subject: [PATCH 031/233] feat: Update backend/Dockerfile - node:20 --> node:22 Signed-off-by: Mathieu Benoit --- .../templates/default-app/packages/backend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/templates/default-app/packages/backend/Dockerfile b/packages/create-app/templates/default-app/packages/backend/Dockerfile index 68d6c2fd58..ee80245d8b 100644 --- a/packages/create-app/templates/default-app/packages/backend/Dockerfile +++ b/packages/create-app/templates/default-app/packages/backend/Dockerfile @@ -12,7 +12,7 @@ # Alternatively, there is also a multi-stage Dockerfile documented here: # https://backstage.io/docs/deployment/docker#multi-stage-build -FROM node:20-bookworm-slim +FROM node:22-bookworm-slim # Set Python interpreter for `node-gyp` to use ENV PYTHON=/usr/bin/python3 From 75977819f642e46df29f57ac8be7830b9218eb73 Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Thu, 28 Aug 2025 09:11:43 +0300 Subject: [PATCH 032/233] fix(catalog): msgraph to use only first parent for groups the order of the groups from msgraph is random, thus the parent group for groups having multiple parents can be random. this leads to situation where the grpup parent is changing over the msgraph provider runs leading to unnecessary stitching. other possibility for the fix would be to change the model to allow multiple parents for groups (like mentioned in the TODO) but that could have some weird consequences in apps. Signed-off-by: Hellgren Heikki --- .changeset/loose-memes-bathe.md | 5 +++++ .../src/microsoftGraph/read.test.ts | 18 ++++++++++++++++-- .../src/microsoftGraph/read.ts | 9 ++++++++- 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 .changeset/loose-memes-bathe.md diff --git a/.changeset/loose-memes-bathe.md b/.changeset/loose-memes-bathe.md new file mode 100644 index 0000000000..85eff94399 --- /dev/null +++ b/.changeset/loose-memes-bathe.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +Ensure that msgraph parent group stays same in case the group has multiple parents diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index 33ae04bd76..bd58881728 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -1102,6 +1102,14 @@ describe('read microsoft graph', () => { name: 'c', }, }); + const groupD = group({ + metadata: { + annotations: { + 'graph.microsoft.com/group-id': 'group-id-d', + }, + name: 'd', + }, + }); const user1 = user({ metadata: { annotations: { @@ -1118,16 +1126,17 @@ describe('read microsoft graph', () => { name: 'user2', }, }); - const groups = [rootGroup, groupA, groupB, groupC]; + const groups = [rootGroup, groupA, groupB, groupC, groupD]; const users = [user1, user2]; const groupMember = new Map>(); groupMember.set('group-id-b', new Set(['group-id-c'])); + groupMember.set('group-id-d', new Set(['group-id-c'])); const groupMemberOf = new Map>(); groupMemberOf.set('user-id-1', new Set(['group-id-a'])); groupMemberOf.set('user-id-2', new Set(['group-id-c'])); // We have a root groups - // We have three groups: a, b, c. c is child of b + // We have three groups: a, b, c. c is child of b and d, b should be picked as it's first // we have two users: u1, u2. u1 is member of a, u2 is member of c resolveRelations(rootGroup, groups, users, groupMember, groupMemberOf); @@ -1147,6 +1156,11 @@ describe('read microsoft graph', () => { expect(groupC.spec.parent).toEqual('group:default/b'); expect(groupC.spec.children).toEqual(expect.arrayContaining([])); + expect(groupD.spec.parent).toEqual('group:default/root'); + expect(groupD.spec.children).toEqual( + expect.arrayContaining(['group:default/c']), + ); + expect(user1.spec.memberOf).toEqual( expect.arrayContaining(['group:default/a']), ); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 4761c45ac7..8bc7e6899b 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -359,7 +359,14 @@ export function resolveRelations( } }); + // TODO: Until we have better support for multiple parents in the model, + // the order of the parents is important as changing it causes + // unnecessary entity stitching randomly. retrieveItems(parentGroups, id).forEach(p => { + // Only set the parent if it doesn't exist yet + if (group.spec.parent) { + return; + } const parentGroup = groupMap.get(p); if (parentGroup) { // TODO: Only having a single parent group might not match every companies model, but fine for now. @@ -546,5 +553,5 @@ function retrieveItems( target: Map>, key: string, ): Set { - return target.get(key) ?? new Set(); + return new Set([...(target.get(key) ?? [])].sort()); } From 33bd4d05076b214644029a2ba3aed8bec276de60 Mon Sep 17 00:00:00 2001 From: Stanislav C <150145013+stanislav-c@users.noreply.github.com> Date: Thu, 28 Aug 2025 10:52:42 +0200 Subject: [PATCH 033/233] fix: deduplicate discovered backend features Signed-off-by: Stanislav C <150145013+stanislav-c@users.noreply.github.com> --- .changeset/afraid-streets-pay.md | 5 +++++ packages/backend-defaults/src/PackageDiscoveryService.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/afraid-streets-pay.md diff --git a/.changeset/afraid-streets-pay.md b/.changeset/afraid-streets-pay.md new file mode 100644 index 0000000000..d8e0bde4ab --- /dev/null +++ b/.changeset/afraid-streets-pay.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Deduplicate discovered features discovered with discoveryFeatureLoader diff --git a/packages/backend-defaults/src/PackageDiscoveryService.ts b/packages/backend-defaults/src/PackageDiscoveryService.ts index fe0ab931a7..abf015baf3 100644 --- a/packages/backend-defaults/src/PackageDiscoveryService.ts +++ b/packages/backend-defaults/src/PackageDiscoveryService.ts @@ -175,6 +175,6 @@ export class PackageDiscoveryService { } } - return { features }; + return { features: Array.from(new Set(features)) }; } } From 98ec79ca8392cafdb5fdce89eccf60cf245985f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Pola=C5=A1ko?= Date: Thu, 28 Aug 2025 17:10:12 +0200 Subject: [PATCH 034/233] The const is being exported while the type is not being exported. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Martin Polaško --- .../kubernetes-common/src/certificate-authority-constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/kubernetes-common/src/certificate-authority-constants.ts b/plugins/kubernetes-common/src/certificate-authority-constants.ts index 46401bde23..71bcad8a53 100644 --- a/plugins/kubernetes-common/src/certificate-authority-constants.ts +++ b/plugins/kubernetes-common/src/certificate-authority-constants.ts @@ -17,7 +17,7 @@ /** * A constant that specifies the location of the certificate for the service account. * - * @internal + * @public */ export const SERVICEACCOUNT_CA_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'; From bdd7f952d7abd2ab9e4cab050c807bd28d846d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Pola=C5=A1ko?= Date: Thu, 28 Aug 2025 17:43:49 +0200 Subject: [PATCH 035/233] chore(kubernetes-common): add changeset and update API report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make SERVICEACCOUNT_CA_PATH public so it can be imported by external modules. Signed-off-by: Martin Polaško --- .changeset/kind-places-reply.md | 5 +++++ plugins/kubernetes-common/report.api.md | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 .changeset/kind-places-reply.md diff --git a/.changeset/kind-places-reply.md b/.changeset/kind-places-reply.md new file mode 100644 index 0000000000..4b1d361e6e --- /dev/null +++ b/.changeset/kind-places-reply.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-common': patch +--- + +Make SERVICEACCOUNT_CA_PATH public so it can be imported by external modules. diff --git a/plugins/kubernetes-common/report.api.md b/plugins/kubernetes-common/report.api.md index 2d94aaefff..748ae94656 100644 --- a/plugins/kubernetes-common/report.api.md +++ b/plugins/kubernetes-common/report.api.md @@ -455,6 +455,10 @@ export interface SecretsFetchResponse { type: 'secrets'; } +// @public +export const SERVICEACCOUNT_CA_PATH = + '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'; + // @public (undocumented) export interface ServiceFetchResponse { // (undocumented) From 6985276cf646f2fa8ebec3cd0e66c19a7b0a5ef1 Mon Sep 17 00:00:00 2001 From: Thomas Cardonne Date: Mon, 1 Sep 2025 13:30:02 +0200 Subject: [PATCH 036/233] chore: change invalid log level message Signed-off-by: Thomas Cardonne --- .../backend-defaults/src/entrypoints/rootLogger/config.test.ts | 2 +- packages/backend-defaults/src/entrypoints/rootLogger/config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/config.test.ts b/packages/backend-defaults/src/entrypoints/rootLogger/config.test.ts index 6fde1d3ec2..e32cc13247 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/config.test.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/config.test.ts @@ -69,7 +69,7 @@ describe('getRootLoggerConfig', () => { expect(() => getRootLoggerConfig(mockServices.rootConfig({ data: config })), ).toThrow( - "Invalid config at backend.logger.overrides[0].level, 'invalid' is not a valid Winston npm logging level", + "Invalid config at backend.logger.overrides[0].level, 'invalid' is not a valid logging level, must be one of 'error', 'warn', 'info' or 'debug'.", ); }); }); diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/config.ts b/packages/backend-defaults/src/entrypoints/rootLogger/config.ts index 4a1f9e1446..24660f82c8 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/config.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/config.ts @@ -32,7 +32,7 @@ export const getRootLoggerConfig = ( const overrideLevel = override.getString('level'); if (winstonLevels[overrideLevel] === undefined) { throw new Error( - `Invalid config at backend.logger.overrides[${i}].level, '${overrideLevel}' is not a valid Winston npm logging level`, + `Invalid config at backend.logger.overrides[${i}].level, '${overrideLevel}' is not a valid logging level, must be one of 'error', 'warn', 'info' or 'debug'.`, ); } From b6ab2d405dcf57f9e228bdea0af052366386bc25 Mon Sep 17 00:00:00 2001 From: Thomas Cardonne Date: Mon, 1 Sep 2025 13:44:36 +0200 Subject: [PATCH 037/233] tests: add test case Signed-off-by: Thomas Cardonne --- .../rootLogger/WinstonLogger.test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.test.ts b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.test.ts index 8fc35a6e6f..7dba10cccf 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.test.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.test.ts @@ -170,4 +170,30 @@ describe('WinstonLogger', () => { expect.any(Function), ); }); + + it('should filter logs above the default log level with an override', () => { + const mockTransport = new Transport({ + log: jest.fn(), + logv: jest.fn(), + }); + + const logger = WinstonLogger.create({ + level: 'debug', + format: format.json(), + transports: [mockTransport], + }); + + logger.setLevelOverrides([ + { + matchers: { + plugin: 'catalog', + }, + level: 'error', + }, + ]); + + logger.info('info log', { plugin: 'catalog' }); + + expect(mockTransport.log).not.toHaveBeenCalled(); + }); }); From f4045adea049d62776f049c0aad3b77401ca0bb1 Mon Sep 17 00:00:00 2001 From: Thomas Cardonne Date: Mon, 1 Sep 2025 13:45:50 +0200 Subject: [PATCH 038/233] refactor: use JsonPrimitive | JsonPrimitive[] instead of JsonValue Signed-off-by: Thomas Cardonne --- packages/backend-defaults/report-rootLogger.api.md | 4 ++-- .../src/entrypoints/rootLogger/config.ts | 10 ++++++++-- .../src/entrypoints/rootLogger/types.ts | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/backend-defaults/report-rootLogger.api.md b/packages/backend-defaults/report-rootLogger.api.md index aa27a366e3..99dc263358 100644 --- a/packages/backend-defaults/report-rootLogger.api.md +++ b/packages/backend-defaults/report-rootLogger.api.md @@ -6,7 +6,7 @@ import { config } from 'winston'; import { Format } from 'logform'; import { JsonObject } from '@backstage/types'; -import { JsonValue } from '@backstage/types'; +import { JsonPrimitive } from '@backstage/types'; import { LoggerService } from '@backstage/backend-plugin-api'; import { RootLoggerService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; @@ -55,7 +55,7 @@ export type WinstonLoggerLevelOverride = { // @public (undocumented) export type WinstonLoggerLevelOverrideMatchers = { - [key: string]: JsonValue | undefined; + [key: string]: JsonPrimitive | JsonPrimitive[] | undefined; }; // @public (undocumented) diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/config.ts b/packages/backend-defaults/src/entrypoints/rootLogger/config.ts index 24660f82c8..11687d72f4 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/config.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/config.ts @@ -15,7 +15,11 @@ */ import { RootConfigService } from '@backstage/backend-plugin-api'; import { JsonObject } from '@backstage/types'; -import { RootLoggerConfig, winstonLevels } from './types'; +import { + RootLoggerConfig, + winstonLevels, + WinstonLoggerLevelOverrideMatchers, +} from './types'; export const getRootLoggerConfig = ( config: RootConfigService, @@ -36,7 +40,9 @@ export const getRootLoggerConfig = ( ); } - const matchers = override.getConfig('matchers').get(); + const matchers = override + .getConfig('matchers') + .get(); return { matchers, diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/types.ts b/packages/backend-defaults/src/entrypoints/rootLogger/types.ts index b1f6fcc5b3..f8277d8075 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/types.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/types.ts @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JsonObject, JsonValue } from '@backstage/types'; +import { JsonObject, JsonPrimitive } from '@backstage/types'; import { config as winstonConfig } from 'winston'; /** * @public */ export type WinstonLoggerLevelOverrideMatchers = { - [key: string]: JsonValue | undefined; + [key: string]: JsonPrimitive | JsonPrimitive[] | undefined; }; /** From 7b5129c490bff5a50edd742fd504c1003152ca74 Mon Sep 17 00:00:00 2001 From: Thomas Cardonne Date: Mon, 1 Sep 2025 15:20:48 +0200 Subject: [PATCH 039/233] refactor: avoid recompiling the regex on every test Signed-off-by: Thomas Cardonne --- .../entrypoints/rootLogger/WinstonLogger.ts | 17 +++-- .../src/entrypoints/rootLogger/utils.test.ts | 76 ++++++++----------- .../src/entrypoints/rootLogger/utils.ts | 68 ++++++++++------- 3 files changed, 86 insertions(+), 75 deletions(-) diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts index 90f3042e33..c48cef2f1f 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts @@ -31,7 +31,7 @@ import { import { MESSAGE } from 'triple-beam'; import { escapeRegExp } from '../../lib/escapeRegExp'; import { winstonLevels, WinstonLoggerLevelOverride } from './types'; -import { isLogMatching } from './utils'; +import { createLogMatcher } from './utils'; /** * @public @@ -188,12 +188,15 @@ export class WinstonLogger implements RootLoggerService { format: Format; setOverrides: (overrides: WinstonLoggerLevelOverride[]) => void; } { - const overrides: WinstonLoggerLevelOverride[] = []; + const overrides: { + predicate: (log: TransformableInfo) => boolean; + level: string; + }[] = []; return { format: format(log => { for (const override of overrides) { - if (isLogMatching(log, override.matchers)) { + if (override.predicate(log)) { // Discard the log if the log level is below the override // eg, if the override level is 'warn' (1) and the log is 'debug' (5) if (winstonLevels[log.level] > winstonLevels[override.level]) { @@ -213,8 +216,12 @@ export class WinstonLogger implements RootLoggerService { return log; })(), setOverrides: newOverrides => { - // Replace the content while preserving the reference - overrides.splice(0, overrides.length, ...newOverrides); + const newOverridesPredicates = newOverrides.map(o => ({ + predicate: createLogMatcher(o.matchers), + level: o.level, + })); + // Replace the content while preserving the reference to support live config updates + overrides.splice(0, overrides.length, ...newOverridesPredicates); }, }; } diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/utils.test.ts b/packages/backend-defaults/src/entrypoints/rootLogger/utils.test.ts index c60414cfc3..ede3de600b 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/utils.test.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/utils.test.ts @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { isLogMatching } from './utils'; +import { createLogMatcher } from './utils'; -describe('isLogMatching', () => { +describe('createLogMatcher', () => { const log = { level: 'info', message: 'This is a simple log from the catalog plugin', @@ -25,68 +25,56 @@ describe('isLogMatching', () => { }; it('should match with a simple matcher', () => { - expect( - isLogMatching(log, { - plugin: 'catalog', - }), - ).toEqual(true); + const matcher = createLogMatcher({ plugin: 'catalog' }); + expect(matcher(log)).toEqual(true); }); it('should not match with a simple matcher', () => { - expect( - isLogMatching(log, { - plugin: 'search', - }), - ).toEqual(false); + const matcher = createLogMatcher({ plugin: 'search' }); + expect(matcher(log)).toEqual(false); }); it('should match with an AND matcher', () => { - expect( - isLogMatching(log, { - plugin: 'catalog', - action: 'read', - }), - ).toEqual(true); + const matcher = createLogMatcher({ + plugin: 'catalog', + action: 'read', + }); + expect(matcher(log)).toEqual(true); }); it('should not match log with an AND matcher', () => { - expect( - isLogMatching(log, { - plugin: 'catalog', - action: 'write', - }), - ).toEqual(false); + const matcher = createLogMatcher({ + plugin: 'catalog', + action: 'write', + }); + expect(matcher(log)).toEqual(false); }); it('should match with an OR matcher', () => { - expect( - isLogMatching(log, { - plugin: ['auth', 'catalog'], - }), - ).toEqual(true); + const matcher = createLogMatcher({ + plugin: ['auth', 'catalog'], + }); + expect(matcher(log)).toEqual(true); }); it('should not match log with an OR matcher', () => { - expect( - isLogMatching(log, { - plugin: ['auth', 'search'], - }), - ).toEqual(false); + const matcher = createLogMatcher({ + plugin: ['auth', 'search'], + }); + expect(matcher(log)).toEqual(false); }); it('should match log with a regex matcher', () => { - expect( - isLogMatching(log, { - message: '/This is a simple log/', - }), - ).toEqual(true); + const matcher = createLogMatcher({ + message: '/This is a simple log/', + }); + expect(matcher(log)).toEqual(true); }); it('should not match log with a regex matcher', () => { - expect( - isLogMatching(log, { - message: '/^simple log/', - }), - ).toEqual(false); + const matcher = createLogMatcher({ + message: '/^simple log/', + }); + expect(matcher(log)).toEqual(false); }); }); diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/utils.ts b/packages/backend-defaults/src/entrypoints/rootLogger/utils.ts index 205a1b1520..adf38fc4da 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/utils.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/utils.ts @@ -17,62 +17,78 @@ import { TransformableInfo } from 'logform'; import { WinstonLoggerLevelOverrideMatchers } from './types'; +/** Parse a slash-delimited regex like `/pattern/flags` into a RegExp, or null if not a regex-string */ +const parseRegex = (s: string): RegExp | null => { + if (!s.startsWith('/')) return null; + const lastSlash = s.lastIndexOf('/'); + if (lastSlash <= 0) return null; + + const pattern = s.slice(1, lastSlash); + const flags = s.slice(lastSlash + 1); + + try { + return new RegExp(pattern, flags); + } catch { + return null; // fall back to treating it as a plain string + } +}; + /** - * Determines if a given log field matches a specified matcher. + * Create a predicate function that determines whether a log field matches a given matcher. * * The matcher can be: * - A string (exact match or regex pattern delimited by slashes, e.g. `/pattern/`) * - A non-string value (compared by strict equality) * - An array of matchers (returns true if any matcher matches) * - * @param logField - The log field value to test for a match. * @param matcher - The matcher or array of matchers to compare against the log field. - * @returns `true` if the log field matches the matcher, otherwise `false`. + * @returns A function that takes a log field and returns `true` if it matches the matcher, otherwise `false`. */ -const isLogFieldMatching = ( - logField: unknown, +const createLogFieldMatcher = ( matcher: WinstonLoggerLevelOverrideMatchers[0], -): boolean => { +): ((logField: unknown) => boolean) => { + // Array of matchers: create predicates for each element and OR them together if (Array.isArray(matcher)) { - return matcher.some(m => isLogFieldMatching(logField, m)); + const fns = matcher.map(m => createLogFieldMatcher(m)); + return (logField: unknown) => fns.some(fn => fn(logField)); } + // Non-string matcher: strict equality if (typeof matcher !== 'string') { - return logField === matcher; + return (logField: unknown) => logField === matcher; } - if ( - matcher.startsWith('/') && - matcher.endsWith('/') && - typeof logField === 'string' - ) { - const regex = new RegExp(matcher.slice(1, -1)); - return regex.test(logField); + // String matcher: maybe a slash-delimited regex (/pattern/flags) + const regex = parseRegex(matcher); + if (regex) { + return (logField: unknown) => + typeof logField === 'string' && regex.test(logField); } - return logField === matcher; + // Plain string matcher: strict equality + return (logField: unknown) => logField === matcher; }; /** - * Determines whether a log entry matches all specified override matchers. + * Create a predicate function that determines whether a log entry matches + * all specified override matchers. * * Iterates over each key-matcher pair in the provided `matchers` object, * retrieves the corresponding field from the `log` object, and checks if * the field matches the matcher using `isLogFieldMatching`. Returns `true` * only if all matchers are satisfied. * - * @param log - The log entry to be checked, typically containing various log fields. * @param matchers - An object where each key corresponds to a log field and each value is a matcher to test against that field. - * @returns `true` if the log entry matches all provided matchers, otherwise `false`. + * @returns A function that takes a log entry and returns `true` if it matches all specified matchers, otherwise `false`. */ -export const isLogMatching = ( - log: TransformableInfo, +export const createLogMatcher = ( matchers: WinstonLoggerLevelOverrideMatchers, -): boolean => { - const matched = Object.entries(matchers).every(([key, matcher]) => { - const logField = log[key]; - return isLogFieldMatching(logField, matcher); +): ((log: TransformableInfo) => boolean) => { + const logFieldMatchers = Object.entries(matchers).map(([key, m]) => { + const fn = createLogFieldMatcher(m); + return [key, fn] as const; }); - return matched; + return (log: TransformableInfo) => + logFieldMatchers.every(([key, fn]) => fn(log[key])); }; From 28ea47efe43f5b9176921334aedf25ca836c975b Mon Sep 17 00:00:00 2001 From: Owen Shartle Date: Mon, 1 Sep 2025 20:42:44 -0400 Subject: [PATCH 040/233] Adding 'subpage' as an accepted word for the spell checker. Signed-off-by: Owen Shartle --- .github/vale/config/vocabularies/Backstage/accept.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index ff6ea3aacb..495325a90e 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -459,6 +459,7 @@ subfolders subheader subheaders subkey +subpage subpath subroutes substring From ff40e2297ca4dde38b1a82a96428a1308fb62531 Mon Sep 17 00:00:00 2001 From: Owen Shartle Date: Mon, 1 Sep 2025 21:09:50 -0400 Subject: [PATCH 041/233] Using catalogApiMock from @backstage/plugin-catalog-react/testUtils. Signed-off-by: Owen Shartle --- .../src/test-utils.tsx | 16 ++++++++++------ .../TechDocsReaderPage.test.tsx | 5 ++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/plugins/techdocs-addons-test-utils/src/test-utils.tsx b/plugins/techdocs-addons-test-utils/src/test-utils.tsx index 2d7420ec62..a7cb74cb93 100644 --- a/plugins/techdocs-addons-test-utils/src/test-utils.tsx +++ b/plugins/techdocs-addons-test-utils/src/test-utils.tsx @@ -44,6 +44,7 @@ import { entityPresentationApiRef, entityRouteRef, } from '@backstage/plugin-catalog-react'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { searchApiRef } from '@backstage/plugin-search-react'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; @@ -236,12 +237,15 @@ export class TechDocsAddonTester { }), }; - const catalogApi = { - getEntityByRef: jest.fn().mockResolvedValue({ - kind: 'Component', - metadata: { namespace: 'default', name: 'docs' }, - }), - }; + const catalogApi = catalogApiMock({ + entities: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { namespace: 'default', name: 'docs' }, + }, + ], + }); const apis: TechdocsAddonTesterApis = [ [fetchApiRef, fetchApi], diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx index c645d1db83..aea45a60fa 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx @@ -27,6 +27,7 @@ import { renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; +import { catalogApiMock as catalogApiMockFactory } from '@backstage/plugin-catalog-react/testUtils'; import { techdocsApiRef, techdocsStorageApiRef } from '../../../api'; @@ -96,9 +97,7 @@ const entityPresentationApiMock: jest.Mocked< }), }; -const catalogApiMock = { - getEntityByRef: jest.fn().mockResolvedValue(mockEntityMetadata), -}; +const catalogApiMock = catalogApiMockFactory.mock(); const fetchApiMock = { fetch: jest.fn().mockResolvedValue({ From eb772f5f1886fa46e1bbc2dfa046e48609cc6c29 Mon Sep 17 00:00:00 2001 From: Yannik Daellenbach Date: Tue, 12 Aug 2025 15:03:11 +0200 Subject: [PATCH 042/233] Init `auth-backend-module-openshift-provider` Signed-off-by: Yannik Daellenbach --- packages/backend/package.json | 1 + .../.eslintrc.js | 1 + .../README.md | 5 + .../catalog-info.yaml | 10 + .../config.d.ts | 44 +++ .../dev/index.ts | 26 ++ .../package.json | 55 +++ .../report.api.md | 28 ++ .../src/authenticator.test.ts | 338 ++++++++++++++++++ .../src/authenticator.ts | 184 ++++++++++ .../src/index.ts | 25 ++ .../src/module.ts | 45 +++ .../src/resolvers.ts | 68 ++++ yarn.lock | 98 +++-- 14 files changed, 890 insertions(+), 38 deletions(-) create mode 100644 plugins/auth-backend-module-openshift-provider/.eslintrc.js create mode 100644 plugins/auth-backend-module-openshift-provider/README.md create mode 100644 plugins/auth-backend-module-openshift-provider/catalog-info.yaml create mode 100644 plugins/auth-backend-module-openshift-provider/config.d.ts create mode 100644 plugins/auth-backend-module-openshift-provider/dev/index.ts create mode 100644 plugins/auth-backend-module-openshift-provider/package.json create mode 100644 plugins/auth-backend-module-openshift-provider/report.api.md create mode 100644 plugins/auth-backend-module-openshift-provider/src/authenticator.test.ts create mode 100644 plugins/auth-backend-module-openshift-provider/src/authenticator.ts create mode 100644 plugins/auth-backend-module-openshift-provider/src/index.ts create mode 100644 plugins/auth-backend-module-openshift-provider/src/module.ts create mode 100644 plugins/auth-backend-module-openshift-provider/src/resolvers.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index 48cd018321..6e1e5cb4a7 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -37,6 +37,7 @@ "@backstage/plugin-auth-backend": "workspace:^", "@backstage/plugin-auth-backend-module-github-provider": "workspace:^", "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^", + "@backstage/plugin-auth-backend-module-openshift-provider": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-catalog-backend": "workspace:^", "@backstage/plugin-catalog-backend-module-backstage-openapi": "workspace:^", diff --git a/plugins/auth-backend-module-openshift-provider/.eslintrc.js b/plugins/auth-backend-module-openshift-provider/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend-module-openshift-provider/README.md b/plugins/auth-backend-module-openshift-provider/README.md new file mode 100644 index 0000000000..ae4700d561 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/README.md @@ -0,0 +1,5 @@ +# @backstage/plugin-auth-backend-module-openshift-provider + +The openshift-provider backend module for the auth plugin. + +_This plugin was created through the Backstage CLI_ diff --git a/plugins/auth-backend-module-openshift-provider/catalog-info.yaml b/plugins/auth-backend-module-openshift-provider/catalog-info.yaml new file mode 100644 index 0000000000..3db615244a --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth-backend-module-openshift-provider + title: '@backstage/plugin-auth-backend-module-openshift-provider' + description: The OpenShift backend module for the auth plugin. +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: auth-maintainers diff --git a/plugins/auth-backend-module-openshift-provider/config.d.ts b/plugins/auth-backend-module-openshift-provider/config.d.ts new file mode 100644 index 0000000000..1fdaeb1532 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/config.d.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { HumanDuration } from '@backstage/types'; + +export interface Config { + auth?: { + providers?: { + /** @visibility frontend */ + openshift?: { + [authEnv: string]: { + clientId: string; + /** + * @visibility secret + */ + clientSecret: string; + authorizationUrl: string; + tokenUrl: string; + callbackUrl?: string; + openshiftApiServerUrl: string; + signIn?: { + resolvers: Array<{ + resolver: 'displayNameMatchingUserEntityName'; + dangerouslyAllowSignInWithoutUserInCatalog?: boolean; + }>; + }; + sessionDuration?: HumanDuration | string; + }; + }; + }; + }; +} diff --git a/plugins/auth-backend-module-openshift-provider/dev/index.ts b/plugins/auth-backend-module-openshift-provider/dev/index.ts new file mode 100644 index 0000000000..99f6828292 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/dev/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createBackend } from '@backstage/backend-defaults'; +import authPlugin from '@backstage/plugin-auth-backend'; +import authModuleOpenShiftProvider from '../src'; + +const backend = createBackend(); + +backend.add(authPlugin); +backend.add(authModuleOpenShiftProvider); + +backend.start(); diff --git a/plugins/auth-backend-module-openshift-provider/package.json b/plugins/auth-backend-module-openshift-provider/package.json new file mode 100644 index 0000000000..9fad0b542e --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/package.json @@ -0,0 +1,55 @@ +{ + "name": "@backstage/plugin-auth-backend-module-openshift-provider", + "version": "0.0.0", + "description": "The OpenShift backend module for the auth plugin.", + "backstage": { + "role": "backend-plugin-module", + "pluginId": "auth", + "pluginPackage": "@backstage/plugin-auth-backend" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/auth-backend-module-openshift-provider" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "config.d.ts" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "@backstage/types": "workspace:^", + "passport-oauth2": "^1.8.0", + "zod": "^3.24.2" + }, + "devDependencies": { + "@backstage/backend-defaults": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/plugin-auth-backend": "workspace:^", + "express": "^4.18.2", + "msw": "^2.7.3", + "supertest": "^7.1.0" + }, + "configSchema": "config.d.ts" +} diff --git a/plugins/auth-backend-module-openshift-provider/report.api.md b/plugins/auth-backend-module-openshift-provider/report.api.md new file mode 100644 index 0000000000..aa429e1d47 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/report.api.md @@ -0,0 +1,28 @@ +## API Report File for "@backstage/plugin-auth-backend-module-openshift-provider" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { OAuthAuthenticator } from '@backstage/plugin-auth-node'; +import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node'; +import { PassportProfile } from '@backstage/plugin-auth-node'; + +// @public (undocumented) +const authModuleOpenshiftProvider: BackendFeature; +export default authModuleOpenshiftProvider; + +// @public (undocumented) +export const openshiftAuthenticator: OAuthAuthenticator< + OpenShiftAuthenticatorContext, + PassportProfile +>; + +// @public (undocumented) +export interface OpenShiftAuthenticatorContext { + // (undocumented) + helper: PassportOAuthAuthenticatorHelper; + // (undocumented) + openshiftApiServerUrl: string; +} +``` diff --git a/plugins/auth-backend-module-openshift-provider/src/authenticator.test.ts b/plugins/auth-backend-module-openshift-provider/src/authenticator.test.ts new file mode 100644 index 0000000000..a5875ed35c --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/src/authenticator.test.ts @@ -0,0 +1,338 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { setupServer } from 'msw/node'; +import { + decodeOAuthState, + encodeOAuthState, +} from '@backstage/plugin-auth-node'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; +import { http, HttpResponse } from 'msw'; +import { openshiftAuthenticator } from './authenticator'; +import { ConfigReader } from '@backstage/config'; +import { + OAuthState, + OAuthAuthenticatorStartInput, + OAuthAuthenticatorAuthenticateInput, +} from '@backstage/plugin-auth-node'; +import express from 'express'; + +describe('openshiftAuthenticator', () => { + let implementation: any; + let oauthState: OAuthState; + + const mswServer = setupServer(); + registerMswTestHooks(mswServer); + + beforeEach(() => { + mswServer.use( + http.post('https://openshift.test/oauth/token', () => { + return HttpResponse.json({ + access_token: 'accessToken', + scope: 'user:full', + expires_in: 60 * 60 * 24, + }); + }), + http.get( + 'https://api.openshift.test/apis/user.openshift.io/v1/users/~', + async () => { + return HttpResponse.json({ + kind: 'User', + apiVersion: 'user.openshift.io/v1', + metadata: { + name: 'alice', + uid: 'ca993628-8817-4a3b-9811-be4a34c60bf4', + resourceVersion: '1', + creationTimestamp: '2022-01-11T13:10:45Z', + managedFields: [], + }, + fullName: 'Alice Adams', + identities: ['SSO:id'], + groups: ['system:authenticated', 'system:authenticated:oauth'], + }); + }, + ), + http.delete( + 'https://api.openshift.test/apis/oauth.openshift.io/v1/oauthaccesstokens/:id', + ({ params }) => { + const { id } = params; + + if (typeof id !== 'string') { + return new Response(null, { status: 401 }); + } + + if (!id.startsWith('sha256~')) { + return new Response(null, { status: 401 }); + } + + return new Response(null, { status: 200 }); + }, + ), + ); + + implementation = openshiftAuthenticator.initialize({ + callbackUrl: 'https://backstage.test/callback', + config: new ConfigReader({ + clientId: 'clientId', + clientSecret: 'clientSecret', + authorizationUrl: 'https://openshift.test/oauth/authorize', + tokenUrl: 'https://openshift.test/oauth/token', + openshiftApiServerUrl: 'https://api.openshift.test', + }), + }); + + oauthState = { + nonce: 'nonce', + env: 'env', + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('#start', () => { + let fakeSession: Record; + let startRequest: OAuthAuthenticatorStartInput; + + beforeEach(() => { + fakeSession = {}; + startRequest = { + state: encodeOAuthState(oauthState), + req: { + method: 'GET', + url: 'test', + session: fakeSession, + }, + } as unknown as OAuthAuthenticatorStartInput; + }); + + it('initiates authorization code grant', async () => { + const startResponse = await openshiftAuthenticator.start( + startRequest, + implementation, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('response_type')).toBe('code'); + }); + + it('passes client ID from config', async () => { + const startResponse = await openshiftAuthenticator.start( + startRequest, + implementation, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('client_id')).toBe('clientId'); + }); + + it('passes callback URL from config', async () => { + const startResponse = await openshiftAuthenticator.start( + startRequest, + implementation, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('redirect_uri')).toBe( + 'https://backstage.test/callback', + ); + }); + + it('encodes OAuth state in query param', async () => { + const startResponse = await openshiftAuthenticator.start( + startRequest, + implementation, + ); + const { searchParams } = new URL(startResponse.url); + const stateParam = searchParams.get('state'); + const decodedState = decodeOAuthState(stateParam!); + + expect(decodedState).toMatchObject(oauthState); + }); + }); + + describe('#authenticate', () => { + let handlerRequest: OAuthAuthenticatorAuthenticateInput; + + beforeEach(() => { + handlerRequest = { + req: { + method: 'GET', + query: { + code: 'authorization_code', + state: encodeOAuthState(oauthState), + }, + session: { + 'oauth2:openshift': { + state: encodeOAuthState(oauthState), + }, + }, + } as unknown as express.Request, + }; + }); + + it('exchanges authorization code for access token', async () => { + const authenticatorResult = await openshiftAuthenticator.authenticate( + handlerRequest, + implementation, + ); + const accessToken = authenticatorResult.session.accessToken; + + expect(accessToken).toEqual('accessToken'); + }); + + it('returns granted scope', async () => { + const authenticatorResult = await openshiftAuthenticator.authenticate( + handlerRequest, + implementation, + ); + const responseScope = authenticatorResult.session.scope; + + expect(responseScope).toEqual('user:full'); + }); + + it('returns a default session.tokentype field', async () => { + const authenticatorResult = await openshiftAuthenticator.authenticate( + handlerRequest, + implementation, + ); + const tokenType = authenticatorResult.session.tokenType; + + expect(tokenType).toEqual('bearer'); + }); + + it('returns displayName', async () => { + const authenticatorResult = await openshiftAuthenticator.authenticate( + handlerRequest, + implementation, + ); + + expect(authenticatorResult).toMatchObject({ + fullProfile: { + displayName: 'alice', + }, + }); + }); + + it('should store access token as refresh token', async () => { + const authenticatorResult = await openshiftAuthenticator.authenticate( + handlerRequest, + implementation, + ); + + expect(authenticatorResult.session.refreshToken).toBe( + authenticatorResult.session.accessToken, + ); + }); + }); + + describe('#refresh', () => { + it('gets new refresh token (access token)', async () => { + const refreshResponse = await openshiftAuthenticator.refresh( + { + scope: 'user:full', + refreshToken: 'access-token', + req: {} as express.Request, + }, + implementation, + ); + + expect(refreshResponse.session.refreshToken).toBe('access-token'); + }); + + it('should throw error when invalid access token was provided', async () => { + mswServer.use( + http.get( + 'https://api.openshift.test/apis/user.openshift.io/v1/users/~', + async () => { + return HttpResponse.json( + { + kind: 'Status', + apiVersion: 'v1', + metadata: {}, + status: 'Failure', + message: 'Unauthorized', + reason: 'Unauthorized', + code: 401, + }, + { + status: 401, + }, + ); + }, + ), + ); + + await expect( + openshiftAuthenticator.refresh( + { + scope: 'user:full', + refreshToken: 'invalid-access-token', + req: {} as express.Request, + }, + implementation, + ), + ).rejects.toThrow('HTTP error! Status: 401'); + }); + }); + + describe('#logout', () => { + it('should delete valid access token', async () => { + await expect( + openshiftAuthenticator.logout?.( + { + refreshToken: 'access-token', + req: {} as express.Request, + }, + implementation, + ), + ).resolves.not.toThrow(); + }); + + it('should throw when refresh token is not set', async () => { + await expect( + openshiftAuthenticator.logout?.( + { + req: {} as express.Request, + }, + implementation, + ), + ).rejects.toThrow(); + }); + + it('should throw when access cannot be deleted', async () => { + mswServer.use( + http.delete( + 'https://api.openshift.test/apis/oauth.openshift.io/v1/oauthaccesstokens/:id', + () => { + return new Response(null, { status: 401 }); + }, + ), + ); + + await expect( + openshiftAuthenticator.logout?.( + { + refreshToken: 'access-token', + req: {} as express.Request, + }, + implementation, + ), + ).rejects.toThrow(); + }); + }); +}); diff --git a/plugins/auth-backend-module-openshift-provider/src/authenticator.ts b/plugins/auth-backend-module-openshift-provider/src/authenticator.ts new file mode 100644 index 0000000000..0c9acb6287 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/src/authenticator.ts @@ -0,0 +1,184 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createOAuthAuthenticator, + PassportOAuthAuthenticatorHelper, + PassportOAuthDoneCallback, + PassportProfile, +} from '@backstage/plugin-auth-node'; +import { createHash } from 'node:crypto'; +import OAuth2Strategy from 'passport-oauth2'; +import { z } from 'zod'; + +/** @public */ +export interface OpenShiftAuthenticatorContext { + openshiftApiServerUrl: string; + helper: PassportOAuthAuthenticatorHelper; +} + +/** @private + * Schema for user.openshift.io/v1, + * see https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/user_and_group_apis/user-user-openshift-io-v1#user-user-openshift-io-v1 + */ +const OpenShiftUser = z.object({ + metadata: z.object({ + name: z.string(), + }), +}); + +/** @public */ +export const openshiftAuthenticator = createOAuthAuthenticator< + OpenShiftAuthenticatorContext, + PassportProfile +>({ + defaultProfileTransform: + PassportOAuthAuthenticatorHelper.defaultProfileTransform, + scopes: { + required: ['user:full'], + }, + initialize({ callbackUrl, config }) { + const clientId = config.getString('clientId'); + const clientSecret = config.getString('clientSecret'); + const authorizationUrl = config.getString('authorizationUrl'); + const tokenUrl = config.getString('tokenUrl'); + const openshiftApiServerUrl = config.getString('openshiftApiServerUrl'); + + // userUrl: `${openshiftApiServerUrl}/apis/user.openshift.io/v1/users/~`, + const strategy = new OAuth2Strategy( + { + clientID: clientId, + clientSecret: clientSecret, + callbackURL: callbackUrl, + authorizationURL: authorizationUrl, + tokenURL: tokenUrl, + passReqToCallback: false, + }, + ( + accessToken: any, + refreshToken: string, + params: any, + fullProfile: PassportProfile, + done: PassportOAuthDoneCallback, + ) => { + done(undefined, { fullProfile, params, accessToken }, { refreshToken }); + }, + ); + + strategy.userProfile = function userProfile( + accessToken: string, + done: (err?: unknown, profile?: any) => void, + ): void { + this._oauth2.useAuthorizationHeaderforGET(true); + + this._oauth2.get( + `${openshiftApiServerUrl}/apis/user.openshift.io/v1/users/~`, + accessToken, + (error, data, _) => { + if (error !== null && error.statusCode !== 200) { + done(new Error(`HTTP error! Status: ${error.statusCode}`)); + return; + } + + if (!data) { + done(new Error('No data provided!')); + return; + } + + if (typeof data !== 'string') { + done(new Error('Data of type Buffer is not supported!')); + return; + } + + const user = OpenShiftUser.parse(JSON.parse(data)); + done(null, { displayName: user.metadata.name }); + }, + ); + }; + + return { + openshiftApiServerUrl, + helper: PassportOAuthAuthenticatorHelper.from(strategy), + }; + }, + async start(input, { helper }) { + return helper.start(input, { + accessType: 'offline', + prompt: 'consent', + }); + }, + async authenticate(input, { helper }) { + // Same workaround as the GitHub provider; see https://github.com/backstage/backstage/issues/25383 + const { fullProfile, session } = await helper.authenticate(input); + session.refreshToken = session.accessToken; + session.refreshTokenExpiresInSeconds = session.expiresInSeconds; + return { fullProfile, session }; + }, + async refresh(input, { helper }) { + // Because the session is refreshed on login, this override is crucial, + // see https://github.com/backstage/backstage/issues/25383 + const accessToken = input.refreshToken; + + const fullProfile = await helper.fetchProfile(accessToken).catch(error => { + if (error.oauthError?.statusCode === 401) { + throw new Error('Invalid access token'); + } + throw error; + }); + + return { + fullProfile, + session: { + accessToken, + tokenType: 'bearer', + scope: input.scope, + refreshToken: input.refreshToken, + }, + }; + }, + async logout(input, { openshiftApiServerUrl, helper }) { + // Due to the implementation of createOAuthRouteHandlers, only the refresh token is set. + // In this provider, the refresh token actually IS the access token. + const accessToken = input.refreshToken; + if (!accessToken) { + throw new Error('access token/refresh token needs to be set for logout'); + } + + // Check if access token is still valid. + try { + await helper.fetchProfile(accessToken); + } catch { + // Invalid token, no need to delete OAuthAccessToken. + return; + } + + // Calculate token name, see: + // https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/oauth_apis/oauthaccesstoken-oauth-openshift-io-v1#apis-oauth-openshift-io-v1-oauthaccesstokens + const tokenName = createHash('sha256') + .update(accessToken.slice('sha256~'.length)) + .digest() + .toString('base64url'); + + const response = await fetch( + `${openshiftApiServerUrl}/apis/oauth.openshift.io/v1/oauthaccesstokens/sha256~${tokenName}`, + { method: 'DELETE', headers: { Authorization: `Bearer ${accessToken}` } }, + ); + + if (response.status === 401) { + throw new Error('unauthorized'); + } + }, +}); diff --git a/plugins/auth-backend-module-openshift-provider/src/index.ts b/plugins/auth-backend-module-openshift-provider/src/index.ts new file mode 100644 index 0000000000..4c8dd6cb22 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/src/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * The openshift-provider backend module for the auth plugin. + * + * @packageDocumentation + */ +export { + openshiftAuthenticator, + type OpenShiftAuthenticatorContext, +} from './authenticator'; +export { authModuleOpenshiftProvider as default } from './module'; diff --git a/plugins/auth-backend-module-openshift-provider/src/module.ts b/plugins/auth-backend-module-openshift-provider/src/module.ts new file mode 100644 index 0000000000..18d96c4778 --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/src/module.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { + authProvidersExtensionPoint, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { openshiftAuthenticator } from './authenticator'; +import { openshiftSignInResolvers } from './resolvers'; + +/** @public */ +export const authModuleOpenshiftProvider = createBackendModule({ + pluginId: 'auth', + moduleId: 'openshift-provider', + register(reg) { + reg.registerInit({ + deps: { providers: authProvidersExtensionPoint }, + async init({ providers }) { + providers.registerProvider({ + providerId: 'openshift', + factory: createOAuthProviderFactory({ + authenticator: openshiftAuthenticator, + signInResolverFactories: { + ...openshiftSignInResolvers, + }, + }), + }); + }, + }); + }, +}); diff --git a/plugins/auth-backend-module-openshift-provider/src/resolvers.ts b/plugins/auth-backend-module-openshift-provider/src/resolvers.ts new file mode 100644 index 0000000000..dee55ec4ca --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/src/resolvers.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createSignInResolverFactory, + OAuthAuthenticatorResult, + PassportProfile, + SignInInfo, +} from '@backstage/plugin-auth-node'; + +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { z } from 'zod'; + +export namespace openshiftSignInResolvers { + export const displayNameMatchingUserEntityName = createSignInResolverFactory({ + optionsSchema: z + .object({ + dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(), + }) + .optional(), + create(options = {}) { + return async ( + info: SignInInfo>, + ctx, + ) => { + const { displayName } = info.profile; + + if (!displayName) { + throw new Error( + `OpenShift user profile does not contain a displayName`, + ); + } + + const userRef = stringifyEntityRef({ + kind: 'User', + name: displayName, + namespace: DEFAULT_NAMESPACE, + }); + + return await ctx.signInWithCatalogUser( + { entityRef: userRef }, + { + dangerousEntityRefFallback: + options?.dangerouslyAllowSignInWithoutUserInCatalog + ? { entityRef: { name: displayName } } + : undefined, + }, + ); + }; + }, + }); +} diff --git a/yarn.lock b/yarn.lock index 853749b99e..5642a3bf10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4120,6 +4120,27 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-auth-backend-module-openshift-provider@workspace:^, @backstage/plugin-auth-backend-module-openshift-provider@workspace:plugins/auth-backend-module-openshift-provider": + version: 0.0.0-use.local + resolution: "@backstage/plugin-auth-backend-module-openshift-provider@workspace:plugins/auth-backend-module-openshift-provider" + dependencies: + "@backstage/backend-defaults": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/plugin-auth-backend": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" + "@backstage/types": "workspace:^" + express: "npm:^4.18.2" + msw: "npm:^2.7.3" + passport-oauth2: "npm:^1.8.0" + supertest: "npm:^7.1.0" + zod: "npm:^3.24.2" + languageName: unknown + linkType: soft + "@backstage/plugin-auth-backend-module-pinniped-provider@workspace:plugins/auth-backend-module-pinniped-provider": version: 0.0.0-use.local resolution: "@backstage/plugin-auth-backend-module-pinniped-provider@workspace:plugins/auth-backend-module-pinniped-provider" @@ -11081,9 +11102,9 @@ __metadata: languageName: node linkType: hard -"@mswjs/interceptors@npm:^0.39.1": - version: 0.39.2 - resolution: "@mswjs/interceptors@npm:0.39.2" +"@mswjs/interceptors@npm:^0.37.0": + version: 0.37.1 + resolution: "@mswjs/interceptors@npm:0.37.1" dependencies: "@open-draft/deferred-promise": "npm:^2.2.0" "@open-draft/logger": "npm:^0.3.0" @@ -11091,7 +11112,7 @@ __metadata: is-node-process: "npm:^1.2.0" outvariant: "npm:^1.4.3" strict-event-emitter: "npm:^0.5.1" - checksum: 10/faaa95d636363a197f125c32066457fa74d5063d8ccae4c9c0e0510179060d92b1faf8640df45a0623e0bf42a30d610c83364a58e0eb0ca412c87b2e835936c1 + checksum: 10/332d8aa50beb4834ccbda6a800ca00b1204adc0eba23e1c1f7bb9f4e564a92707e563f7a2424d4a8607404ec91424e5d8c34a87c250b191ca7b24dff12eba2c5 languageName: node linkType: hard @@ -26393,10 +26414,10 @@ __metadata: languageName: node linkType: hard -"component-emitter@npm:^1.3.1": - version: 1.3.1 - resolution: "component-emitter@npm:1.3.1" - checksum: 10/94550aa462c7bd5a61c1bc480e28554aa306066930152d1b1844a0dd3845d4e5db7e261ddec62ae184913b3e59b55a2ad84093b9d3596a8f17c341514d6c483d +"component-emitter@npm:^1.3.0": + version: 1.3.0 + resolution: "component-emitter@npm:1.3.0" + checksum: 10/dfc1ec2e7aa2486346c068f8d764e3eefe2e1ca0b24f57506cd93b2ae3d67829a7ebd7cc16e2bf51368fac2f45f78fcff231718e40b1975647e4a86be65e1d05 languageName: node linkType: hard @@ -27620,15 +27641,15 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0": - version: 4.4.1 - resolution: "debug@npm:4.4.1" +"debug@npm:4, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0": + version: 4.4.0 + resolution: "debug@npm:4.4.0" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10/8e2709b2144f03c7950f8804d01ccb3786373df01e406a0f66928e47001cf2d336cbed9ee137261d4f90d68d8679468c755e3548ed83ddacdc82b194d2468afe + checksum: 10/1847944c2e3c2c732514b93d11886575625686056cd765336212dc15de2d2b29612b6cd80e1afba767bb8e1803b778caf9973e98169ef1a24a7a7009e1820367 languageName: node linkType: hard @@ -30049,6 +30070,7 @@ __metadata: "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-backend-module-github-provider": "workspace:^" "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^" + "@backstage/plugin-auth-backend-module-openshift-provider": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-catalog-backend": "workspace:^" "@backstage/plugin-catalog-backend-module-backstage-openapi": "workspace:^" @@ -31103,7 +31125,7 @@ __metadata: languageName: node linkType: hard -"formidable@npm:^3.5.4": +"formidable@npm:^3.5.1": version: 3.5.4 resolution: "formidable@npm:3.5.4" dependencies: @@ -38633,15 +38655,15 @@ __metadata: languageName: node linkType: hard -"msw@npm:^2.0.0, msw@npm:^2.0.8": - version: 2.10.4 - resolution: "msw@npm:2.10.4" +"msw@npm:^2.0.0, msw@npm:^2.0.8, msw@npm:^2.7.3": + version: 2.7.3 + resolution: "msw@npm:2.7.3" dependencies: "@bundled-es-modules/cookie": "npm:^2.0.1" "@bundled-es-modules/statuses": "npm:^1.0.1" "@bundled-es-modules/tough-cookie": "npm:^0.1.6" "@inquirer/confirm": "npm:^5.0.0" - "@mswjs/interceptors": "npm:^0.39.1" + "@mswjs/interceptors": "npm:^0.37.0" "@open-draft/deferred-promise": "npm:^2.2.0" "@open-draft/until": "npm:^2.1.0" "@types/cookie": "npm:^0.6.0" @@ -38662,7 +38684,7 @@ __metadata: optional: true bin: msw: cli/index.js - checksum: 10/e2f25dda1aba66c7444c29c41d3157cb15c0332055ab7ebfb74ef4b506e7b90098cf37c577768edb5b2b2dbf0d6ed6a7a3ca8ee6da3d72df5a25823d82f33316 + checksum: 10/f193329a68fc22e477a6f8504aa44a92bd12847f2eeac1dfbd8ec1cc43ff293112ec067de1c7fe312ba02beecb313fb00aeeebf5817432b57af2d796b2dff2fa languageName: node linkType: hard @@ -40612,7 +40634,7 @@ __metadata: languageName: node linkType: hard -"passport-oauth2@npm:1.8.0, passport-oauth2@npm:1.x.x, passport-oauth2@npm:^1.1.2, passport-oauth2@npm:^1.4.0, passport-oauth2@npm:^1.6.0, passport-oauth2@npm:^1.6.1, passport-oauth2@npm:^1.7.0": +"passport-oauth2@npm:1.8.0, passport-oauth2@npm:1.x.x, passport-oauth2@npm:^1.1.2, passport-oauth2@npm:^1.4.0, passport-oauth2@npm:^1.6.0, passport-oauth2@npm:^1.6.1, passport-oauth2@npm:^1.7.0, passport-oauth2@npm:^1.8.0": version: 1.8.0 resolution: "passport-oauth2@npm:1.8.0" dependencies: @@ -42268,7 +42290,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.10.1, qs@npm:^6.10.3, qs@npm:^6.11.2, qs@npm:^6.12.2, qs@npm:^6.12.3, qs@npm:^6.14.0, qs@npm:^6.7.0, qs@npm:^6.9.4": +"qs@npm:^6.10.1, qs@npm:^6.10.3, qs@npm:^6.11.0, qs@npm:^6.11.2, qs@npm:^6.12.2, qs@npm:^6.12.3, qs@npm:^6.14.0, qs@npm:^6.7.0, qs@npm:^6.9.4": version: 6.14.0 resolution: "qs@npm:6.14.0" dependencies: @@ -46507,30 +46529,30 @@ __metadata: languageName: node linkType: hard -"superagent@npm:^10.2.3": - version: 10.2.3 - resolution: "superagent@npm:10.2.3" +"superagent@npm:^9.0.1": + version: 9.0.2 + resolution: "superagent@npm:9.0.2" dependencies: - component-emitter: "npm:^1.3.1" + component-emitter: "npm:^1.3.0" cookiejar: "npm:^2.1.4" - debug: "npm:^4.3.7" + debug: "npm:^4.3.4" fast-safe-stringify: "npm:^2.1.1" - form-data: "npm:^4.0.4" - formidable: "npm:^3.5.4" + form-data: "npm:^4.0.0" + formidable: "npm:^3.5.1" methods: "npm:^1.1.2" mime: "npm:2.6.0" - qs: "npm:^6.11.2" - checksum: 10/377bf938e68927dd772169c5285be27872bf6e84fac01c52bcd9396bc5b348c9ded8f8be54649510ec09a67bc5096055847b37cb01b3bca0eb06ff1856170e35 + qs: "npm:^6.11.0" + checksum: 10/d3c0c9051ceec84d5b431eaa410ad81bcd53255cea57af1fc66d683a24c34f3ba4761b411072a9bf489a70e3d5b586a78a0e6f2eac6a561067e7d196ddab0907 languageName: node linkType: hard -"supertest@npm:^7.0.0": - version: 7.1.4 - resolution: "supertest@npm:7.1.4" +"supertest@npm:^7.0.0, supertest@npm:^7.1.0": + version: 7.1.0 + resolution: "supertest@npm:7.1.0" dependencies: methods: "npm:^1.1.2" - superagent: "npm:^10.2.3" - checksum: 10/ecb5d41f2b62b257dbdcabac245c32b8e8fb264fe2636dd85c2c883569d23dc14adc0a471abb84187cbdb49bc36ad870ad355b4a0b85973f510fd57fc229e6cc + superagent: "npm:^9.0.1" + checksum: 10/20069f739a44821dfa4f7f397b9086ef31a358366331138f97945eedb2e231796e7c55b032125d3bd12f9839f089fbb809893dbc0f98edc57e12333b9f42b726 languageName: node linkType: hard @@ -50029,10 +50051,10 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.22.4, zod@npm:^3.23.8": - version: 3.25.76 - resolution: "zod@npm:3.25.76" - checksum: 10/f0c963ec40cd96858451d1690404d603d36507c1fc9682f2dae59ab38b578687d542708a7fdbf645f77926f78c9ed558f57c3d3aa226c285f798df0c4da16995 +"zod@npm:^3.22.4, zod@npm:^3.23.8, zod@npm:^3.24.2": + version: 3.25.67 + resolution: "zod@npm:3.25.67" + checksum: 10/0e35432dcca7f053e63f5dd491a87c78abe0d981817547252c3b6d05f0f58788695d1a69724759c6501dff3fd62929be24c9f314a3625179bee889150f7a61fa languageName: node linkType: hard From 909a5cc65aa33b35d654c38331896ce6e5aff199 Mon Sep 17 00:00:00 2001 From: Yannik Daellenbach Date: Sat, 12 Apr 2025 11:55:36 +0200 Subject: [PATCH 043/233] Add `openshiftAuthApiRef` and `OpenShiftAuth` to core API Signed-off-by: Yannik Daellenbach --- packages/core-app-api/report.api.md | 7 +++ .../src/apis/implementations/auth/index.ts | 1 + .../auth/openshift/OpenShiftAuth.ts | 52 +++++++++++++++++++ .../implementations/auth/openshift/index.ts | 17 ++++++ packages/core-plugin-api/report.api.md | 5 ++ .../src/apis/definitions/auth.ts | 17 ++++++ 6 files changed, 99 insertions(+) create mode 100644 packages/core-app-api/src/apis/implementations/auth/openshift/OpenShiftAuth.ts create mode 100644 packages/core-app-api/src/apis/implementations/auth/openshift/index.ts diff --git a/packages/core-app-api/report.api.md b/packages/core-app-api/report.api.md index 445d73392d..0cecc22f4a 100644 --- a/packages/core-app-api/report.api.md +++ b/packages/core-app-api/report.api.md @@ -52,6 +52,7 @@ import { Observable } from '@backstage/types'; import { oktaAuthApiRef } from '@backstage/core-plugin-api'; import { oneloginAuthApiRef } from '@backstage/core-plugin-api'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; +import { openshiftAuthApiRef } from '@backstage/core-plugin-api'; import { PendingOAuthRequest } from '@backstage/core-plugin-api'; import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; @@ -651,6 +652,12 @@ export type OpenLoginPopupOptions = { height?: number; }; +// @public +export class OpenShiftAuth { + // (undocumented) + static create(options: OAuthApiCreateOptions): typeof openshiftAuthApiRef.T; +} + // @public export type PopupOptions = { size?: diff --git a/packages/core-app-api/src/apis/implementations/auth/index.ts b/packages/core-app-api/src/apis/implementations/auth/index.ts index e02e07961a..00effb9384 100644 --- a/packages/core-app-api/src/apis/implementations/auth/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/index.ts @@ -26,4 +26,5 @@ export * from './bitbucket'; export * from './bitbucketServer'; export * from './atlassian'; export * from './vmwareCloud'; +export * from './openshift'; export type { OAuthApiCreateOptions, AuthApiCreateOptions } from './types'; diff --git a/packages/core-app-api/src/apis/implementations/auth/openshift/OpenShiftAuth.ts b/packages/core-app-api/src/apis/implementations/auth/openshift/OpenShiftAuth.ts new file mode 100644 index 0000000000..1d820189d2 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/openshift/OpenShiftAuth.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { openshiftAuthApiRef } from '@backstage/core-plugin-api'; +import { OAuth2 } from '../oauth2'; +import { OAuthApiCreateOptions } from '../types'; + +const DEFAULT_PROVIDER = { + id: 'openshift', + title: 'OpenShift', + icon: () => null, +}; + +/** + * Implements the OAuth flow to OpenShift + * + * @public + */ +export default class OpenShiftAuth { + static create(options: OAuthApiCreateOptions): typeof openshiftAuthApiRef.T { + const { + configApi, + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = ['user:info'], + } = options; + + return OAuth2.create({ + configApi, + discoveryApi, + oauthRequestApi, + provider, + environment, + defaultScopes, + }); + } +} diff --git a/packages/core-app-api/src/apis/implementations/auth/openshift/index.ts b/packages/core-app-api/src/apis/implementations/auth/openshift/index.ts new file mode 100644 index 0000000000..65452114ae --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/openshift/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { default as OpenShiftAuth } from './OpenShiftAuth'; diff --git a/packages/core-plugin-api/report.api.md b/packages/core-plugin-api/report.api.md index 54bbfb94cf..b40088dc78 100644 --- a/packages/core-plugin-api/report.api.md +++ b/packages/core-plugin-api/report.api.md @@ -604,6 +604,11 @@ export type OpenIdConnectApi = { getIdToken(options?: AuthRequestOptions): Promise; }; +// @public +export const openshiftAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>; + // @public @deprecated export type OptionalParams< Params extends { diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index 9339a3e18a..f8686cb128 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -474,3 +474,20 @@ export const vmwareCloudAuthApiRef: ApiRef< > = createApiRef({ id: 'core.auth.vmware-cloud', }); + +/** + * Provides authentication towards OpenShift APIs and identities. + * + * @public + * @remarks + * + * See {@link https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/authentication_and_authorization/configuring-oauth-clients} + * on how to configure the OAuth clients and + * {@link https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html-single/authentication_and_authorization/index#tokens-scoping-about_configuring-internal-oauth} + * for available scopes. + */ +export const openshiftAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + id: 'core.auth.openshift', +}); From ac720abcf802e33c7a09219536672999705513c9 Mon Sep 17 00:00:00 2001 From: Yannik Daellenbach Date: Sat, 12 Apr 2025 12:01:26 +0200 Subject: [PATCH 044/233] Add OpenShift authenticator to the default user-settings providers page Signed-off-by: Yannik Daellenbach --- .../components/AuthProviders/DefaultProviderSettings.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx index b0ddbf31cf..ce9c1093ca 100644 --- a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx +++ b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx @@ -26,6 +26,7 @@ import { bitbucketServerAuthApiRef, atlassianAuthApiRef, oneloginAuthApiRef, + openshiftAuthApiRef, } from '@backstage/core-plugin-api'; import { userSettingsTranslationRef } from '../../translation'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; @@ -128,6 +129,14 @@ export const DefaultProviderSettings = (props: { icon={Star} /> )} + {configuredProviders.includes('openshift') && ( + + )} ); }; From f80b2f765a37dd20683cd85138b4aa118966bb14 Mon Sep 17 00:00:00 2001 From: Christoph Raaflaub Date: Tue, 8 Apr 2025 09:31:18 +0200 Subject: [PATCH 045/233] Add openshift-provider backend module documentation Signed-off-by: Christoph Raaflaub --- docs/auth/index.md | 1 + docs/auth/openshift/provider.md | 77 +++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 docs/auth/openshift/provider.md diff --git a/docs/auth/index.md b/docs/auth/index.md index 9f4c229e7f..bff1f3df26 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -34,6 +34,7 @@ Backstage comes with many common authentication providers in the core library: - [Okta](okta/provider.md) - [OAuth 2 Custom Proxy](oauth2-proxy/provider.md) - [OneLogin](onelogin/provider.md) +- [OpenShift](openshift/provider.md) - [VMware Cloud](vmware-cloud/provider.md) These built-in providers handle the authentication flow for a particular service, including required scopes, callbacks, etc. These providers are each added to a diff --git a/docs/auth/openshift/provider.md b/docs/auth/openshift/provider.md new file mode 100644 index 0000000000..f208aaca81 --- /dev/null +++ b/docs/auth/openshift/provider.md @@ -0,0 +1,77 @@ +--- +id: provider +title: OpenShift Authentication Provider +sidebar_label: OpenShift +description: Adding OpenShift OAuth as an authentication provider in Backstage +--- + +The Backstage `core-plugin-api` package comes with a OpenShift authentication +provider that can authenticate users using OpenShift OAuth. + +## Use Case + +This setup enables the Kubernetes integration to use the users rights to access the OpenShift clusters (OAuth 2.0 On-Behalf-Of / [Kubernetes Client Side Provider](https://backstage.io/docs/features/kubernetes/authentication/#client-side-providers)). + +The users in Backstage are imported from LDAP using the [LDAP organizational data provider](https://backstage.io/docs/integrations/ldap/org). +The OpenShift OAuth server is connected to an SSO, which is also backed by the same LDAP service. + +With this setup everything is aligned across services. The LDAP relative distinguished name (RDN) matches the name of the OpenShift user entity. + +The OpenShift [built-in OAuth server](https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/authentication_and_authorization/configuring-internal-oauth#oauth-server-metadata_configuring-internal-oauth) is based on OAuth 2.0. Therefore this Auth implementation builds on [passport-oauth2](https://github.com/jaredhanson/passport-oauth2) + +## Create an OAuth client in OpenShift + +Make sure that an OAuth client exists in the OpenShift cluster. + +To configure the OpenShift integration, create an [`OAuthClient`](https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/authentication_and_authorization/configuring-oauth-clients). + +The redirect URI must be in the following format: `https:///api/auth/openshift/handler/frame`. + +## Configuration + +The provider configuration can then be added to your `app-config.yaml` under the +root `auth` configuration: + +```yaml +auth: + environment: development + providers: + openshift: + development: + clientId: ${AUTH_OPENSHIFT_CLIENT_ID} + clientSecret: ${AUTH_OPENSHIFT_CLIENT_SECRET} + authorizationUrl: ${AUTH_OPENSHIFT_AUTHORIZATION_URL} + tokenUrl: ${AUTH_OPENSHIFT_TOKEN_URL} + openshiftApiServerUrl: ${OPENSHIFT_API_SERVER_URL} + signIn: + resolvers: + - resolver: displayNameMatchingUserEntityName +``` + +The OpenShift provider is a structure with these configuration keys: + +- `clientId`: The client ID of your OpenShift OAuth client, e.g., `my-backstage` +- `clientSecret`: The client secret tied to the OpenShift OAuth client. +- `authorizationUrl`: The OpenShift OAuth client auth endpoint, format: `https:///oauth/authorize`. +- `tokenUrl`: The OpenShift OAuth client token endpoint, format: `https:///oauth/token`. +- `openshiftApiServerUrl`: The OpenShift API server endpoint, format: `https://`. +- `signIn`: The configuration for the sign-in process, including the **resolvers** + that should be used to match the user from the auth provider with the user + entity in the Backstage catalog (typically a single resolver is sufficient). + +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-openshift-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-openshift-provider')); +/* highlight-add-end */ +``` From 0173a3d14c82a556bb5255156dcce839bb122953 Mon Sep 17 00:00:00 2001 From: Yannik Daellenbach Date: Thu, 10 Apr 2025 17:52:26 +0200 Subject: [PATCH 046/233] Document `sessionDuration` Signed-off-by: Yannik Daellenbach --- docs/auth/openshift/provider.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/auth/openshift/provider.md b/docs/auth/openshift/provider.md index f208aaca81..ab458a80e9 100644 --- a/docs/auth/openshift/provider.md +++ b/docs/auth/openshift/provider.md @@ -43,6 +43,9 @@ auth: authorizationUrl: ${AUTH_OPENSHIFT_AUTHORIZATION_URL} tokenUrl: ${AUTH_OPENSHIFT_TOKEN_URL} openshiftApiServerUrl: ${OPENSHIFT_API_SERVER_URL} + ## uncomment to set lifespan of user session + # sessionDuration: { hours: 24 } # supports `ms` library format (e.g. '24h', '2 days'), ISO duration, "human duration" as used in code + # sessionDuration: 1d signIn: resolvers: - resolver: displayNameMatchingUserEntityName @@ -55,10 +58,13 @@ The OpenShift provider is a structure with these configuration keys: - `authorizationUrl`: The OpenShift OAuth client auth endpoint, format: `https:///oauth/authorize`. - `tokenUrl`: The OpenShift OAuth client token endpoint, format: `https:///oauth/token`. - `openshiftApiServerUrl`: The OpenShift API server endpoint, format: `https://`. +- `sessionDuration`: (optional): Lifespan of the user session. - `signIn`: The configuration for the sign-in process, including the **resolvers** that should be used to match the user from the auth provider with the user entity in the Backstage catalog (typically a single resolver is sufficient). +The provider needs to use the scope **user:full**. + ## Backend Installation To add the provider to the backend we will first need to install the package by running this command: From 7502dd06787cff1aad2a3025c8603abe33f3e3da Mon Sep 17 00:00:00 2001 From: Yannik Daellenbach Date: Sat, 12 Apr 2025 12:26:10 +0200 Subject: [PATCH 047/233] Add auth provider for OpenShift Signed-off-by: Yannik Daellenbach --- packages/app-defaults/src/defaults/apis.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index 63ace7b4d5..97989dff91 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -35,6 +35,7 @@ import { FetchMiddlewares, VMwareCloudAuth, FrontendHostDiscovery, + OpenShiftAuth, } from '@backstage/core-app-api'; import { @@ -58,6 +59,7 @@ import { bitbucketServerAuthApiRef, atlassianAuthApiRef, vmwareCloudAuthApiRef, + openshiftAuthApiRef, } from '@backstage/core-plugin-api'; import { permissionApiRef, @@ -275,6 +277,22 @@ export const apis = [ }); }, }), + createApiFactory({ + api: openshiftAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => { + return OpenShiftAuth.create({ + configApi, + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }); + }, + }), createApiFactory({ api: permissionApiRef, deps: { From a9ba7c5a262fc44f12044a791be107cc7279e9cc Mon Sep 17 00:00:00 2001 From: Yannik Daellenbach Date: Sat, 12 Apr 2025 12:35:00 +0200 Subject: [PATCH 048/233] Configure example app to support sign in with OpenShift Signed-off-by: Yannik Daellenbach --- packages/app/src/identityProviders.ts | 7 +++++++ packages/backend/src/index.ts | 1 + 2 files changed, 8 insertions(+) diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index 66f1460210..372477cd63 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -23,6 +23,7 @@ import { oneloginAuthApiRef, bitbucketAuthApiRef, bitbucketServerAuthApiRef, + openshiftAuthApiRef, } from '@backstage/core-plugin-api'; export const providers = [ @@ -74,4 +75,10 @@ export const providers = [ message: 'Sign In using Bitbucket Server', apiRef: bitbucketServerAuthApiRef, }, + { + id: 'openshift-auth-provider', + title: 'OpenShift', + message: 'Sign In using OpenShift', + apiRef: openshiftAuthApiRef, + }, ]; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index f46e947b0d..e32d3148b3 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -33,6 +33,7 @@ const searchLoader = createBackendFeatureLoader({ backend.add(import('@backstage/plugin-auth-backend')); backend.add(import('./authModuleGithubProvider')); backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); +backend.add(import('@backstage/plugin-auth-backend-module-openshift-provider')); backend.add(import('@backstage/plugin-app-backend')); backend.add(import('@backstage/plugin-catalog-backend-module-unprocessed')); backend.add( From 5a842530fddef7d8d111620f253df926686f7322 Mon Sep 17 00:00:00 2001 From: Yannik Daellenbach Date: Sat, 12 Apr 2025 12:40:41 +0200 Subject: [PATCH 049/233] Add changeset for init of `auth-backend-module-openshift-provider` Signed-off-by: Yannik Daellenbach --- .changeset/ten-boxes-lie.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ten-boxes-lie.md diff --git a/.changeset/ten-boxes-lie.md b/.changeset/ten-boxes-lie.md new file mode 100644 index 0000000000..751b01e7d9 --- /dev/null +++ b/.changeset/ten-boxes-lie.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-openshift-provider': minor +--- + +Add new `auth-backend-module-openshift-provider`. This authentication provider enables Backstage to sign in with OpenShift. From 51146276696fa5a5bfc5a019f4c2a40ae04f2998 Mon Sep 17 00:00:00 2001 From: Yannik Daellenbach Date: Mon, 19 May 2025 08:57:15 +0200 Subject: [PATCH 050/233] Add changeset for integration of `auth-backend-module-openshift-provider` to the core and `user-settings` Signed-off-by: Yannik Daellenbach --- .changeset/hot-friends-act.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/hot-friends-act.md diff --git a/.changeset/hot-friends-act.md b/.changeset/hot-friends-act.md new file mode 100644 index 0000000000..37b2e3f112 --- /dev/null +++ b/.changeset/hot-friends-act.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': minor +--- + +Make `openshiftAuthApiRef` available in `@backstage/core-plugin-api`. From 3fca9069fe032061a1a1c6cac143d058d2ef18ff Mon Sep 17 00:00:00 2001 From: Yannik Daellenbach Date: Tue, 15 Jul 2025 11:33:02 +0200 Subject: [PATCH 051/233] Add changeset for core-plugin Signed-off-by: Yannik Daellenbach --- .changeset/wet-onions-sneeze.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wet-onions-sneeze.md diff --git a/.changeset/wet-onions-sneeze.md b/.changeset/wet-onions-sneeze.md new file mode 100644 index 0000000000..0fd61d764a --- /dev/null +++ b/.changeset/wet-onions-sneeze.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': minor +--- + +Add `OpenShiftAuth` helper to create default OAuth flow for OpenShift. From 320a9ac35c88a1eab1e1151d65b67e925d580199 Mon Sep 17 00:00:00 2001 From: Yannik Daellenbach Date: Tue, 15 Jul 2025 11:36:03 +0200 Subject: [PATCH 052/233] Add changeset for user-settings Signed-off-by: Yannik Daellenbach --- .changeset/lemon-jobs-create.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lemon-jobs-create.md diff --git a/.changeset/lemon-jobs-create.md b/.changeset/lemon-jobs-create.md new file mode 100644 index 0000000000..d0e28b9c5c --- /dev/null +++ b/.changeset/lemon-jobs-create.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': patch +--- + +Add the OpenShift authenticator provider to the default `user-settings` providers page. From 99567045c546bad5772e8e53b6eb218634bc74fe Mon Sep 17 00:00:00 2001 From: Yannik Daellenbach Date: Tue, 15 Jul 2025 11:40:39 +0200 Subject: [PATCH 053/233] Add changeset for app-defaults Signed-off-by: Yannik Daellenbach --- .changeset/dirty-spies-drop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dirty-spies-drop.md diff --git a/.changeset/dirty-spies-drop.md b/.changeset/dirty-spies-drop.md new file mode 100644 index 0000000000..88bff91892 --- /dev/null +++ b/.changeset/dirty-spies-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/app-defaults': minor +--- + +Add and configure the OpenShift authentication provider to the default APIs. From 2a2f54c7de1dd7a5c8daed0bea499b5f435f371f Mon Sep 17 00:00:00 2001 From: Yannik Daellenbach Date: Mon, 28 Apr 2025 10:39:26 +0200 Subject: [PATCH 054/233] Remove `refresh` and `logout` tests because of false positives in CodeQL action Signed-off-by: Yannik Daellenbach --- .../src/authenticator.test.ts | 112 ------------------ 1 file changed, 112 deletions(-) diff --git a/plugins/auth-backend-module-openshift-provider/src/authenticator.test.ts b/plugins/auth-backend-module-openshift-provider/src/authenticator.test.ts index a5875ed35c..28a7738d21 100644 --- a/plugins/auth-backend-module-openshift-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-openshift-provider/src/authenticator.test.ts @@ -65,22 +65,6 @@ describe('openshiftAuthenticator', () => { }); }, ), - http.delete( - 'https://api.openshift.test/apis/oauth.openshift.io/v1/oauthaccesstokens/:id', - ({ params }) => { - const { id } = params; - - if (typeof id !== 'string') { - return new Response(null, { status: 401 }); - } - - if (!id.startsWith('sha256~')) { - return new Response(null, { status: 401 }); - } - - return new Response(null, { status: 200 }); - }, - ), ); implementation = openshiftAuthenticator.initialize({ @@ -239,100 +223,4 @@ describe('openshiftAuthenticator', () => { ); }); }); - - describe('#refresh', () => { - it('gets new refresh token (access token)', async () => { - const refreshResponse = await openshiftAuthenticator.refresh( - { - scope: 'user:full', - refreshToken: 'access-token', - req: {} as express.Request, - }, - implementation, - ); - - expect(refreshResponse.session.refreshToken).toBe('access-token'); - }); - - it('should throw error when invalid access token was provided', async () => { - mswServer.use( - http.get( - 'https://api.openshift.test/apis/user.openshift.io/v1/users/~', - async () => { - return HttpResponse.json( - { - kind: 'Status', - apiVersion: 'v1', - metadata: {}, - status: 'Failure', - message: 'Unauthorized', - reason: 'Unauthorized', - code: 401, - }, - { - status: 401, - }, - ); - }, - ), - ); - - await expect( - openshiftAuthenticator.refresh( - { - scope: 'user:full', - refreshToken: 'invalid-access-token', - req: {} as express.Request, - }, - implementation, - ), - ).rejects.toThrow('HTTP error! Status: 401'); - }); - }); - - describe('#logout', () => { - it('should delete valid access token', async () => { - await expect( - openshiftAuthenticator.logout?.( - { - refreshToken: 'access-token', - req: {} as express.Request, - }, - implementation, - ), - ).resolves.not.toThrow(); - }); - - it('should throw when refresh token is not set', async () => { - await expect( - openshiftAuthenticator.logout?.( - { - req: {} as express.Request, - }, - implementation, - ), - ).rejects.toThrow(); - }); - - it('should throw when access cannot be deleted', async () => { - mswServer.use( - http.delete( - 'https://api.openshift.test/apis/oauth.openshift.io/v1/oauthaccesstokens/:id', - () => { - return new Response(null, { status: 401 }); - }, - ), - ); - - await expect( - openshiftAuthenticator.logout?.( - { - refreshToken: 'access-token', - req: {} as express.Request, - }, - implementation, - ), - ).rejects.toThrow(); - }); - }); }); From 1845e57a3d27394712ac0a20540543c01e64a68e Mon Sep 17 00:00:00 2001 From: Yannik Daellenbach Date: Fri, 20 Jun 2025 14:38:09 +0200 Subject: [PATCH 055/233] Describe Kubernetes plugin integration as use case Signed-off-by: Yannik Daellenbach --- docs/auth/openshift/provider.md | 57 ++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/docs/auth/openshift/provider.md b/docs/auth/openshift/provider.md index ab458a80e9..37c5064580 100644 --- a/docs/auth/openshift/provider.md +++ b/docs/auth/openshift/provider.md @@ -10,14 +10,61 @@ provider that can authenticate users using OpenShift OAuth. ## Use Case -This setup enables the Kubernetes integration to use the users rights to access the OpenShift clusters (OAuth 2.0 On-Behalf-Of / [Kubernetes Client Side Provider](https://backstage.io/docs/features/kubernetes/authentication/#client-side-providers)). +This setup enables the [Kubernetes plugin](../../features/kubernetes/index.md) to access OpenShift clusters using the user's permissions, +leveraging OAuth 2.0 _On-Behalf-Of_ flow via the [Kubernetes Client Side Provider](../../features/kubernetes/authentication.md). -The users in Backstage are imported from LDAP using the [LDAP organizational data provider](https://backstage.io/docs/integrations/ldap/org). -The OpenShift OAuth server is connected to an SSO, which is also backed by the same LDAP service. +To make this work, the corresponding `User` entities must exist in the Backstage catalog, +and their names must match the OpenShift users. -With this setup everything is aligned across services. The LDAP relative distinguished name (RDN) matches the name of the OpenShift user entity. +Although the OpenShift authentication provider does not support OIDC natively, +you can still configure it for use with the Kubernetes integration by treating it as an OIDC provider +in the `KubernetesAuthProviders` configuration. -The OpenShift [built-in OAuth server](https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/authentication_and_authorization/configuring-internal-oauth#oauth-server-metadata_configuring-internal-oauth) is based on OAuth 2.0. Therefore this Auth implementation builds on [passport-oauth2](https://github.com/jaredhanson/passport-oauth2) +```ts title="packages/app/src/apis.ts" +import { + KubernetesAuthProviders, + kubernetesAuthProvidersApiRef, +} from '@backstage/plugin-kubernetes'; +import { + googleAuthApiRef, + microsoftAuthApiRef, + openshiftAuthApiRef, +} from '@backstage/core-plugin-api'; + +export const apis: AnyApiFactory[] = [ + // ... + createApiFactory({ + api: kubernetesAuthProvidersApiRef, + deps: { + microsoftAuthApi: microsoftAuthApiRef, + googleAuthApi: googleAuthApiRef, + openshiftAuthApi: openshiftAuthApiRef, + }, + factory({ microsoftAuthApi, googleAuthApi, openshiftAuthApi }) { + return new KubernetesAuthProviders({ + microsoftAuthApi, + googleAuthApi, + oidcProviders: { + openshift: { + async getIdToken(_) { + return await openshiftAuthApi.getAccessToken('user:full'); + }, + }, + }, + }); + }, + }), + //... +]; +``` + +:::note Note + +The OpenShift auth API does **not** implement the `OpenIdConnectApi` interface. In other words, it does **not** return an ID token. +Instead, it returns an **access token**, which is used by the Kubernetes integration in place of an ID token. +This is the only functional difference from the standard OIDC-based authentication flow. + +::: ## Create an OAuth client in OpenShift From f6309a56d9542ad450c455f62023f176bb903253 Mon Sep 17 00:00:00 2001 From: Yannik Daellenbach Date: Tue, 15 Jul 2025 11:54:27 +0200 Subject: [PATCH 056/233] Forward `openshiftAuthApiRef` to the new frontend system Signed-off-by: Yannik Daellenbach --- packages/frontend-defaults/src/createApp.test.tsx | 1 + packages/frontend-plugin-api/report.api.md | 3 +++ packages/frontend-plugin-api/src/apis/definitions/auth.ts | 1 + 3 files changed, 5 insertions(+) diff --git a/packages/frontend-defaults/src/createApp.test.tsx b/packages/frontend-defaults/src/createApp.test.tsx index 351bc8f1b5..7b5e345e12 100644 --- a/packages/frontend-defaults/src/createApp.test.tsx +++ b/packages/frontend-defaults/src/createApp.test.tsx @@ -372,6 +372,7 @@ describe('createApp', () => { + diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index e6259c70d7..f9e2336408 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -69,6 +69,7 @@ import { OAuthScope } from '@backstage/core-plugin-api'; import { oktaAuthApiRef } from '@backstage/core-plugin-api'; import { oneloginAuthApiRef } from '@backstage/core-plugin-api'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; +import { openshiftAuthApiRef } from '@backstage/core-plugin-api'; import { PendingOAuthRequest } from '@backstage/core-plugin-api'; import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; @@ -1518,6 +1519,8 @@ export { oneloginAuthApiRef }; export { OpenIdConnectApi }; +export { openshiftAuthApiRef }; + // @public export interface OverridableFrontendPlugin< TRoutes extends { diff --git a/packages/frontend-plugin-api/src/apis/definitions/auth.ts b/packages/frontend-plugin-api/src/apis/definitions/auth.ts index 89509082f0..5a31c1603d 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/auth.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/auth.ts @@ -37,4 +37,5 @@ export { microsoftAuthApiRef, oneloginAuthApiRef, vmwareCloudAuthApiRef, + openshiftAuthApiRef, } from '@backstage/core-plugin-api'; From 894d51497f14137a37d8e7b2aa355bfe827497b7 Mon Sep 17 00:00:00 2001 From: Yannik Daellenbach Date: Tue, 15 Jul 2025 11:58:26 +0200 Subject: [PATCH 057/233] Add changeset for `openshiftApiRef` addition in frontend-plugin-api Signed-off-by: Yannik Daellenbach --- .changeset/tired-cobras-fly.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tired-cobras-fly.md diff --git a/.changeset/tired-cobras-fly.md b/.changeset/tired-cobras-fly.md new file mode 100644 index 0000000000..e2e19803f8 --- /dev/null +++ b/.changeset/tired-cobras-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': minor +--- + +Make `openshiftApiRef` available to the new frontend system. From 3c1d47131e10235355174b4763abb841174ba1e6 Mon Sep 17 00:00:00 2001 From: Yannik Daellenbach Date: Tue, 15 Jul 2025 11:55:37 +0200 Subject: [PATCH 058/233] Add authentication provider implementation for OpenShift to the app plugin Signed-off-by: Yannik Daellenbach --- plugins/app/report.api.md | 15 +++++++++++++++ plugins/app/src/defaultApis.ts | 22 ++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/plugins/app/report.api.md b/plugins/app/report.api.md index 24885bed4b..d12ada15b4 100644 --- a/plugins/app/report.api.md +++ b/plugins/app/report.api.md @@ -539,6 +539,21 @@ const appPlugin: OverridableFrontendPlugin< params: ApiFactory, ) => ExtensionBlueprintParams; }>; + 'api:app/openshift-auth': ExtensionDefinition<{ + kind: 'api'; + name: 'openshift-auth'; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: < + TApi, + TImpl extends TApi, + TDeps extends { [name in string]: unknown }, + >( + params: ApiFactory, + ) => ExtensionBlueprintParams; + }>; 'api:app/permission': ExtensionDefinition<{ kind: 'api'; name: 'permission'; diff --git a/plugins/app/src/defaultApis.ts b/plugins/app/src/defaultApis.ts index 0337f6c4f4..0b4eb5c825 100644 --- a/plugins/app/src/defaultApis.ts +++ b/plugins/app/src/defaultApis.ts @@ -35,6 +35,7 @@ import { createFetchApi, FetchMiddlewares, VMwareCloudAuth, + OpenShiftAuth, } from '../../../packages/core-app-api/src/apis/implementations'; import { @@ -56,6 +57,7 @@ import { bitbucketServerAuthApiRef, atlassianAuthApiRef, vmwareCloudAuthApiRef, + openshiftAuthApiRef, } from '@backstage/core-plugin-api'; import { ApiBlueprint, dialogApiRef } from '@backstage/frontend-plugin-api'; import { @@ -353,6 +355,26 @@ export const apis = [ }, }), }), + ApiBlueprint.make({ + name: 'openshift-auth', + params: defineParams => + defineParams({ + api: openshiftAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => { + return OpenShiftAuth.create({ + configApi, + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }); + }, + }), + }), ApiBlueprint.make({ name: 'permission', params: defineParams => From 99790dbf90fd9454adbe1caf608905c78f3664c8 Mon Sep 17 00:00:00 2001 From: Yannik Daellenbach Date: Tue, 15 Jul 2025 12:01:11 +0200 Subject: [PATCH 059/233] Add changeset for the addition of the OpenShift auth provider to app Signed-off-by: Yannik Daellenbach --- .changeset/kind-eyes-worry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/kind-eyes-worry.md diff --git a/.changeset/kind-eyes-worry.md b/.changeset/kind-eyes-worry.md new file mode 100644 index 0000000000..5568a00e28 --- /dev/null +++ b/.changeset/kind-eyes-worry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app': minor +--- + +Add implementation of OpenShift authentication provider. From 10b54cb6716579661d3398db99de9674242f4a77 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Date: Wed, 3 Sep 2025 09:58:19 +0530 Subject: [PATCH 060/233] Minor doc changes Signed-off-by: Aditya Kumar --- docs/frontend-system/building-plugins/02-testing.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/frontend-system/building-plugins/02-testing.md b/docs/frontend-system/building-plugins/02-testing.md index 21449a5bef..9e5bb1be44 100644 --- a/docs/frontend-system/building-plugins/02-testing.md +++ b/docs/frontend-system/building-plugins/02-testing.md @@ -78,7 +78,7 @@ This pattern also works for many other context providers. An important example i ## Testing extensions -To facilitate testing of frontend extensions, the `@backstage/frontend-test-utils` package provides a tester class which starts up an entire frontend harness, complete with a number of default features. You can then provide overrides for extensions whose behavior you need to adjust for the test run. +To facilitate testing of frontend extensions, the `@backstage/frontend-test-utils` package provides a tester class that starts up an entire frontend harness, complete with a number of default features. You can then provide overrides for extensions whose behavior you need to adjust for the test run. A number of features (frontend extensions and overrides) are also accepted by the tester. Here are some examples of how these facilities can be useful: @@ -92,7 +92,7 @@ import { createExtensionTester } from '@backstage/frontend-test-utils'; import { indexPageExtension } from './plugin'; describe('Index page', () => { - it('should render a the index page', async () => { + it('should render the index page', async () => { await renderInTestApp( createExtensionTester(indexPageExtension).reactElement(), ); @@ -108,7 +108,7 @@ Note that the `.reactElement()` method will look for the `coreExtensionData.reac ### Multiple extensions -In some cases you might need to test multiple extensions together, in particular when testing inputs. In this case, you can add more extensions to the tester instance using the `.add(...)` method. It also accepts an optional options object as the second argument, which you can use to provide configuration for the extension instance. +In some cases, you might need to test multiple extensions together, particularly when testing inputs. In this case, you can add more extensions to the tester instance using the `.add(...)` method. It also accepts an optional options object as the second argument, which you can use to provide configuration for the extension instance. ```tsx import { screen } from '@testing-library/react'; @@ -134,7 +134,7 @@ describe('Index page', async () => { }); ``` -When testing multiple extensions you may sometimes want to access the output of other extensions than the main test subject. You can use the `.query(ext)` method to query a different extension that has been added to the tester, by passing the extension used with the `createExtensionTester(...).add(ext)` +When testing multiple extensions, you may sometimes want to access the output of other extensions than the main test subject. You can use the `.query(ext)` method to query a different extension that has been added to the tester, by passing the extension used with the `createExtensionTester(...).add(ext)` ### Setting configuration @@ -147,7 +147,7 @@ import { createExtensionTester } from '@backstage/frontend-test-utils'; import { indexPageExtension, detailsPageExtension } from './plugin'; describe('Index page', () => { - it('should accepts a custom title via config', async () => { + it('should accept a custom title via config', async () => { const tester = createExtensionTester(indexPageExtension, { // Extension configuration for the index page config: { title: 'Custom page' }, From 1ad3d94a227ca8a80d528ae358f6c040cce1ac2b Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Thu, 19 Jun 2025 13:37:46 +0300 Subject: [PATCH 061/233] feat: allow opening dependency graph in fullscreen Signed-off-by: Hellgren Heikki --- .changeset/quiet-papayas-mate.md | 5 + packages/core-components/package.json | 2 + packages/core-components/report-alpha.api.md | 1 + packages/core-components/report.api.md | 1 + .../DependencyGraph/DependencyGraph.test.tsx | 25 +-- .../DependencyGraph/DependencyGraph.tsx | 194 ++++++++++++------ packages/core-components/src/translation.ts | 3 + yarn.lock | 27 +++ 8 files changed, 181 insertions(+), 77 deletions(-) create mode 100644 .changeset/quiet-papayas-mate.md diff --git a/.changeset/quiet-papayas-mate.md b/.changeset/quiet-papayas-mate.md new file mode 100644 index 0000000000..1d2089f4e8 --- /dev/null +++ b/.changeset/quiet-papayas-mate.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Dependency graph can now be opened in full screen mode diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 509a3bb38f..ba7e98aa96 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -80,6 +80,7 @@ "pluralize": "^8.0.0", "qs": "^6.9.4", "rc-progress": "3.5.1", + "react-full-screen": "^1.1.1", "react-helmet": "6.1.0", "react-hook-form": "^7.12.2", "react-idle-timer": "5.7.2", @@ -106,6 +107,7 @@ "@types/d3-selection": "^3.0.1", "@types/d3-shape": "^3.0.1", "@types/d3-zoom": "^3.0.1", + "@types/fscreen": "^1", "@types/google-protobuf": "^3.7.2", "@types/react": "^18.0.0", "@types/react-helmet": "^6.1.0", diff --git a/packages/core-components/report-alpha.api.md b/packages/core-components/report-alpha.api.md index 32bd2f4939..96ddebcd3a 100644 --- a/packages/core-components/report-alpha.api.md +++ b/packages/core-components/report-alpha.api.md @@ -61,6 +61,7 @@ export const coreComponentsTranslationRef: TranslationRef< readonly 'alertDisplay.message_other': '({{ count }} newer messages)'; readonly 'autoLogout.stillTherePrompt.title': 'Logging out due to inactivity'; readonly 'autoLogout.stillTherePrompt.buttonText': "Yes! Don't log me out"; + readonly 'dependencyGraph.fullscreenTooltip': 'Toggle fullscreen'; readonly 'proxiedSignInPage.title': 'You do not appear to be signed in. Please try reloading the browser page.'; } >; diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index fff1d0365a..effd6ac6ed 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -257,6 +257,7 @@ export interface DependencyGraphProps extends SVGProps { acyclicer?: 'greedy'; align?: DependencyGraphTypes.Alignment; + allowFullscreen?: boolean; curve?: 'curveStepBefore' | 'curveMonotoneX'; defs?: JSX.Element | JSX.Element[]; direction?: DependencyGraphTypes.Direction; diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.test.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.test.tsx index 6535808059..0dbf3a36b9 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.test.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import { render } from '@testing-library/react'; import { DependencyGraph } from './DependencyGraph'; import { DependencyGraphTypes as Types } from './types'; import { EDGE_TEST_ID, LABEL_TEST_ID, NODE_TEST_ID } from './constants'; +import { renderInTestApp } from '@backstage/test-utils'; describe('', () => { beforeAll(() => { @@ -36,9 +36,8 @@ describe('', () => { const CUSTOM_TEST_ID = 'custom-test-id'; it('renders each node and edge supplied', async () => { - const { getByText, queryAllByTestId, findAllByTestId } = render( - , - ); + const { getByText, queryAllByTestId, findAllByTestId } = + await renderInTestApp(); const renderedNodes = await findAllByTestId(NODE_TEST_ID); expect(renderedNodes).toHaveLength(3); expect(getByText(nodes[0].id)).toBeInTheDocument(); @@ -49,9 +48,10 @@ describe('', () => { }); it('update render if already referenced nodes are added later', async () => { - const { getByText, queryAllByTestId, findAllByTestId, rerender } = render( - , - ); + const { getByText, queryAllByTestId, findAllByTestId, rerender } = + await renderInTestApp( + , + ); let renderedNodes = await findAllByTestId(NODE_TEST_ID); expect(renderedNodes).toHaveLength(2); @@ -75,9 +75,10 @@ describe('', () => { { ...edges[0], label: 'first' }, { ...edges[1], label: 'second' }, ]; - const { getByText, getAllByTestId, findAllByTestId } = render( - , - ); + const { getByText, getAllByTestId, findAllByTestId } = + await renderInTestApp( + , + ); const renderedEdges = await findAllByTestId(EDGE_TEST_ID); expect(renderedEdges).toHaveLength(2); expect(getAllByTestId(LABEL_TEST_ID)).toHaveLength(2); @@ -94,7 +95,7 @@ describe('', () => { ); - const { getByText, findByTestId, container } = render( + const { getByText, findByTestId, container } = await renderInTestApp( , ); const node = await findByTestId(CUSTOM_TEST_ID); @@ -112,7 +113,7 @@ describe('', () => { ); - const { getByText, findByTestId, container } = render( + const { getByText, findByTestId, container } = await renderInTestApp( ({ + root: { + overflow: 'hidden', + minHeight: '100%', + minWidth: '100%', + }, + fullscreen: { + backgroundColor: theme.palette.background.paper, + }, +})); /** * Properties of {@link DependencyGraph} @@ -181,9 +200,18 @@ export interface DependencyGraphProps * Default: 'grow' */ fit?: 'grow' | 'contain'; + /** + * Controls if user can toggle fullscreen mode + * + * @remarks + * + * Default: true + */ + allowFullscreen?: boolean; } const WORKSPACE_ID = 'workspace'; +const DEPENDENCY_GRAPH_SVG = 'dependency-graph'; /** * Graph component used to visualize relations between entities @@ -216,11 +244,15 @@ export function DependencyGraph( curve = 'curveMonotoneX', showArrowHeads = false, fit = 'grow', + allowFullscreen = true, ...svgProps } = props; const theme = useTheme(); const [containerWidth, setContainerWidth] = useState(100); const [containerHeight, setContainerHeight] = useState(100); + const fullScreenHandle = useFullScreenHandle(); + const styles = useStyles(); + const { t } = useTranslationRef(coreComponentsTranslationRef); const graph = useRef>>( new dagre.graphlib.Graph(), @@ -242,11 +274,17 @@ export function DependencyGraph( const containerRef = useMemo( () => - debounce((node: SVGSVGElement) => { - if (!node) { + debounce((root: HTMLDivElement) => { + if (!root) { return; } // Set up zooming + panning + const node: SVGSVGElement = root.querySelector( + `svg#${DEPENDENCY_GRAPH_SVG}`, + ) as SVGSVGElement; + if (!node) { + return; + } const container = d3Selection.select(node); const workspace = d3Selection.select(node.getElementById(WORKSPACE_ID)); @@ -282,7 +320,7 @@ export function DependencyGraph( } const { width: newContainerWidth, height: newContainerHeight } = - node.getBoundingClientRect(); + root.getBoundingClientRect(); if (containerWidth !== newContainerWidth) { setContainerWidth(newContainerWidth); } @@ -406,68 +444,94 @@ export function DependencyGraph( } return ( - - - - - - {defs} - - +
+ + {allowFullscreen && ( + + + {fullScreenHandle.active ? ( + + ) : ( + + )} + + + )} + - {graphEdges.map(e => { - const edge = graph.current.edge(e) as GraphEdge; - if (!edge) return null; - return ( - + + - ); - })} - {graphNodes.map((id: string) => { - const node = graph.current.node(id); - if (!node) return null; - return ( - - ); - })} + + {defs} + + + + {graphEdges.map(e => { + const edge = graph.current.edge(e) as GraphEdge; + if (!edge) return null; + return ( + + ); + })} + {graphNodes.map((id: string) => { + const node = graph.current.node(id); + if (!node) return null; + return ( + + ); + })} + + - - + +
); } diff --git a/packages/core-components/src/translation.ts b/packages/core-components/src/translation.ts index b884f0af92..aa644fa909 100644 --- a/packages/core-components/src/translation.ts +++ b/packages/core-components/src/translation.ts @@ -120,6 +120,9 @@ export const coreComponentsTranslationRef = createTranslationRef({ buttonText: "Yes! Don't log me out", }, }, + dependencyGraph: { + fullscreenTooltip: 'Toggle fullscreen', + }, proxiedSignInPage: { title: 'You do not appear to be signed in. Please try reloading the browser page.', diff --git a/yarn.lock b/yarn.lock index b7c187d56d..efecc7d9c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3182,6 +3182,7 @@ __metadata: "@types/d3-selection": "npm:^3.0.1" "@types/d3-shape": "npm:^3.0.1" "@types/d3-zoom": "npm:^3.0.1" + "@types/fscreen": "npm:^1" "@types/google-protobuf": "npm:^3.7.2" "@types/react": "npm:^18.0.0" "@types/react-helmet": "npm:^6.1.0" @@ -3207,6 +3208,7 @@ __metadata: rc-progress: "npm:3.5.1" react: "npm:^18.0.2" react-dom: "npm:^18.0.2" + react-full-screen: "npm:^1.1.1" react-helmet: "npm:6.1.0" react-hook-form: "npm:^7.12.2" react-idle-timer: "npm:5.7.2" @@ -20266,6 +20268,13 @@ __metadata: languageName: node linkType: hard +"@types/fscreen@npm:^1": + version: 1.0.4 + resolution: "@types/fscreen@npm:1.0.4" + checksum: 10/78459a457ce7a6b7d72a5f17fdb54bbeb93c58ab77fd2858aac610fed2435bc4be9e5d2fb9883b6669b7f3a1204115cc2be59a027ab937ee8b5186225d2ea53d + languageName: node + linkType: hard + "@types/git-url-parse@npm:^9.0.0": version: 9.0.3 resolution: "@types/git-url-parse@npm:9.0.3" @@ -31293,6 +31302,13 @@ __metadata: languageName: node linkType: hard +"fscreen@npm:^1.0.2": + version: 1.2.0 + resolution: "fscreen@npm:1.2.0" + checksum: 10/ac50f9ac52a157b8fe6aaecdf9efa7c1cfa90b42a76c3bc6b85372fab05c5a9cd72c1b7f4c2e273eba1a0e630e381fd72ae135fcc57acd05a0943d5d0c21b451 + languageName: node + linkType: hard + "fsevents@npm:2.3.2": version: 2.3.2 resolution: "fsevents@npm:2.3.2" @@ -42823,6 +42839,17 @@ __metadata: languageName: node linkType: hard +"react-full-screen@npm:^1.1.1": + version: 1.1.1 + resolution: "react-full-screen@npm:1.1.1" + dependencies: + fscreen: "npm:^1.0.2" + peerDependencies: + react: ">= 16.8.0" + checksum: 10/70ad927b9d6c485ac46b5bb4b1639ef9a860da28290b3a1c419c42b9c427d78b80e8dba403eb6451458af56838012c81d5e12ef05097395f154defc32fe06c34 + languageName: node + linkType: hard + "react-grid-layout@npm:1.3.4": version: 1.3.4 resolution: "react-grid-layout@npm:1.3.4" From ba9e598c64a654beeb36441772eb6b8d8214a588 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Date: Wed, 3 Sep 2025 11:46:21 +0530 Subject: [PATCH 062/233] Fixed a broken link Signed-off-by: Aditya Kumar --- docs/frontend-system/building-plugins/01-index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/building-plugins/01-index.md b/docs/frontend-system/building-plugins/01-index.md index 283c162fa7..3ccda08db1 100644 --- a/docs/frontend-system/building-plugins/01-index.md +++ b/docs/frontend-system/building-plugins/01-index.md @@ -185,7 +185,7 @@ export const examplePlugin = createFrontendPlugin({ ## Plugin specific extensions -There are many different plugins that you can extend with additional functionality through extensions. One such plugin is [the catalog plugin](../../features/software-catalog/), one of the core features of Backstage. It lets you catalog the software in your organization, where each item in the catalog has its own page that can be populated with tools and information relating to that catalog entity. In this example we will explore how our plugin can provide such a tool to display on an entity page. +There are many different plugins that you can extend with additional functionality through extensions. One such plugin is [the catalog plugin](../../features/software-catalog/index.md), one of the core features of Backstage. It lets you catalog the software in your organization, where each item in the catalog has its own page that can be populated with tools and information relating to that catalog entity. In this example we will explore how our plugin can provide such a tool to display on an entity page. ```tsx title="in src/plugin.ts - An example entity content extension" import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; From 676b704db21df381c9472efa3564b86be964ea1b Mon Sep 17 00:00:00 2001 From: Aditya Kumar Date: Wed, 3 Sep 2025 13:37:26 +0530 Subject: [PATCH 063/233] Fixed a broken link Signed-off-by: Aditya Kumar --- docs/frontend-system/building-plugins/01-index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/building-plugins/01-index.md b/docs/frontend-system/building-plugins/01-index.md index 3ccda08db1..03da20ea73 100644 --- a/docs/frontend-system/building-plugins/01-index.md +++ b/docs/frontend-system/building-plugins/01-index.md @@ -185,7 +185,7 @@ export const examplePlugin = createFrontendPlugin({ ## Plugin specific extensions -There are many different plugins that you can extend with additional functionality through extensions. One such plugin is [the catalog plugin](../../features/software-catalog/index.md), one of the core features of Backstage. It lets you catalog the software in your organization, where each item in the catalog has its own page that can be populated with tools and information relating to that catalog entity. In this example we will explore how our plugin can provide such a tool to display on an entity page. +There are many different plugins that you can extend with additional functionality through extensions. One such plugin is [the catalog plugin](https://backstage.io/docs/features/software-catalog/), one of the core features of Backstage. It lets you catalog the software in your organization, where each item in the catalog has its own page that can be populated with tools and information relating to that catalog entity. In this example we will explore how our plugin can provide such a tool to display on an entity page. ```tsx title="in src/plugin.ts - An example entity content extension" import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; From 2204f5b77edff470b212b6c4ff2a2e58c41e0e44 Mon Sep 17 00:00:00 2001 From: Andreas Berger Date: Wed, 3 Sep 2025 14:53:40 +0200 Subject: [PATCH 064/233] Prevent deadlock in catalog deferred stitching resolves #30843 Signed-off-by: Andreas Berger --- .changeset/late-swans-press.md | 5 + .../stitcher/markForStitching.test.ts | 139 ++++++++++++++++++ .../operations/stitcher/markForStitching.ts | 106 +++++++++---- 3 files changed, 219 insertions(+), 31 deletions(-) create mode 100644 .changeset/late-swans-press.md diff --git a/.changeset/late-swans-press.md b/.changeset/late-swans-press.md new file mode 100644 index 0000000000..987672bd77 --- /dev/null +++ b/.changeset/late-swans-press.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Prevent deadlock in catalog deferred stitching diff --git a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts index 1afa0b9c30..33712b99f4 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts @@ -435,4 +435,143 @@ describe('markForStitching', () => { } }, ); + + const deadlockTestDatabases = TestDatabases.create({ + ids: ['POSTGRES_17', 'POSTGRES_16', 'SQLITE_3'], + disableDocker: false, + }); + it.each(deadlockTestDatabases.eachSupportedId())( + 'reproduces deadlock scenario when concurrent transactions update overlapping entity sets %p', + async databaseId => { + const knex = await deadlockTestDatabases.init(databaseId); + await applyDatabaseMigrations(knex); + + // Setup test data with multiple entities + const entityRefs = [ + 'k:ns/entity-a', + 'k:ns/entity-b', + 'k:ns/entity-c', + 'k:ns/entity-d', + 'k:ns/entity-e', + 'k:ns/entity-f', + ]; + + await knex('refresh_state').insert( + entityRefs.map((ref, i) => ({ + entity_id: `${i + 1}`, + entity_ref: ref, + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: knex.fn.now(), + last_discovery_at: knex.fn.now(), + next_stitch_at: null, + next_stitch_ticket: null, + })), + ); + + // This test attempts to reproduce the deadlock by running concurrent transactions + // that update overlapping sets of entities in different orders + const errorResults = []; + + for (let attempt = 0; attempt < 10; attempt++) { + // Transaction 1: Update entities A, B, C, D, E + const transaction1 = knex.transaction(async trx => { + await markForStitching({ + knex: trx, + strategy: { + mode: 'deferred', + pollingInterval: { seconds: 1 }, + stitchTimeout: { seconds: 1 }, + }, + entityRefs: [ + 'k:ns/entity-a', + 'k:ns/entity-b', + 'k:ns/entity-c', + 'k:ns/entity-d', + 'k:ns/entity-e', + ], + }); + + // Add a small delay to increase chance of collision + await new Promise(resolve => setTimeout(resolve, 10)); + + await markForStitching({ + knex: trx, + strategy: { + mode: 'deferred', + pollingInterval: { seconds: 1 }, + stitchTimeout: { seconds: 1 }, + }, + entityRefs: ['k:ns/entity-f'], + }); + }); + + // Transaction 2: Update entities F, E, D, C, B (reverse order) + const transaction2 = knex.transaction(async trx => { + await markForStitching({ + knex: trx, + strategy: { + mode: 'deferred', + pollingInterval: { seconds: 1 }, + stitchTimeout: { seconds: 1 }, + }, + entityRefs: [ + 'k:ns/entity-f', + 'k:ns/entity-e', + 'k:ns/entity-d', + 'k:ns/entity-c', + 'k:ns/entity-b', + ], + }); + + // Add a small delay to increase chance of collision + await new Promise(resolve => setTimeout(resolve, 10)); + + await markForStitching({ + knex: trx, + strategy: { + mode: 'deferred', + pollingInterval: { seconds: 1 }, + stitchTimeout: { seconds: 1 }, + }, + entityRefs: ['k:ns/entity-a'], + }); + }); + + // Run both transactions concurrently to create potential deadlock + errorResults.push( + Promise.allSettled([transaction1, transaction2]).then(results => + results + .filter(r => r.status === 'rejected') + .map(r => (r as PromiseRejectedResult).reason), + ), + ); + } + + const allResults = await Promise.all(errorResults); + + const deadlockErrors = allResults + .flat() + .filter( + error => + error?.code === '40P01' || + error?.message?.includes('deadlock detected') || + error?.message?.includes('deadlock'), + ); + expect(deadlockErrors.length).toEqual(0); + + // Verify final state - all entities should have been marked for stitching + const finalState = await knex('refresh_state') + .select('entity_ref', 'next_stitch_at', 'next_stitch_ticket') + .whereNotNull('next_stitch_at') + .orderBy('entity_ref'); + + expect(finalState.length).toBeGreaterThan(0); + finalState.forEach(row => { + expect(row.next_stitch_at).not.toBeNull(); + expect(row.next_stitch_ticket).not.toBeNull(); + }); + }, + ); }); diff --git a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts index ecc364a9cc..a3d63778c3 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts @@ -20,6 +20,11 @@ import { v4 as uuid } from 'uuid'; import { StitchingStrategy } from '../../../stitching/types'; import { DbFinalEntitiesRow, DbRefreshStateRow } from '../../tables'; +const UPDATE_CHUNK_SIZE = 100; // Smaller chunks reduce contention +const DEADLOCK_SQLSTATE = '40P01'; +const DEADLOCK_RETRY_ATTEMPTS = 3; +const DEADLOCK_BASE_DELAY_MS = 25; + /** * Marks a number of entities for stitching some time in the near * future. @@ -32,9 +37,9 @@ export async function markForStitching(options: { entityRefs?: Iterable; entityIds?: Iterable; }): Promise { - // Splitting to chunks just to cover pathological cases that upset the db - const entityRefs = split(options.entityRefs); - const entityIds = split(options.entityIds); + // Sort inputs to ensure consistent lock order across concurrent writers + const entityRefs = split(options.entityRefs, true); + const entityIds = split(options.entityIds, true); const knex = options.knex; const mode = options.strategy.mode; @@ -51,13 +56,15 @@ export async function markForStitching(options: { .select('entity_id') .whereIn('entity_ref', chunk), ); - await knex - .table('refresh_state') - .update({ - result_hash: 'force-stitching', - next_update_at: knex.fn.now(), - }) - .whereIn('entity_ref', chunk); + await retryOnDeadlock(async () => { + await knex + .table('refresh_state') + .update({ + result_hash: 'force-stitching', + next_update_at: knex.fn.now(), + }) + .whereIn('entity_ref', chunk); + }); } for (const chunk of entityIds) { @@ -67,44 +74,81 @@ export async function markForStitching(options: { hash: 'force-stitching', }) .whereIn('entity_id', chunk); - await knex - .table('refresh_state') - .update({ - result_hash: 'force-stitching', - next_update_at: knex.fn.now(), - }) - .whereIn('entity_id', chunk); + await retryOnDeadlock(async () => { + await knex + .table('refresh_state') + .update({ + result_hash: 'force-stitching', + next_update_at: knex.fn.now(), + }) + .whereIn('entity_id', chunk); + }); } } else if (mode === 'deferred') { // It's OK that this is shared across refresh state rows; it just needs to // be uniquely generated for every new stitch request. const ticket = uuid(); + // Update by primary key in deterministic order to avoid deadlocks for (const chunk of entityRefs) { - await knex('refresh_state') - .update({ - next_stitch_at: knex.fn.now(), - next_stitch_ticket: ticket, - }) - .whereIn('entity_ref', chunk); + await retryOnDeadlock(async () => { + await knex('refresh_state') + .update({ + next_stitch_at: knex.fn.now(), + next_stitch_ticket: ticket, + }) + .whereIn('entity_ref', chunk); + }); } for (const chunk of entityIds) { - await knex('refresh_state') - .update({ - next_stitch_at: knex.fn.now(), - next_stitch_ticket: ticket, - }) - .whereIn('entity_id', chunk); + // Ensure ids are sorted for deterministic lock order + + await retryOnDeadlock(async () => { + await knex('refresh_state') + .update({ + next_stitch_at: knex.fn.now(), + next_stitch_ticket: ticket, + }) + .whereIn('entity_id', chunk); + }); } } else { throw new Error(`Unknown stitching strategy mode ${mode}`); } } -function split(input: Iterable | undefined): string[][] { +function split(input: Iterable | undefined, sort = false): string[][] { if (!input) { return []; } - return splitToChunks(Array.isArray(input) ? input : [...input], 200); + const array = Array.isArray(input) ? input.slice() : [...input]; + if (sort) { + array.sort(); + } + return splitToChunks(array, UPDATE_CHUNK_SIZE); +} + +async function retryOnDeadlock( + fn: () => Promise, + retries = DEADLOCK_RETRY_ATTEMPTS, + baseMs = DEADLOCK_BASE_DELAY_MS, +): Promise { + let attempt = 0; + for (;;) { + try { + return await fn(); + } catch (e: any) { + if (e?.code === DEADLOCK_SQLSTATE && attempt < retries) { + await sleep(baseMs * Math.pow(2, attempt)); + attempt++; + continue; + } + throw e; + } + } +} + +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); } From afd368e1a224391e114471aabdd3e8a7b1b5ee76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 3 Sep 2025 14:59:39 +0200 Subject: [PATCH 065/233] remove the last remnants of old system structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/brave-jars-speak.md | 5 +++++ .changeset/gold-words-smoke.md | 5 +++++ plugins/app-backend/src/index.ts | 1 - .../package.json | 4 ---- .../report-alpha.api.md | 13 ------------- .../report.api.md | 5 +++++ .../src/alpha.ts | 18 ------------------ .../src/index.ts | 1 + .../catalogModulePuppetDbEntityProvider.ts | 2 +- 9 files changed, 17 insertions(+), 37 deletions(-) create mode 100644 .changeset/brave-jars-speak.md create mode 100644 .changeset/gold-words-smoke.md delete mode 100644 plugins/catalog-backend-module-puppetdb/report-alpha.api.md delete mode 100644 plugins/catalog-backend-module-puppetdb/src/alpha.ts diff --git a/.changeset/brave-jars-speak.md b/.changeset/brave-jars-speak.md new file mode 100644 index 0000000000..2060239326 --- /dev/null +++ b/.changeset/brave-jars-speak.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-puppetdb': patch +--- + +**BREAKING ALPHA**: The module has been moved from the `/alpha` export to the root of the package. diff --git a/.changeset/gold-words-smoke.md b/.changeset/gold-words-smoke.md new file mode 100644 index 0000000000..85141e402c --- /dev/null +++ b/.changeset/gold-words-smoke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app-backend': patch +--- + +Internal update to not expose the old `createRouter`. diff --git a/plugins/app-backend/src/index.ts b/plugins/app-backend/src/index.ts index 9f83f6e30a..3f8a0002a7 100644 --- a/plugins/app-backend/src/index.ts +++ b/plugins/app-backend/src/index.ts @@ -21,4 +21,3 @@ */ export { appPlugin as default } from './service/appPlugin'; -export * from './service/router'; diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index d82414a792..c777cb51bb 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -24,16 +24,12 @@ "license": "Apache-2.0", "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", "./package.json": "./package.json" }, "main": "src/index.ts", "types": "src/index.ts", "typesVersions": { "*": { - "alpha": [ - "src/alpha.ts" - ], "package.json": [ "package.json" ] diff --git a/plugins/catalog-backend-module-puppetdb/report-alpha.api.md b/plugins/catalog-backend-module-puppetdb/report-alpha.api.md deleted file mode 100644 index 211add3895..0000000000 --- a/plugins/catalog-backend-module-puppetdb/report-alpha.api.md +++ /dev/null @@ -1,13 +0,0 @@ -## API Report File for "@backstage/plugin-catalog-backend-module-puppetdb" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; - -// @alpha -const catalogModulePuppetDbEntityProvider: BackendFeature; -export default catalogModulePuppetDbEntityProvider; - -// (No @packageDocumentation comment for this package) -``` diff --git a/plugins/catalog-backend-module-puppetdb/report.api.md b/plugins/catalog-backend-module-puppetdb/report.api.md index f6fa5bfc83..6b846b29dd 100644 --- a/plugins/catalog-backend-module-puppetdb/report.api.md +++ b/plugins/catalog-backend-module-puppetdb/report.api.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { EntityProvider } from '@backstage/plugin-catalog-node'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; @@ -16,6 +17,10 @@ import { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugi // @public export const ANNOTATION_PUPPET_CERTNAME = 'puppet.com/certname'; +// @public +const catalogModulePuppetDbEntityProvider: BackendFeature; +export default catalogModulePuppetDbEntityProvider; + // @public export const DEFAULT_PROVIDER_ID = 'default'; diff --git a/plugins/catalog-backend-module-puppetdb/src/alpha.ts b/plugins/catalog-backend-module-puppetdb/src/alpha.ts deleted file mode 100644 index 01a9b25f7c..0000000000 --- a/plugins/catalog-backend-module-puppetdb/src/alpha.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export * from './module'; -export { default } from './module'; diff --git a/plugins/catalog-backend-module-puppetdb/src/index.ts b/plugins/catalog-backend-module-puppetdb/src/index.ts index b5c5f9d209..9b10bd630f 100644 --- a/plugins/catalog-backend-module-puppetdb/src/index.ts +++ b/plugins/catalog-backend-module-puppetdb/src/index.ts @@ -22,3 +22,4 @@ export * from './providers'; export * from './puppet'; +export { default } from './module'; diff --git a/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.ts b/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.ts index aa524f2409..1ab95c90d8 100644 --- a/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.ts +++ b/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.ts @@ -24,7 +24,7 @@ import { PuppetDbEntityProvider } from '../providers/PuppetDbEntityProvider'; /** * Registers the `PuppetDbEntityProvider` with the catalog processing extension point. * - * @alpha + * @public */ export const catalogModulePuppetDbEntityProvider = createBackendModule({ pluginId: 'catalog', From 02f3d811604877ab6c5deaf704e9054746020d8e Mon Sep 17 00:00:00 2001 From: Praphull <1288395+praphull-purohit@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:28:41 +0200 Subject: [PATCH 066/233] Add block creations field to github branch protection scaffolder action Signed-off-by: Praphull <1288395+praphull-purohit@users.noreply.github.com> --- .../src/actions/gitHelpers.ts | 3 +++ .../src/actions/githubBranchProtection.ts | 3 +++ .../src/actions/inputProperties.ts | 9 +++++++++ 3 files changed, 15 insertions(+) diff --git a/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts b/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts index c720690ee6..d7ef3496dd 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts @@ -44,6 +44,7 @@ type BranchProtectionOptions = { dismissStaleReviews?: boolean; requiredCommitSigning?: boolean; requiredLinearHistory?: boolean; + blockCreations?: boolean; }; export const enableBranchProtectionOnDefaultRepoBranch = async ({ @@ -64,6 +65,7 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ dismissStaleReviews = false, requiredCommitSigning = false, requiredLinearHistory = false, + blockCreations = false, }: BranchProtectionOptions): Promise => { const tryOnce = async () => { try { @@ -96,6 +98,7 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ }, required_conversation_resolution: requiredConversationResolution, required_linear_history: requiredLinearHistory, + block_creations: blockCreations, }); if (requiredCommitSigning) { diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts index f82bfc6950..6ae0f67135 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts @@ -57,6 +57,7 @@ export function createGithubBranchProtectionAction(options: { requireLastPushApproval: inputProps.requireLastPushApproval, requiredCommitSigning: inputProps.requiredCommitSigning, requiredLinearHistory: inputProps.requiredLinearHistory, + blockCreations: inputProps.blockCreations, token: inputProps.token, }, }, @@ -76,6 +77,7 @@ export function createGithubBranchProtectionAction(options: { requireLastPushApproval = false, requiredCommitSigning = false, requiredLinearHistory = false, + blockCreations = false, token: providedToken, } = ctx.input; @@ -129,6 +131,7 @@ export function createGithubBranchProtectionAction(options: { dismissStaleReviews, requiredCommitSigning, requiredLinearHistory, + blockCreations, }); }, }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts b/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts index e1df40f85d..bfa232c45d 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts @@ -348,6 +348,14 @@ const requiredLinearHistory = (z: typeof zod) => }) .optional(); +const blockCreations = (z: typeof zod) => + z + .boolean({ + description: `Prevents creation of new branches during push, unless the push is initiated by a user, team, or app (defined in restrictions) which has the ability to push.`, + }) + .default(false) + .optional(); + const repoVariables = (z: typeof zod) => z .record(z.string(), { @@ -449,4 +457,5 @@ export { protectEnforceAdmins, bypassPullRequestAllowances, branch, + blockCreations, }; From 6393b78fe4115cda68b1f1ff5a6805acc4f26413 Mon Sep 17 00:00:00 2001 From: Praphull <1288395+praphull-purohit@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:38:25 +0200 Subject: [PATCH 067/233] Add changeset Signed-off-by: Praphull <1288395+praphull-purohit@users.noreply.github.com> --- .changeset/ripe-boxes-stand.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ripe-boxes-stand.md diff --git a/.changeset/ripe-boxes-stand.md b/.changeset/ripe-boxes-stand.md new file mode 100644 index 0000000000..e40dedbe90 --- /dev/null +++ b/.changeset/ripe-boxes-stand.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': minor +--- + +Add field block_creations in github branch protection scaffolder actions From f2be6858aef081fc37bc11dec20d110da6d97af3 Mon Sep 17 00:00:00 2001 From: Praphull <1288395+praphull-purohit@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:40:51 +0200 Subject: [PATCH 068/233] Update tests Signed-off-by: Praphull <1288395+praphull-purohit@users.noreply.github.com> --- .../src/actions/githubBranchProtection.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.test.ts index d20a02d460..98d296e176 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.test.ts @@ -105,6 +105,7 @@ describe('github:branch-protection:create', () => { }, required_conversation_resolution: false, required_linear_history: false, + block_creations: false, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, @@ -142,6 +143,7 @@ describe('github:branch-protection:create', () => { }, required_conversation_resolution: false, required_linear_history: false, + block_creations: false, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, @@ -178,6 +180,7 @@ describe('github:branch-protection:create', () => { requireLastPushApproval: true, requiredCommitSigning: true, requiredLinearHistory: true, + blockCreations: true, }, }); @@ -211,6 +214,7 @@ describe('github:branch-protection:create', () => { }, required_conversation_resolution: true, required_linear_history: true, + block_creations: true, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, From 172d57c6b02afa8fec1c5c70c7fca0c734ba99bd Mon Sep 17 00:00:00 2001 From: Praphull <1288395+praphull-purohit@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:51:33 +0200 Subject: [PATCH 069/233] Add API Report Signed-off-by: Praphull <1288395+praphull-purohit@users.noreply.github.com> --- plugins/scaffolder-backend-module-github/report.api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index 85ca128c34..35e4e71c3c 100644 --- a/plugins/scaffolder-backend-module-github/report.api.md +++ b/plugins/scaffolder-backend-module-github/report.api.md @@ -81,6 +81,7 @@ export function createGithubBranchProtectionAction(options: { requireLastPushApproval?: boolean | undefined; requiredCommitSigning?: boolean | undefined; requiredLinearHistory?: boolean | undefined; + blockCreations?: boolean | undefined; token?: string | undefined; }, { From 2f79f0bac84b762651a4aa2d410d564e79efa1ad Mon Sep 17 00:00:00 2001 From: Praphull <1288395+praphull-purohit@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:54:59 +0200 Subject: [PATCH 070/233] Fix tests in examples Signed-off-by: Praphull <1288395+praphull-purohit@users.noreply.github.com> --- .../src/actions/githubBranchProtection.examples.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.examples.test.ts index 31a15f6019..b02d451992 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.examples.test.ts @@ -99,6 +99,7 @@ describe('github:branch-protection:create', () => { }, required_conversation_resolution: false, required_linear_history: false, + block_creations: false, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, @@ -132,6 +133,7 @@ describe('github:branch-protection:create', () => { }, required_conversation_resolution: false, required_linear_history: false, + block_creations: false, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, @@ -165,6 +167,7 @@ describe('github:branch-protection:create', () => { }, required_conversation_resolution: true, required_linear_history: false, + block_creations: false, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, @@ -202,6 +205,7 @@ describe('github:branch-protection:create', () => { }, required_conversation_resolution: true, required_linear_history: true, + block_creations: false, }); expect( mockOctokit.rest.repos.createCommitSignatureProtection, From a599afa296384a1e39cd7db866593265128a27ef Mon Sep 17 00:00:00 2001 From: Praphull <1288395+praphull-purohit@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:57:14 +0200 Subject: [PATCH 071/233] Minor correction in changeset Signed-off-by: Praphull <1288395+praphull-purohit@users.noreply.github.com> --- .changeset/ripe-boxes-stand.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/ripe-boxes-stand.md b/.changeset/ripe-boxes-stand.md index e40dedbe90..163f5ae545 100644 --- a/.changeset/ripe-boxes-stand.md +++ b/.changeset/ripe-boxes-stand.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-backend-module-github': minor --- -Add field block_creations in github branch protection scaffolder actions +Add field blockCreations in github branch protection scaffolder actions From 6474b04936ee6a57de1761d9803e0b8abc8e1755 Mon Sep 17 00:00:00 2001 From: Riley Martine Date: Tue, 26 Aug 2025 17:16:38 -0600 Subject: [PATCH 072/233] Add detail in error messages when yarn plugin can't detect backstage version I was confused for ~15 minutes today when updating to using the yarn plugin. It was failing in docker but not locally, and I didn't know why. It turned out to be because I forgot to copy the backstage.json into the docker image. This was confusing, because the error seemed to indicate I was failing the semver checks. This change propagates error detail down the line, so people will see the actual cause. (i.e. missing file, no version field, semver wrong) Signed-off-by: Riley Martine --- packages/yarn-plugin/package.json | 1 + .../src/util/getCurrentBackstageVersion.test.ts | 16 +++++++--------- .../src/util/getCurrentBackstageVersion.ts | 14 ++++++++++---- yarn.lock | 1 + 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/yarn-plugin/package.json b/packages/yarn-plugin/package.json index c534c50c76..4bd43fce40 100644 --- a/packages/yarn-plugin/package.json +++ b/packages/yarn-plugin/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "@backstage/cli-common": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/release-manifests": "workspace:^", "@yarnpkg/core": "^4.4.1", "@yarnpkg/fslib": "^3.1.2", diff --git a/packages/yarn-plugin/src/util/getCurrentBackstageVersion.test.ts b/packages/yarn-plugin/src/util/getCurrentBackstageVersion.test.ts index 4b1f008fce..a0ce45c18f 100644 --- a/packages/yarn-plugin/src/util/getCurrentBackstageVersion.test.ts +++ b/packages/yarn-plugin/src/util/getCurrentBackstageVersion.test.ts @@ -73,17 +73,15 @@ describe('getCurrentBackstageVersion', () => { }); it.each` - description | content - ${'is missing'} | ${{}} - ${'is invalid'} | ${{ 'backstage.json': '}{' }} - ${'has missing version'} | ${{ 'backstage.json': '{"a":"b"}' }} - ${'has invalid version'} | ${{ 'backstage.json': '{"version":"foobar"}' }} - `('throws if backstage.json $description', ({ content }) => { + description | content | message + ${'is missing'} | ${{}} | ${/valid version string not found.*no such file/i} + ${'is invalid'} | ${{ 'backstage.json': '}{' }} | ${/valid version string not found.*not valid json/i} + ${'has missing version'} | ${{ 'backstage.json': '{"a":"b"}' }} | ${/valid version string not found.*version field is missing/i} + ${'has invalid version'} | ${{ 'backstage.json': '{"version":"foobar"}' }} | ${/valid version string not found.*exists but is not valid semver/i} + `('throws if backstage.json $description', ({ content, message }) => { mockDir.addContent(content); - expect(() => getCurrentBackstageVersion()).toThrow( - /valid version string not found/i, - ); + expect(() => getCurrentBackstageVersion()).toThrow(message); }); it('caches repeated calls', () => { diff --git a/packages/yarn-plugin/src/util/getCurrentBackstageVersion.ts b/packages/yarn-plugin/src/util/getCurrentBackstageVersion.ts index 5280854fb3..be80d29376 100644 --- a/packages/yarn-plugin/src/util/getCurrentBackstageVersion.ts +++ b/packages/yarn-plugin/src/util/getCurrentBackstageVersion.ts @@ -18,6 +18,7 @@ import assert from 'assert'; import { valid as semverValid } from 'semver'; import { ppath, xfs } from '@yarnpkg/fslib'; import { BACKSTAGE_JSON } from '@backstage/cli-common'; +import { ForwardedError } from '@backstage/errors'; import { memoize } from './memoize'; import { getWorkspaceRoot } from './getWorkspaceRoot'; @@ -26,11 +27,16 @@ export const getCurrentBackstageVersion = memoize(() => { let backstageVersion: string | null = null; try { - backstageVersion = semverValid(xfs.readJsonSync(backstageJsonPath).version); + const backstageVersionRaw = xfs.readJsonSync(backstageJsonPath).version; + assert(backstageVersionRaw !== undefined, 'Version field is missing'); + backstageVersion = semverValid(backstageVersionRaw); - assert(backstageVersion !== null); - } catch { - throw new Error('Valid version string not found in backstage.json'); + assert(backstageVersion !== null, 'Version exists but is not valid semver'); + } catch (err) { + throw new ForwardedError( + 'Valid version string not found in backstage.json', + err, + ); } return backstageVersion; diff --git a/yarn.lock b/yarn.lock index 5487f23662..0eb01e6648 100644 --- a/yarn.lock +++ b/yarn.lock @@ -49826,6 +49826,7 @@ __metadata: "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" + "@backstage/errors": "workspace:^" "@backstage/release-manifests": "workspace:^" "@yarnpkg/builder": "npm:^4.2.1" "@yarnpkg/core": "npm:^4.4.1" From 681c726ecc08e443158e0cbbd7786ca1ce7e4c59 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 05:10:36 +0000 Subject: [PATCH 073/233] chore(deps): bump form-data from 2.5.1 to 2.5.5 Bumps [form-data](https://github.com/form-data/form-data) from 2.5.1 to 2.5.5. - [Release notes](https://github.com/form-data/form-data/releases) - [Changelog](https://github.com/form-data/form-data/blob/v2.5.5/CHANGELOG.md) - [Commits](https://github.com/form-data/form-data/compare/v2.5.1...v2.5.5) --- updated-dependencies: - dependency-name: form-data dependency-version: 2.5.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 853749b99e..cf287c28d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26265,7 +26265,7 @@ __metadata: languageName: node linkType: hard -"combined-stream@npm:^1.0.6, combined-stream@npm:^1.0.8": +"combined-stream@npm:^1.0.8": version: 1.0.8 resolution: "combined-stream@npm:1.0.8" dependencies: @@ -31054,13 +31054,16 @@ __metadata: linkType: hard "form-data@npm:^2.3.2, form-data@npm:^2.5.0": - version: 2.5.1 - resolution: "form-data@npm:2.5.1" + version: 2.5.5 + resolution: "form-data@npm:2.5.5" dependencies: asynckit: "npm:^0.4.0" - combined-stream: "npm:^1.0.6" - mime-types: "npm:^2.1.12" - checksum: 10/2e2e5e927979ba3623f9b4c4bcc939275fae3f2dea9dafc8db3ca656a3d75476605de2c80f0e6f1487987398e056f0b4c738972d6e1edd83392d5686d0952eed + combined-stream: "npm:^1.0.8" + es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.2" + mime-types: "npm:^2.1.35" + safe-buffer: "npm:^5.2.1" + checksum: 10/4b6a8d07bb67089da41048e734215f68317a8e29dd5385a972bf5c458a023313c69d3b5d6b8baafbb7f808fa9881e0e2e030ffe61e096b3ddc894c516401271d languageName: node linkType: hard From 2b208f1963ef4e135233a2b2f0d5a6dfd096b6a0 Mon Sep 17 00:00:00 2001 From: Andreas Berger Date: Thu, 4 Sep 2025 10:40:22 +0200 Subject: [PATCH 074/233] Adjustments after review Signed-off-by: Andreas Berger --- .../stitcher/markForStitching.test.ts | 10 ++--- .../operations/stitcher/markForStitching.ts | 41 ++++++++++++------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts index 33712b99f4..ff450e9ccf 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.test.ts @@ -436,14 +436,10 @@ describe('markForStitching', () => { }, ); - const deadlockTestDatabases = TestDatabases.create({ - ids: ['POSTGRES_17', 'POSTGRES_16', 'SQLITE_3'], - disableDocker: false, - }); - it.each(deadlockTestDatabases.eachSupportedId())( + it.each(databases.eachSupportedId())( 'reproduces deadlock scenario when concurrent transactions update overlapping entity sets %p', async databaseId => { - const knex = await deadlockTestDatabases.init(databaseId); + const knex = await databases.init(databaseId); await applyDatabaseMigrations(knex); // Setup test data with multiple entities @@ -559,7 +555,7 @@ describe('markForStitching', () => { error?.message?.includes('deadlock detected') || error?.message?.includes('deadlock'), ); - expect(deadlockErrors.length).toEqual(0); + expect(deadlockErrors).toEqual([]); // Verify final state - all entities should have been marked for stitching const finalState = await knex('refresh_state') diff --git a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts index a3d63778c3..913936202f 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts @@ -21,10 +21,25 @@ import { StitchingStrategy } from '../../../stitching/types'; import { DbFinalEntitiesRow, DbRefreshStateRow } from '../../tables'; const UPDATE_CHUNK_SIZE = 100; // Smaller chunks reduce contention -const DEADLOCK_SQLSTATE = '40P01'; const DEADLOCK_RETRY_ATTEMPTS = 3; const DEADLOCK_BASE_DELAY_MS = 25; +// PostgreSQL deadlock error code +const POSTGRES_DEADLOCK_SQLSTATE = '40P01'; + +/** + * Checks if the given error is a deadlock error for the database engine in use. + */ +function isDeadlockError(knex: Knex | Knex.Transaction, e: unknown): boolean { + if (knex.client.config.client.includes('pg')) { + // PostgreSQL deadlock detection + return (e as any)?.code === POSTGRES_DEADLOCK_SQLSTATE; + } + + // Add more database engine checks here as needed + return false; +} + /** * Marks a number of entities for stitching some time in the near * future. @@ -37,9 +52,8 @@ export async function markForStitching(options: { entityRefs?: Iterable; entityIds?: Iterable; }): Promise { - // Sort inputs to ensure consistent lock order across concurrent writers - const entityRefs = split(options.entityRefs, true); - const entityIds = split(options.entityIds, true); + const entityRefs = sortSplit(options.entityRefs); + const entityIds = sortSplit(options.entityIds); const knex = options.knex; const mode = options.strategy.mode; @@ -64,7 +78,7 @@ export async function markForStitching(options: { next_update_at: knex.fn.now(), }) .whereIn('entity_ref', chunk); - }); + }, knex); } for (const chunk of entityIds) { @@ -82,7 +96,7 @@ export async function markForStitching(options: { next_update_at: knex.fn.now(), }) .whereIn('entity_id', chunk); - }); + }, knex); } } else if (mode === 'deferred') { // It's OK that this is shared across refresh state rows; it just needs to @@ -98,12 +112,10 @@ export async function markForStitching(options: { next_stitch_ticket: ticket, }) .whereIn('entity_ref', chunk); - }); + }, knex); } for (const chunk of entityIds) { - // Ensure ids are sorted for deterministic lock order - await retryOnDeadlock(async () => { await knex('refresh_state') .update({ @@ -111,26 +123,25 @@ export async function markForStitching(options: { next_stitch_ticket: ticket, }) .whereIn('entity_id', chunk); - }); + }, knex); } } else { throw new Error(`Unknown stitching strategy mode ${mode}`); } } -function split(input: Iterable | undefined, sort = false): string[][] { +function sortSplit(input: Iterable | undefined): string[][] { if (!input) { return []; } const array = Array.isArray(input) ? input.slice() : [...input]; - if (sort) { - array.sort(); - } + array.sort(); return splitToChunks(array, UPDATE_CHUNK_SIZE); } async function retryOnDeadlock( fn: () => Promise, + knex: Knex | Knex.Transaction, retries = DEADLOCK_RETRY_ATTEMPTS, baseMs = DEADLOCK_BASE_DELAY_MS, ): Promise { @@ -139,7 +150,7 @@ async function retryOnDeadlock( try { return await fn(); } catch (e: any) { - if (e?.code === DEADLOCK_SQLSTATE && attempt < retries) { + if (isDeadlockError(knex, e) && attempt < retries) { await sleep(baseMs * Math.pow(2, attempt)); attempt++; continue; From 69938650471bae695452a051ee26966971e8c84c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 31 Aug 2025 10:10:36 +0200 Subject: [PATCH 075/233] frontend-app-api: initial error collection implementation Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/report.api.md | 18 + .../extractRouteInfoFromAppNode.test.ts | 16 +- .../src/tree/instantiateAppNodeTree.test.ts | 1169 ++++++++++------- .../src/tree/instantiateAppNodeTree.ts | 204 ++- .../src/tree/resolveAppNodeSpecs.test.ts | 134 +- .../src/tree/resolveAppNodeSpecs.ts | 150 +-- .../src/tree/resolveAppTree.test.ts | 234 ++-- .../src/tree/resolveAppTree.ts | 23 +- .../src/wiring/createErrorCollector.ts | 69 + .../src/wiring/createSpecializedApp.tsx | 43 +- packages/frontend-app-api/src/wiring/index.ts | 1 + 11 files changed, 1314 insertions(+), 747 deletions(-) create mode 100644 packages/frontend-app-api/src/wiring/createErrorCollector.ts diff --git a/packages/frontend-app-api/report.api.md b/packages/frontend-app-api/report.api.md index 039d0bb7b1..1806f47b02 100644 --- a/packages/frontend-app-api/report.api.md +++ b/packages/frontend-app-api/report.api.md @@ -4,16 +4,33 @@ ```ts import { ApiHolder } from '@backstage/core-plugin-api'; +import { AppNode } from '@backstage/frontend-plugin-api'; +import { AppNodeSpec } from '@backstage/frontend-plugin-api'; import { AppTree } from '@backstage/frontend-plugin-api'; import { ConfigApi } from '@backstage/core-plugin-api'; import { ExtensionFactoryMiddleware } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; import { FrontendFeature } from '@backstage/frontend-plugin-api'; +import { FrontendPlugin } from '@backstage/frontend-plugin-api'; import { FrontendPluginInfo } from '@backstage/frontend-plugin-api'; import { JsonObject } from '@backstage/types'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { SubRouteRef } from '@backstage/frontend-plugin-api'; +// @public (undocumented) +export type AppError = { + code: string; + message: string; + context?: { + node?: AppNode; + spec?: AppNodeSpec; + plugin?: FrontendPlugin; + extensionId?: string; + inputName?: string; + dataRefId?: string; + }; +}; + // @public export type CreateAppRouteBinder = < TExternalRoutes extends { @@ -31,6 +48,7 @@ export type CreateAppRouteBinder = < export function createSpecializedApp(options?: CreateSpecializedAppOptions): { apis: ApiHolder; tree: AppTree; + errors?: AppError[]; }; // @public diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts index bb03b5604b..efcc9ef18c 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts @@ -38,6 +38,18 @@ import { Root } from '../extensions/Root'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; import { createRouteAliasResolver } from './RouteAliasResolver'; +import { createErrorCollector } from '../wiring/createErrorCollector'; + +const collector = createErrorCollector(); + +afterEach(() => { + const errors = collector.collectErrors(); + if (errors) { + throw new Error( + `Unexpected errors: ${errors.map(e => e.message).join(', ')}`, + ); + } +}); const ref1 = createRouteRef(); const ref2 = createRouteRef(); @@ -102,10 +114,12 @@ function routeInfoFromExtensions( ], parameters: readAppExtensionsConfig(mockApis.config()), forbidden: new Set(['root']), + collector, }), + collector, ); - instantiateAppNodeTree(tree.root, TestApiRegistry.from()); + instantiateAppNodeTree(tree.root, TestApiRegistry.from(), collector); return extractRouteInfoFromAppNode( tree.root, diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index 57ec7b23f3..9ed0319045 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -43,6 +43,7 @@ import { // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { createSchemaFromZod } from '../../../frontend-plugin-api/src/schema/createSchemaFromZod'; import { TestApiRegistry, withLogCollector } from '@backstage/test-utils'; +import { createErrorCollector } from '../wiring/createErrorCollector'; const testApis = TestApiRegistry.from(); const testDataRef = createExtensionDataRef().with({ id: 'test' }); @@ -51,6 +52,17 @@ const inputMirrorDataRef = createExtensionDataRef().with({ id: 'mirror', }); +const collector = createErrorCollector(); + +afterEach(() => { + const errors = collector.collectErrors(); + if (errors) { + throw new Error( + `Unexpected errors: ${errors.map(e => e.message).join(', ')}`, + ); + } +}); + function makeSpec( extension: Extension, spec?: Partial, @@ -88,6 +100,7 @@ function makeInstanceWithId( node, attachments: new Map(), apis: testApis, + collector, }), }; } @@ -151,75 +164,37 @@ describe('instantiateAppNodeTree', () => { }); it('should instantiate a single node', () => { - const tree = resolveAppTree('root-node', [ - makeSpec(simpleExtension, { id: 'root-node' }), - ]); + const tree = resolveAppTree( + 'root-node', + [makeSpec(simpleExtension, { id: 'root-node' })], + collector, + ); expect(tree.root.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root, testApis); + instantiateAppNodeTree(tree.root, testApis, collector); expect(tree.root.instance).toBeDefined(); expect(tree.root.instance?.getData(testDataRef)).toBe('test'); // Multiple calls should have no effect - instantiateAppNodeTree(tree.root, testApis); + instantiateAppNodeTree(tree.root, testApis, collector); expect(tree.root.instance).toBeDefined(); }); it('should not instantiate disabled nodes', () => { - const tree = resolveAppTree('root-node', [ - makeSpec(simpleExtension, { id: 'root-node', disabled: true }), - ]); + const tree = resolveAppTree( + 'root-node', + [makeSpec(simpleExtension, { id: 'root-node', disabled: true })], + collector, + ); expect(tree.root.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root, testApis); + instantiateAppNodeTree(tree.root, testApis, collector); expect(tree.root.instance).not.toBeDefined(); }); it('should instantiate a node with attachments', () => { - const tree = resolveAppTree('root-node', [ - makeSpec( - createV1Extension({ - namespace: 'root-node', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - test: createV1ExtensionInput({ test: testDataRef }), - }, - output: { - inputMirror: inputMirrorDataRef, - }, - factory({ inputs }) { - return { inputMirror: inputs }; - }, - }), - ), - makeSpec(simpleExtension, { - id: 'child-node', - attachTo: { id: 'root-node', input: 'test' }, - }), - ]); - - const childNode = tree.nodes.get('child-node'); - expect(childNode).toBeDefined(); - - expect(tree.root.instance).not.toBeDefined(); - expect(childNode?.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root, testApis); - expect(tree.root.instance).toBeDefined(); - expect(childNode?.instance).toBeDefined(); - expect(tree.root.instance?.getData(inputMirrorDataRef)).toMatchObject({ - test: [ - { node: { spec: { id: 'child-node' } }, output: { test: 'test' } }, - ], - }); - - // Multiple calls should have no effect - instantiateAppNodeTree(tree.root, testApis); - expect(tree.root.instance).toBeDefined(); - expect(childNode?.instance).toBeDefined(); - }); - - it('should not instantiate disabled attachments', () => { - const tree = resolveAppTree('root-node', [ - { - ...makeSpec( + const tree = resolveAppTree( + 'root-node', + [ + makeSpec( createV1Extension({ namespace: 'root-node', attachTo: { id: 'ignored', input: 'ignored' }, @@ -234,22 +209,72 @@ describe('instantiateAppNodeTree', () => { }, }), ), - }, - { - ...makeSpec(simpleExtension), - id: 'child-node', - // Using an invalid input should not be an error when disabled - attachTo: { id: 'root-node', input: 'invalid' }, - disabled: true, - }, - ]); + makeSpec(simpleExtension, { + id: 'child-node', + attachTo: { id: 'root-node', input: 'test' }, + }), + ], + collector, + ); const childNode = tree.nodes.get('child-node'); expect(childNode).toBeDefined(); expect(tree.root.instance).not.toBeDefined(); expect(childNode?.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root, testApis); + instantiateAppNodeTree(tree.root, testApis, collector); + expect(tree.root.instance).toBeDefined(); + expect(childNode?.instance).toBeDefined(); + expect(tree.root.instance?.getData(inputMirrorDataRef)).toMatchObject({ + test: [ + { node: { spec: { id: 'child-node' } }, output: { test: 'test' } }, + ], + }); + + // Multiple calls should have no effect + instantiateAppNodeTree(tree.root, testApis, collector); + expect(tree.root.instance).toBeDefined(); + expect(childNode?.instance).toBeDefined(); + }); + + it('should not instantiate disabled attachments', () => { + const tree = resolveAppTree( + 'root-node', + [ + { + ...makeSpec( + createV1Extension({ + namespace: 'root-node', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + test: createV1ExtensionInput({ test: testDataRef }), + }, + output: { + inputMirror: inputMirrorDataRef, + }, + factory({ inputs }) { + return { inputMirror: inputs }; + }, + }), + ), + }, + { + ...makeSpec(simpleExtension), + id: 'child-node', + // Using an invalid input should not be an error when disabled + attachTo: { id: 'root-node', input: 'invalid' }, + disabled: true, + }, + ], + collector, + ); + + const childNode = tree.nodes.get('child-node'); + expect(childNode).toBeDefined(); + + expect(tree.root.instance).not.toBeDefined(); + expect(childNode?.instance).not.toBeDefined(); + instantiateAppNodeTree(tree.root, testApis, collector); expect(tree.root.instance).toBeDefined(); expect(childNode?.instance).not.toBeDefined(); expect(tree.root.instance?.getData(inputMirrorDataRef)).toEqual({ @@ -264,13 +289,14 @@ describe('instantiateAppNodeTree', () => { node: makeNode(simpleExtension), attachments, apis: testApis, + collector, }); - expect(Array.from(instance.getDataRefs())).toEqual([ + expect(Array.from(instance?.getDataRefs() ?? [])).toEqual([ testDataRef, otherDataRef.optional(), ]); - expect(instance.getData(testDataRef)).toEqual('test'); + expect(instance?.getData(testDataRef)).toEqual('test'); }); it('should create an extension with different kind of inputs', () => { @@ -346,12 +372,13 @@ describe('instantiateAppNodeTree', () => { }, }), ), + collector, }); - expect(Array.from(instance.getDataRefs())).toEqual([ + expect(Array.from(instance?.getDataRefs() ?? [])).toEqual([ inputMirrorDataRef, ]); - expect(instance.getData(inputMirrorDataRef)).toMatchObject({ + expect(instance?.getData(inputMirrorDataRef)).toMatchObject({ optionalSingletonPresent: { node: { spec: { id: 'app/test' } }, output: { test: 'optionalSingletonPresent' }, @@ -371,118 +398,158 @@ describe('instantiateAppNodeTree', () => { }); it('should refuse to create an extension with invalid config', () => { - expect(() => + const node = makeNode(simpleExtension, { + config: { other: 'not-a-number' }, + }); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode(simpleExtension, { - config: { other: 'not-a-number' }, - }), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Invalid configuration for extension 'app/test'; caused by Error: Expected number, received string at 'other'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'INVALID_CONFIGURATION', + message: + "Invalid configuration for extension 'app/test'; caused by Error: Expected number, received string at 'other'", + context: { node }, + }, + ]); }); it('should forward extension factory errors', () => { - expect(() => + const node = makeNode( + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: {}, + factory() { + const error = new Error('NOPE'); + error.name = 'NopeError'; + throw error; + }, + }), + ); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - createV1Extension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: {}, - factory() { - const error = new Error('NOPE'); - error.name = 'NopeError'; - throw error; - }, - }), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test'; caused by NopeError: NOPE", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'FAILED_TO_INSTANTIATE_EXTENSION', + message: + "Failed to instantiate extension 'app/test'; caused by NopeError: NOPE", + context: { node }, + }, + ]); }); it('should refuse to create an instance with duplicate output', () => { - expect(() => + const node = makeNode( + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + test1: testDataRef, + test2: testDataRef, + }, + factory({}) { + return { test1: 'test', test2: 'test2' }; + }, + }), + ); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - createV1Extension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: { - test1: testDataRef, - test2: testDataRef, - }, - factory({}) { - return { test1: 'test', test2: 'test2' }; - }, - }), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', duplicate extension data 'test' received via output 'test2'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'FAILED_TO_INSTANTIATE_EXTENSION', + message: + "Failed to instantiate extension 'app/test', duplicate extension data 'test' received via output 'test2'", + context: { node }, + }, + ]); }); it('should refuse to create an instance with disconnected output data', () => { - expect(() => + const node = makeNode( + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + test: testDataRef, + }, + factory({}) { + return { nonexistent: 'test' } as any; + }, + }), + ); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - createV1Extension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: { - test: testDataRef, - }, - factory({}) { - return { nonexistent: 'test' } as any; - }, - }), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', unknown output provided via 'nonexistent'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'FAILED_TO_INSTANTIATE_EXTENSION', + message: + "Failed to instantiate extension 'app/test', unknown output provided via 'nonexistent'", + context: { node }, + }, + ]); }); it('should refuse to create an instance with missing required input', () => { - expect(() => + const node = makeNode( + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createV1ExtensionInput( + { + test: testDataRef, + }, + { singleton: true }, + ), + }, + output: {}, + factory: () => ({}), + }), + ); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - createV1Extension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createV1ExtensionInput( - { - test: testDataRef, - }, - { singleton: true }, - ), - }, - output: {}, - factory: () => ({}), - }), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', input 'singleton' is required but was not received", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'FAILED_TO_INSTANTIATE_EXTENSION', + message: + "Failed to instantiate extension 'app/test', input 'singleton' is required but was not received", + context: { node }, + }, + ]); }); it('should warn when creating an instance with undeclared inputs', () => { @@ -521,6 +588,7 @@ describe('instantiateAppNodeTree', () => { factory: () => ({}), }), ), + collector, }), ); @@ -555,6 +623,7 @@ describe('instantiateAppNodeTree', () => { factory: () => ({}), }), ), + collector, }), ); @@ -565,7 +634,24 @@ describe('instantiateAppNodeTree', () => { }); it('should refuse to create an instance with multiple inputs for required singleton', () => { - expect(() => + const node = makeNode( + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createV1ExtensionInput( + { + test: testDataRef, + }, + { singleton: true }, + ), + }, + output: {}, + factory: () => ({}), + }), + ); + expect( createAppNodeInstance({ apis: testApis, attachments: new Map([ @@ -577,31 +663,39 @@ describe('instantiateAppNodeTree', () => { ], ], ]), - node: makeNode( - createV1Extension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createV1ExtensionInput( - { - test: testDataRef, - }, - { singleton: true }, - ), - }, - output: {}, - factory: () => ({}), - }), - ), + node, + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', expected exactly one 'singleton' input but received multiple: 'app/test', 'app/test'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'FAILED_TO_INSTANTIATE_EXTENSION', + message: + "Failed to instantiate extension 'app/test', expected exactly one 'singleton' input but received multiple: 'app/test', 'app/test'", + context: { node }, + }, + ]); }); it('should refuse to create an instance with multiple inputs for optional singleton', () => { - expect(() => + const node = makeNode( + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createV1ExtensionInput( + { + test: testDataRef, + }, + { singleton: true, optional: true }, + ), + }, + output: {}, + factory: () => ({}), + }), + ); + expect( createAppNodeInstance({ apis: testApis, attachments: new Map([ @@ -613,57 +707,56 @@ describe('instantiateAppNodeTree', () => { ], ], ]), - node: makeNode( - createV1Extension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createV1ExtensionInput( - { - test: testDataRef, - }, - { singleton: true, optional: true }, - ), - }, - output: {}, - factory: () => ({}), - }), - ), + node, + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', expected at most one 'singleton' input but received multiple: 'app/test', 'app/test'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'FAILED_TO_INSTANTIATE_EXTENSION', + message: + "Failed to instantiate extension 'app/test', expected at most one 'singleton' input but received multiple: 'app/test', 'app/test'", + context: { node }, + }, + ]); }); it('should refuse to create an instance with multiple inputs that did not provide required data', () => { - expect(() => + const node = makeNode( + createV1Extension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createV1ExtensionInput( + { + other: otherDataRef, + }, + { singleton: true }, + ), + }, + output: {}, + factory: () => ({}), + }), + ); + expect( createAppNodeInstance({ apis: testApis, attachments: new Map([ ['singleton', [makeInstanceWithId(simpleExtension, undefined)]], ]), - node: makeNode( - createV1Extension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createV1ExtensionInput( - { - other: otherDataRef, - }, - { singleton: true }, - ), - }, - output: {}, - factory: () => ({}), - }), - ), + node, + collector, }), - ).toThrowErrorMatchingInlineSnapshot( - `"Failed to instantiate extension 'app/test', extension 'app/test' could not be attached because its output data ('test', 'other') does not match what the input 'singleton' requires ('other')"`, - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'FAILED_TO_INSTANTIATE_EXTENSION', + message: + "Failed to instantiate extension 'app/test', extension 'app/test' could not be attached because its output data ('test', 'other') does not match what the input 'singleton' requires ('other')", + context: { node }, + }, + ]); }); }); }); @@ -727,71 +820,37 @@ describe('instantiateAppNodeTree', () => { } it('should instantiate a single node', () => { - const tree = resolveAppTree('root-node', [ - makeSpec(simpleExtension, { id: 'root-node' }), - ]); + const tree = resolveAppTree( + 'root-node', + [makeSpec(simpleExtension, { id: 'root-node' })], + collector, + ); expect(tree.root.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root, testApis); + instantiateAppNodeTree(tree.root, testApis, collector); expect(tree.root.instance).toBeDefined(); expect(tree.root.instance?.getData(testDataRef)).toBe('test'); // Multiple calls should have no effect - instantiateAppNodeTree(tree.root, testApis); + instantiateAppNodeTree(tree.root, testApis, collector); expect(tree.root.instance).toBeDefined(); }); it('should not instantiate disabled nodes', () => { - const tree = resolveAppTree('root-node', [ - makeSpec(simpleExtension, { id: 'root-node', disabled: true }), - ]); + const tree = resolveAppTree( + 'root-node', + [makeSpec(simpleExtension, { id: 'root-node', disabled: true })], + collector, + ); expect(tree.root.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root, testApis); + instantiateAppNodeTree(tree.root, testApis, collector); expect(tree.root.instance).not.toBeDefined(); }); it('should instantiate a node with attachments', () => { - const tree = resolveAppTree('root-node', [ - makeSpec( - resolveExtensionDefinition( - createExtension({ - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - test: createExtensionInput([testDataRef]), - }, - output: [inputMirrorDataRef], - factory: mirrorInputs, - }), - { namespace: 'root-node' }, - ), - ), - makeSpec(simpleExtension, { - id: 'child-node', - attachTo: { id: 'root-node', input: 'test' }, - }), - ]); - - const childNode = tree.nodes.get('child-node'); - expect(childNode).toBeDefined(); - - expect(tree.root.instance).not.toBeDefined(); - expect(childNode?.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root, testApis); - expect(tree.root.instance).toBeDefined(); - expect(childNode?.instance).toBeDefined(); - expect(tree.root.instance?.getData(inputMirrorDataRef)).toMatchObject({ - test: [{ node: { spec: { id: 'child-node' } }, test: 'test' }], - }); - - // Multiple calls should have no effect - instantiateAppNodeTree(tree.root, testApis); - expect(tree.root.instance).toBeDefined(); - expect(childNode?.instance).toBeDefined(); - }); - - it('should not instantiate disabled attachments', () => { - const tree = resolveAppTree('root-node', [ - { - ...makeSpec( + const tree = resolveAppTree( + 'root-node', + [ + makeSpec( resolveExtensionDefinition( createExtension({ attachTo: { id: 'ignored', input: 'ignored' }, @@ -804,22 +863,68 @@ describe('instantiateAppNodeTree', () => { { namespace: 'root-node' }, ), ), - }, - { - ...makeSpec(simpleExtension), - id: 'child-node', - // Using an invalid input should not be an error when disabled - attachTo: { id: 'root-node', input: 'invalid' }, - disabled: true, - }, - ]); + makeSpec(simpleExtension, { + id: 'child-node', + attachTo: { id: 'root-node', input: 'test' }, + }), + ], + collector, + ); const childNode = tree.nodes.get('child-node'); expect(childNode).toBeDefined(); expect(tree.root.instance).not.toBeDefined(); expect(childNode?.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root, testApis); + instantiateAppNodeTree(tree.root, testApis, collector); + expect(tree.root.instance).toBeDefined(); + expect(childNode?.instance).toBeDefined(); + expect(tree.root.instance?.getData(inputMirrorDataRef)).toMatchObject({ + test: [{ node: { spec: { id: 'child-node' } }, test: 'test' }], + }); + + // Multiple calls should have no effect + instantiateAppNodeTree(tree.root, testApis, collector); + expect(tree.root.instance).toBeDefined(); + expect(childNode?.instance).toBeDefined(); + }); + + it('should not instantiate disabled attachments', () => { + const tree = resolveAppTree( + 'root-node', + [ + { + ...makeSpec( + resolveExtensionDefinition( + createExtension({ + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + test: createExtensionInput([testDataRef]), + }, + output: [inputMirrorDataRef], + factory: mirrorInputs, + }), + { namespace: 'root-node' }, + ), + ), + }, + { + ...makeSpec(simpleExtension), + id: 'child-node', + // Using an invalid input should not be an error when disabled + attachTo: { id: 'root-node', input: 'invalid' }, + disabled: true, + }, + ], + collector, + ); + + const childNode = tree.nodes.get('child-node'); + expect(childNode).toBeDefined(); + + expect(tree.root.instance).not.toBeDefined(); + expect(childNode?.instance).not.toBeDefined(); + instantiateAppNodeTree(tree.root, testApis, collector); expect(tree.root.instance).toBeDefined(); expect(childNode?.instance).not.toBeDefined(); expect(tree.root.instance?.getData(inputMirrorDataRef)).toEqual({ @@ -834,10 +939,13 @@ describe('instantiateAppNodeTree', () => { node: makeNode(simpleExtension), attachments, apis: testApis, + collector, }); - expect(Array.from(instance.getDataRefs())).toEqual([testDataRef]); - expect(instance.getData(testDataRef)).toEqual('test'); + expect(Array.from(instance?.getDataRefs() ?? [])).toEqual([ + testDataRef, + ]); + expect(instance?.getData(testDataRef)).toEqual('test'); }); it('should create an extension with different kind of inputs', () => { @@ -903,12 +1011,13 @@ describe('instantiateAppNodeTree', () => { { namespace: 'app' }, ), ), + collector, }); - expect(Array.from(instance.getDataRefs())).toEqual([ + expect(Array.from(instance?.getDataRefs() ?? [])).toEqual([ inputMirrorDataRef, ]); - expect(instance.getData(inputMirrorDataRef)).toMatchObject({ + expect(instance?.getData(inputMirrorDataRef)).toMatchObject({ optionalSingletonPresent: { node: { spec: { id: 'app/test' } }, test: 'optionalSingletonPresent', @@ -941,24 +1050,35 @@ describe('instantiateAppNodeTree', () => { }); yield* output; }, + collector, }); - expect(Array.from(instance.getDataRefs())).toEqual([testDataRef]); - expect(instance.getData(testDataRef)).toEqual('modified'); + expect(Array.from(instance?.getDataRefs() ?? [])).toEqual([ + testDataRef, + ]); + expect(instance?.getData(testDataRef)).toEqual('modified'); }); it('should refuse to create an extension with invalid config', () => { - expect(() => + const node = makeNode(simpleExtension, { + config: { other: 'not-a-number' }, + }); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode(simpleExtension, { - config: { other: 'not-a-number' }, - }), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Invalid configuration for extension 'app/test'; caused by Error: Expected number, received string at 'other'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'INVALID_CONFIGURATION', + message: + "Invalid configuration for extension 'app/test'; caused by Error: Expected number, received string at 'other'", + context: { node }, + }, + ]); }); it('should throw if extension factories do not provide an iterable object', () => { @@ -966,14 +1086,21 @@ describe('instantiateAppNodeTree', () => { extension: ExtensionDefinition, middleware?: ExtensionFactoryMiddleware, ) { - return createAppNodeInstance({ + const node = makeNode( + resolveExtensionDefinition(extension, { namespace: 'test' }), + ); + createAppNodeInstance({ extensionFactoryMiddleware: middleware, apis: testApis, - node: makeNode( - resolveExtensionDefinition(extension, { namespace: 'test' }), - ), + node, attachments: new Map(), + collector, }); + const errors = collector.collectErrors(); + for (const error of errors ?? []) { + expect(error.context?.node).toBe(node); + } + return errors; } const baseOpts = { @@ -984,7 +1111,7 @@ describe('instantiateAppNodeTree', () => { const badFactory = () => 'not-iterable' as any; const goodFactory = () => [testDataRef('test')]; - expect(() => + expect( createInstance( createExtension({ attachTo: { id: 'ignored', input: 'ignored' }, @@ -992,11 +1119,15 @@ describe('instantiateAppNodeTree', () => { factory: badFactory, }), ), - ).toThrow( - `Failed to instantiate extension 'test', extension factory did not provide an iterable object`, - ); + ).toEqual([ + { + code: 'EXTENSION_FACTORY_INVALID_OUTPUT', + message: 'extension factory did not provide an iterable object', + context: { node: expect.anything() }, + }, + ]); - expect(() => + expect( createInstance( createExtension({ ...baseOpts, @@ -1005,12 +1136,18 @@ describe('instantiateAppNodeTree', () => { factory: badFactory, }), ), - ).toThrow( - `Failed to instantiate extension 'test', extension factory override did not provide an iterable object`, - ); + ).toEqual([ + { + // TODO: This should probably be EXTENSION_FACTORY_INVALID_OUTPUT + code: 'FAILED_TO_INSTANTIATE_EXTENSION', + message: + "Failed to instantiate extension 'test', extension factory override did not provide an iterable object", + context: { node: expect.anything() }, + }, + ]); // Bad middleware - expect(() => + expect( createInstance( createExtension({ ...baseOpts, @@ -1018,11 +1155,17 @@ describe('instantiateAppNodeTree', () => { }), () => 'not-iterable' as any, ), - ).toThrow( - `Failed to instantiate extension 'test', extension factory middleware did not provide an iterable object`, - ); + ).toEqual([ + { + // TODO: This should probably be EXTENSION_FACTORY_INVALID_OUTPUT + code: 'FAILED_TO_INSTANTIATE_EXTENSION', + message: + "Failed to instantiate extension 'test', extension factory middleware did not provide an iterable object", + context: { node: expect.anything() }, + }, + ]); - expect(() => + expect( createInstance( createExtensionBlueprint({ kind: 'test', @@ -1030,12 +1173,16 @@ describe('instantiateAppNodeTree', () => { factory: badFactory, }).make({ params: {} }), ), - ).toThrow( - `Failed to instantiate extension 'test:test', extension factory did not provide an iterable object`, - ); + ).toEqual([ + { + code: 'EXTENSION_FACTORY_INVALID_OUTPUT', + message: 'extension factory did not provide an iterable object', + context: { node: expect.anything() }, + }, + ]); // Using makeWithOverrides - expect(() => + expect( createInstance( createExtensionBlueprint({ kind: 'test', @@ -1045,12 +1192,16 @@ describe('instantiateAppNodeTree', () => { factory: badFactory, }), ), - ).toThrow( - `Failed to instantiate extension 'test:test', extension factory did not provide an iterable object`, - ); + ).toEqual([ + { + code: 'EXTENSION_FACTORY_INVALID_OUTPUT', + message: 'extension factory did not provide an iterable object', + context: { node: expect.anything() }, + }, + ]); // Using makeWithOverrides and factory middleware - expect(() => + expect( createInstance( createExtensionBlueprint({ kind: 'test', @@ -1061,12 +1212,18 @@ describe('instantiateAppNodeTree', () => { }), orig => orig(), ), - ).toThrow( - `Failed to instantiate extension 'test:test', extension factory did not provide an iterable object`, - ); + ).toEqual([ + { + // TODO: This should probably be EXTENSION_FACTORY_INVALID_OUTPUT + code: 'FAILED_TO_INSTANTIATE_EXTENSION', + message: + "Failed to instantiate extension 'test:test', extension factory did not provide an iterable object", + context: { node: expect.anything() }, + }, + ]); // Using makeWithOverrides and factory middleware - expect(() => + expect( createInstance( createExtensionBlueprint({ kind: 'test', @@ -1077,135 +1234,181 @@ describe('instantiateAppNodeTree', () => { }), orig => orig(), ), - ).toThrow( - `Failed to instantiate extension 'test:test', original blueprint factory did not provide an iterable object`, - ); + ).toEqual([ + { + // TODO: This should probably be EXTENSION_FACTORY_INVALID_OUTPUT + code: 'FAILED_TO_INSTANTIATE_EXTENSION', + message: + "Failed to instantiate extension 'test:test', original blueprint factory did not provide an iterable object", + context: { node: expect.anything() }, + }, + ]); }); it('should forward extension factory errors', () => { - expect(() => + const node = makeNode( + resolveExtensionDefinition( + createExtension({ + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [testDataRef], + factory() { + const error = new Error('NOPE'); + error.name = 'NopeError'; + throw error; + }, + }), + { namespace: 'app' }, + ), + ); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - resolveExtensionDefinition( - createExtension({ - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: [testDataRef], - factory() { - const error = new Error('NOPE'); - error.name = 'NopeError'; - throw error; - }, - }), - { namespace: 'app' }, - ), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test'; caused by NopeError: NOPE", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'FAILED_TO_INSTANTIATE_EXTENSION', + message: + "Failed to instantiate extension 'app/test'; caused by NopeError: NOPE", + context: { node }, + }, + ]); }); it('should refuse to create an instance with duplicate output', () => { - expect(() => + const node = makeNode( + resolveExtensionDefinition( + createExtension({ + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [testDataRef, testDataRef], + factory({}) { + return [testDataRef('test'), testDataRef('test2')]; + }, + }), + { namespace: 'app' }, + ), + ); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - resolveExtensionDefinition( - createExtension({ - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: [testDataRef, testDataRef], - factory({}) { - return [testDataRef('test'), testDataRef('test2')]; - }, - }), - { namespace: 'app' }, - ), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', duplicate extension data output 'test'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_FACTORY_DUPLICATE_OUTPUT', + message: "extension factory output duplicate data 'test'", + context: { dataRefId: 'test', node }, + }, + ]); }); it('should refuse to create an instance without required', () => { - expect(() => + const node = makeNode( + resolveExtensionDefinition( + createExtension({ + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [testDataRef], + factory({}) { + return [] as any; + }, + }), + { namespace: 'app' }, + ), + ); + + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - resolveExtensionDefinition( - createExtension({ - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: [testDataRef], - factory({}) { - return [] as any; - }, - }), - { namespace: 'app' }, - ), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', missing required extension data output 'test'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_FACTORY_MISSING_REQUIRED_OUTPUT', + message: "missing required extension data output 'test'", + context: { + dataRefId: 'test', + node, + }, + }, + ]); }); it('should refuse to create an instance with unknown output data', () => { - expect(() => + const node = makeNode( + resolveExtensionDefinition( + // @ts-expect-error + createExtension({ + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [], // Output not declared + factory({}) { + return [testDataRef('test')] as any; + }, + }), + { namespace: 'app' }, + ), + ); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - resolveExtensionDefinition( - // @ts-expect-error - createExtension({ - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: [], // Output not declared - factory({}) { - return [testDataRef('test')] as any; - }, - }), - { namespace: 'app' }, - ), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', unexpected output 'test'", - ); + ).toBeDefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_FACTORY_UNEXPECTED_OUTPUT', + message: "unexpected output 'test'", + context: { dataRefId: 'test', node }, + }, + ]); }); it('should refuse to create an instance with missing required input', () => { - expect(() => + const node = makeNode( + resolveExtensionDefinition( + createExtension({ + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput([testDataRef], { + singleton: true, + }), + }, + output: [], + factory: () => [], + }), + { namespace: 'app' }, + ), + ); + expect( createAppNodeInstance({ apis: testApis, - node: makeNode( - resolveExtensionDefinition( - createExtension({ - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput([testDataRef], { - singleton: true, - }), - }, - output: [], - factory: () => [], - }), - { namespace: 'app' }, - ), - ), + node, attachments: new Map(), + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', input 'singleton' is required but was not received", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_MISSING_REQUIRED_INPUT', + message: "input 'singleton' is required but was not received", + context: { inputName: 'singleton', node }, + }, + ]); }); it('should warn when creating an instance with undeclared inputs', () => { @@ -1244,6 +1447,7 @@ describe('instantiateAppNodeTree', () => { { namespace: 'app' }, ), ), + collector, }), ); @@ -1280,6 +1484,7 @@ describe('instantiateAppNodeTree', () => { { namespace: 'app' }, ), ), + collector, }), ); @@ -1290,7 +1495,23 @@ describe('instantiateAppNodeTree', () => { }); it('should refuse to create an instance with multiple inputs for required singleton', () => { - expect(() => + const node = makeNode( + resolveExtensionDefinition( + createExtension({ + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput([testDataRef], { + singleton: true, + }), + }, + output: [], + factory: () => [], + }), + { namespace: 'app' }, + ), + ); + expect( createAppNodeInstance({ apis: testApis, attachments: new Map([ @@ -1302,30 +1523,39 @@ describe('instantiateAppNodeTree', () => { ], ], ]), - node: makeNode( - resolveExtensionDefinition( - createExtension({ - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput([testDataRef], { - singleton: true, - }), - }, - output: [], - factory: () => [], - }), - { namespace: 'app' }, - ), - ), + node, + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', expected exactly one 'singleton' input but received multiple: 'app/test', 'app/test'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_TOO_MANY_ATTACHMENTS', + message: + "expected exactly one 'singleton' input but received multiple: 'app/test', 'app/test'", + context: { inputName: 'singleton', node }, + }, + ]); }); it('should refuse to create an instance with multiple inputs for optional singleton', () => { - expect(() => + const node = makeNode( + resolveExtensionDefinition( + createExtension({ + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput([testDataRef], { + singleton: true, + optional: true, + }), + }, + output: [], + factory: () => [], + }), + { namespace: 'app' }, + ), + ); + expect( createAppNodeInstance({ apis: testApis, attachments: new Map([ @@ -1337,56 +1567,55 @@ describe('instantiateAppNodeTree', () => { ], ], ]), - node: makeNode( - resolveExtensionDefinition( - createExtension({ - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput([testDataRef], { - singleton: true, - optional: true, - }), - }, - output: [], - factory: () => [], - }), - { namespace: 'app' }, - ), - ), + node, + collector, }), - ).toThrow( - "Failed to instantiate extension 'app/test', expected at most one 'singleton' input but received multiple: 'app/test', 'app/test'", - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_TOO_MANY_ATTACHMENTS', + message: + "expected at most one 'singleton' input but received multiple: 'app/test', 'app/test'", + context: { inputName: 'singleton', node }, + }, + ]); }); it('should refuse to create an instance with multiple inputs that did not provide required data', () => { - expect(() => + const node = makeNode( + resolveExtensionDefinition( + createExtension({ + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput([otherDataRef], { + singleton: true, + }), + }, + output: [], + factory: () => [], + }), + { namespace: 'app' }, + ), + ); + expect( createAppNodeInstance({ apis: testApis, attachments: new Map([ ['singleton', [makeInstanceWithId(simpleExtension, undefined)]], ]), - node: makeNode( - resolveExtensionDefinition( - createExtension({ - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput([otherDataRef], { - singleton: true, - }), - }, - output: [], - factory: () => [], - }), - { namespace: 'app' }, - ), - ), + node, + collector, }), - ).toThrowErrorMatchingInlineSnapshot( - `"Failed to instantiate extension 'app/test', extension 'app/test' could not be attached because its output data ('test') does not match what the input 'singleton' requires ('other')"`, - ); + ).toBeUndefined(); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_MISSING_INPUT_DATA', + message: + "extension 'app/test' could not be attached because its output data ('test') does not match what the input 'singleton' requires ('other')", + context: { node }, + }, + ]); }); }); }); diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index 2c917ed5d4..a33c5943a1 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -27,6 +27,7 @@ import { AppNode, AppNodeInstance } from '@backstage/frontend-plugin-api'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtension } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; import { createExtensionDataContainer } from '@internal/frontend'; +import { ErrorCollector } from '../wiring/createErrorCollector'; type Mutable = { -readonly [P in keyof T]: T[P]; @@ -63,12 +64,18 @@ function resolveInputDataContainer( extensionData: Array, attachment: AppNode, inputName: string, -): { node: AppNode } & ExtensionDataContainer { + collector: ErrorCollector, +): ({ node: AppNode } & ExtensionDataContainer) | undefined { const dataMap = new Map(); + let failed = false; for (const ref of extensionData) { if (dataMap.has(ref.id)) { - throw new Error(`Unexpected duplicate input data '${ref.id}'`); + collector.report({ + code: 'EXTENSION_DUPLICATE_INPUT', + message: `Unexpected duplicate input data '${ref.id}'`, + }); + continue; } const value = attachment.instance?.getData(ref); if (value === undefined && !ref.config.optional) { @@ -81,14 +88,20 @@ function resolveInputDataContainer( .map(r => `'${r.id}'`) .join(', '); - throw new Error( - `extension '${attachment.spec.id}' could not be attached because its output data (${provided}) does not match what the input '${inputName}' requires (${expected})`, - ); + collector.report({ + code: 'EXTENSION_MISSING_INPUT_DATA', + message: `extension '${attachment.spec.id}' could not be attached because its output data (${provided}) does not match what the input '${inputName}' requires (${expected})`, + }); + failed = true; } dataMap.set(ref.id, value); } + if (failed) { + return undefined; + } + return { node: attachment, get(ref) { @@ -198,42 +211,79 @@ function resolveV2Inputs( >; }, attachments: ReadonlyMap, -): ResolvedExtensionInputs<{ - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; -}> { - return mapValues(inputMap, (input, inputName) => { + collector: ErrorCollector, +): + | undefined + | ResolvedExtensionInputs<{ + [inputName in string]: ExtensionInput< + ExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }> { + let failed = false; + const resolvedInputs = mapValues(inputMap, (input, inputName) => { const attachedNodes = attachments.get(inputName) ?? []; if (input.config.singleton) { if (attachedNodes.length > 1) { - const attachedNodeIds = attachedNodes.map(e => e.spec.id); - throw Error( - `expected ${ + const attachedNodeIds = attachedNodes.map(e => e.spec.id).join("', '"); + collector.report({ + code: 'EXTENSION_TOO_MANY_ATTACHMENTS', + message: `expected ${ input.config.optional ? 'at most' : 'exactly' - } one '${inputName}' input but received multiple: '${attachedNodeIds.join( - "', '", - )}'`, - ); + } one '${inputName}' input but received multiple: '${attachedNodeIds}'`, + context: { inputName }, + }); + failed = true; + return undefined; } else if (attachedNodes.length === 0) { - if (input.config.optional) { - return undefined; + if (!input.config.optional) { + collector.report({ + code: 'EXTENSION_MISSING_REQUIRED_INPUT', + message: `input '${inputName}' is required but was not received`, + context: { inputName }, + }); + failed = true; } - throw Error(`input '${inputName}' is required but was not received`); + return undefined; } - return resolveInputDataContainer( + const data = resolveInputDataContainer( input.extensionData, attachedNodes[0], inputName, + collector, ); + if (data === undefined) { + failed = true; + return undefined; + } + return data; } - return attachedNodes.map(attachment => - resolveInputDataContainer(input.extensionData, attachment, inputName), - ); - }) as ResolvedExtensionInputs<{ + if (failed) { + return undefined; + } + + return attachedNodes.map(attachment => { + const data = resolveInputDataContainer( + input.extensionData, + attachment, + inputName, + collector, + ); + if (data === undefined) { + failed = true; + return undefined; + } + return data; + }); + }); + + if (failed) { + return undefined; + } + + return resolvedInputs as ResolvedExtensionInputs<{ [inputName in string]: ExtensionInput< ExtensionDataRef, { optional: boolean; singleton: boolean } @@ -247,8 +297,10 @@ export function createAppNodeInstance(options: { node: AppNode; apis: ApiHolder; attachments: ReadonlyMap; -}): AppNodeInstance { + collector: ErrorCollector; +}): AppNodeInstance | undefined { const { node, apis, attachments } = options; + const collector = options.collector.child({ node }); const { id, extension, config } = node.spec; const extensionData = new Map(); const extensionDataRefs = new Set>(); @@ -259,9 +311,11 @@ export function createAppNodeInstance(options: { [x: string]: any; }; } catch (e) { - throw new Error( - `Invalid configuration for extension '${id}'; caused by ${e}`, - ); + collector.report({ + code: 'INVALID_CONFIGURATION', + message: `Invalid configuration for extension '${id}'; caused by ${e}`, + }); + return undefined; } try { @@ -293,11 +347,19 @@ export function createAppNodeInstance(options: { extensionDataRefs.add(ref); } } else if (internalExtension.version === 'v2') { + const inputs = resolveV2Inputs( + internalExtension.inputs, + attachments, + collector, + ); + if (inputs === undefined) { + return undefined; + } const context = { node, apis, config: parsedConfig, - inputs: resolveV2Inputs(internalExtension.inputs, attachments), + inputs, }; const outputDataValues = options.extensionFactoryMiddleware ? createExtensionDataContainer( @@ -320,15 +382,32 @@ export function createAppNodeInstance(options: { typeof outputDataValues !== 'object' || !outputDataValues?.[Symbol.iterator] ) { - throw new Error('extension factory did not provide an iterable object'); + collector.report({ + code: 'EXTENSION_FACTORY_INVALID_OUTPUT', + message: 'extension factory did not provide an iterable object', + }); + return undefined; } + let failed = false; + const outputDataMap = new Map(); for (const value of outputDataValues) { if (outputDataMap.has(value.id)) { - throw new Error(`duplicate extension data output '${value.id}'`); + collector.report({ + code: 'EXTENSION_FACTORY_DUPLICATE_OUTPUT', + message: `extension factory output duplicate data '${value.id}'`, + context: { + dataRefId: value.id, + }, + }); + failed = true; + } else { + outputDataMap.set(value.id, value.value); } - outputDataMap.set(value.id, value.value); + } + if (failed) { + return undefined; } for (const ref of internalExtension.output) { @@ -336,34 +415,53 @@ export function createAppNodeInstance(options: { outputDataMap.delete(ref.id); if (value === undefined) { if (!ref.config.optional) { - throw new Error( - `missing required extension data output '${ref.id}'`, - ); + collector.report({ + code: 'EXTENSION_FACTORY_MISSING_REQUIRED_OUTPUT', + message: `missing required extension data output '${ref.id}'`, + context: { + dataRefId: ref.id, + }, + }); + failed = true; } } else { extensionData.set(ref.id, value); extensionDataRefs.add(ref); } } + if (failed) { + return undefined; + } if (outputDataMap.size > 0) { - throw new Error( - `unexpected output '${Array.from(outputDataMap.keys()).join( - "', '", - )}'`, - ); + for (const dataRefId of outputDataMap.keys()) { + // TODO: Make this a warning + collector.report({ + code: 'EXTENSION_FACTORY_UNEXPECTED_OUTPUT', + message: `unexpected output '${dataRefId}'`, + context: { + dataRefId: dataRefId, + }, + }); + } } } else { - throw new Error( - `unexpected extension version '${(internalExtension as any).version}'`, - ); + collector.report({ + code: 'UNEXPECTED_EXTENSION_VERSION', + message: `unexpected extension version '${ + (internalExtension as any).version + }'`, + }); + return undefined; } } catch (e) { - throw new Error( - `Failed to instantiate extension '${id}'${ - e.name === 'Error' ? `, ${e.message}` : `; caused by ${e.stack}` + collector.report({ + code: 'FAILED_TO_INSTANTIATE_EXTENSION', + message: `Failed to instantiate extension '${id}'${ + e.name === 'Error' ? `, ${e.message}` : `; caused by ${e}` }`, - ); + }); + return undefined; } return { @@ -383,8 +481,9 @@ export function createAppNodeInstance(options: { export function instantiateAppNodeTree( rootNode: AppNode, apis: ApiHolder, + errors: ErrorCollector, extensionFactoryMiddleware?: ExtensionFactoryMiddleware, -): void { +): boolean { function createInstance(node: AppNode): AppNodeInstance | undefined { if (node.instance) { return node.instance; @@ -413,10 +512,11 @@ export function instantiateAppNodeTree( node, apis, attachments: instantiatedAttachments, + collector: errors, }); return node.instance; } - createInstance(rootNode); + return createInstance(rootNode) !== undefined; } diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts index 83abcf3dc4..23217f697a 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts @@ -21,6 +21,18 @@ import { ExtensionDefinition, } from '@backstage/frontend-plugin-api'; import { resolveAppNodeSpecs } from './resolveAppNodeSpecs'; +import { createErrorCollector } from '../wiring/createErrorCollector'; + +const collector = createErrorCollector(); + +afterEach(() => { + const errors = collector.collectErrors(); + if (errors) { + throw new Error( + `Unexpected errors: ${errors.map(e => e.message).join(', ')}`, + ); + } +}); function makeExt( id: string, @@ -61,6 +73,7 @@ describe('resolveAppNodeSpecs', () => { features: [], builtinExtensions: [a], parameters: [], + collector, }), ).toEqual([ { @@ -83,6 +96,7 @@ describe('resolveAppNodeSpecs', () => { features: [], builtinExtensions: [a, b], parameters: [], + collector, }), ).toEqual([ { @@ -122,6 +136,7 @@ describe('resolveAppNodeSpecs', () => { attachTo: { id: 'derp', input: 'default' }, }, ], + collector, }), ).toEqual([ { @@ -169,6 +184,7 @@ describe('resolveAppNodeSpecs', () => { config: { foo: { qux: 3 } }, }, ], + collector, }), ).toEqual([ { @@ -209,6 +225,7 @@ describe('resolveAppNodeSpecs', () => { disabled: false, }, ], + collector, }), ).toEqual([ { @@ -249,6 +266,7 @@ describe('resolveAppNodeSpecs', () => { { id: 'd', disabled: false }, { id: 'c', disabled: false }, ], + collector, }), ).toEqual([ { @@ -341,6 +359,7 @@ describe('resolveAppNodeSpecs', () => { ], builtinExtensions: [], parameters: [], + collector, }), ).toEqual([ { @@ -395,6 +414,7 @@ describe('resolveAppNodeSpecs', () => { id, disabled: false, })), + collector, }); expect(result.map(r => r.extension.id)).toEqual([ @@ -405,53 +425,83 @@ describe('resolveAppNodeSpecs', () => { }); it('throws an error when a forbidden extension is overridden by a plugin', () => { - expect(() => - resolveAppNodeSpecs({ - features: [ - createFrontendPlugin({ - pluginId: 'test', - extensions: [makeExtDef('forbidden')], - }), - ], - builtinExtensions: [], - parameters: [], - forbidden: new Set(['test/forbidden']), - }), - ).toThrow( - "It is forbidden to override the following extension(s): 'test/forbidden', which is done by the following plugin(s): 'test'", - ); + const plugin = createFrontendPlugin({ + pluginId: 'test', + extensions: [makeExtDef('forbidden')], + }); + const result = resolveAppNodeSpecs({ + features: [plugin], + builtinExtensions: [], + parameters: [], + forbidden: new Set(['test/forbidden']), + collector, + }); + expect(result).toEqual([]); + + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_FORBIDDEN', + message: + "It is forbidden to override the 'test/forbidden' extension, attempted by the 'test' plugin", + context: { + plugin, + extensionId: 'test/forbidden', + }, + }, + ]); }); it('throws an error when a forbidden extension is overridden by module', () => { - expect(() => - resolveAppNodeSpecs({ - features: [ - createFrontendPlugin({ - pluginId: 'forbidden', - extensions: [], - }), - createFrontendModule({ - pluginId: 'forbidden', - extensions: [makeExtDef()], - }), - ], - builtinExtensions: [], - parameters: [], - forbidden: new Set(['forbidden']), - }), - ).toThrow( - "It is forbidden to override the following extension(s): 'forbidden', which is done by a module for the following plugin(s): 'forbidden'", - ); + const plugin = createFrontendPlugin({ + pluginId: 'forbidden', + extensions: [], + }); + const result = resolveAppNodeSpecs({ + features: [ + plugin, + createFrontendModule({ + pluginId: 'forbidden', + extensions: [makeExtDef()], + }), + ], + builtinExtensions: [], + parameters: [], + forbidden: new Set(['forbidden']), + collector, + }); + expect(result).toEqual([]); + + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_FORBIDDEN', + message: + "It is forbidden to override the 'forbidden' extension, attempted by the 'forbidden' plugin", + context: { + plugin, + extensionId: 'forbidden', + }, + }, + ]); }); it('throws an error when a forbidden extension is parametrized', () => { - expect(() => - resolveAppNodeSpecs({ - features: [], - builtinExtensions: [], - parameters: [{ id: 'forbidden', disabled: false }], - forbidden: new Set(['forbidden']), - }), - ).toThrow("Configuration of the 'forbidden' extension is forbidden"); + const result = resolveAppNodeSpecs({ + features: [], + builtinExtensions: [], + parameters: [{ id: 'forbidden', disabled: false }], + forbidden: new Set(['forbidden']), + collector, + }); + expect(result).toEqual([]); + + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_CONFIG_FORBIDDEN', + message: "Configuration of the 'forbidden' extension is forbidden", + context: { + extensionId: 'forbidden', + }, + }, + ]); }); }); diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts index f9ce7387c2..23a1847b43 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts @@ -18,6 +18,7 @@ import { createFrontendPlugin, Extension, FrontendFeature, + FrontendPlugin, } from '@backstage/frontend-plugin-api'; import { ExtensionParameters } from './readAppExtensionsConfig'; import { AppNodeSpec } from '@backstage/frontend-plugin-api'; @@ -29,6 +30,7 @@ import { } from '../../../frontend-plugin-api/src/wiring/createFrontendModule'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtension } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; +import { ErrorCollector } from '../wiring/createErrorCollector'; /** @internal */ export function resolveAppNodeSpecs(options: { @@ -36,61 +38,58 @@ export function resolveAppNodeSpecs(options: { builtinExtensions?: Extension[]; parameters?: Array; forbidden?: Set; - allowUnknownExtensionConfig?: boolean; + collector: ErrorCollector; }): AppNodeSpec[] { const { builtinExtensions = [], parameters = [], forbidden = new Set(), features = [], - allowUnknownExtensionConfig = false, + collector, } = options; const plugins = features.filter(OpaqueFrontendPlugin.isType); const modules = features.filter(isInternalFrontendModule); + const filterForbidden = ( + extension: Extension & { plugin: FrontendPlugin }, + ) => { + if (forbidden.has(extension.id)) { + collector.report({ + code: 'EXTENSION_FORBIDDEN', + message: `It is forbidden to override the '${extension.id}' extension, attempted by the '${extension.plugin.id}' plugin`, + context: { + plugin: extension.plugin, + extensionId: extension.id, + }, + }); + return false; + } + return true; + }; + const pluginExtensions = plugins.flatMap(plugin => { - return OpaqueFrontendPlugin.toInternal(plugin).extensions.map( - extension => ({ + return OpaqueFrontendPlugin.toInternal(plugin) + .extensions.map(extension => ({ ...extension, plugin, - }), - ); + })) + .filter(filterForbidden); }); const moduleExtensions = modules.flatMap(mod => - toInternalFrontendModule(mod).extensions.flatMap(extension => { - // Modules for plugins that are not installed are ignored - const plugin = plugins.find(p => p.id === mod.pluginId); - if (!plugin) { - return []; - } + toInternalFrontendModule(mod) + .extensions.flatMap(extension => { + // Modules for plugins that are not installed are ignored + const plugin = plugins.find(p => p.id === mod.pluginId); + if (!plugin) { + return []; + } - return [{ ...extension, plugin }]; - }), + return [{ ...extension, plugin }]; + }) + .filter(filterForbidden), ); - // Prevent core override - if (pluginExtensions.some(({ id }) => forbidden.has(id))) { - const pluginsStr = pluginExtensions - .filter(({ id }) => forbidden.has(id)) - .map(({ plugin }) => `'${plugin.id}'`) - .join(', '); - const forbiddenStr = [...forbidden].map(id => `'${id}'`).join(', '); - throw new Error( - `It is forbidden to override the following extension(s): ${forbiddenStr}, which is done by the following plugin(s): ${pluginsStr}`, - ); - } - if (moduleExtensions.some(({ id }) => forbidden.has(id))) { - const pluginsStr = moduleExtensions - .filter(({ id }) => forbidden.has(id)) - .map(({ plugin }) => `'${plugin.id}'`) - .join(', '); - const forbiddenStr = [...forbidden].map(id => `'${id}'`).join(', '); - throw new Error( - `It is forbidden to override the following extension(s): ${forbiddenStr}, which is done by a module for the following plugin(s): ${pluginsStr}`, - ); - } - const appPlugin = plugins.find(plugin => plugin.id === 'app') ?? createFrontendPlugin({ @@ -154,52 +153,41 @@ export function resolveAppNodeSpecs(options: { } } - const duplicatedExtensionIds = new Set(); - const duplicatedExtensionData = configuredExtensions.reduce< - Record> - >((data, { extension, params }) => { - const extensionId = extension.id; - const extensionData = data?.[extensionId]; - if (extensionData) duplicatedExtensionIds.add(extensionId); - const pluginId = params.source?.id ?? 'internal'; - const pluginCount = extensionData?.[pluginId] ?? 0; - return { - ...data, - [extensionId]: { ...extensionData, [pluginId]: pluginCount + 1 }, - }; - }, {}); + const seendExtensionIds = new Set(); + const deduplicatedExtensions = configuredExtensions.filter( + ({ extension, params }) => { + if (seendExtensionIds.has(extension.id)) { + collector.report({ + code: 'EXTENSION_DUPLICATED', + message: `The extension '${extension.id}' is duplicated`, + context: { + plugin: params.plugin, + extensionId: extension.id, + }, + }); + return false; + } + seendExtensionIds.add(extension.id); + return true; + }, + ); - if (duplicatedExtensionIds.size > 0) { - throw new Error( - `The following extensions are duplicated: ${Array.from( - duplicatedExtensionIds, - ) - .map( - extensionId => - `The extension '${extensionId}' was provided ${Object.keys( - duplicatedExtensionData[extensionId], - ) - .map( - pluginId => - `${duplicatedExtensionData[extensionId][pluginId]} time(s) by the plugin '${pluginId}'`, - ) - .join(' and ')}`, - ) - .join(', ')}`, - ); - } - - const order = new Map(); + const order = new Map(); for (const overrideParam of parameters) { const extensionId = overrideParam.id; if (forbidden.has(extensionId)) { - throw new Error( - `Configuration of the '${extensionId}' extension is forbidden`, - ); + collector.report({ + code: 'EXTENSION_CONFIG_FORBIDDEN', + message: `Configuration of the '${extensionId}' extension is forbidden`, + context: { + extensionId, + }, + }); + continue; } - const existing = configuredExtensions.find( + const existing = deduplicatedExtensions.find( e => e.extension.id === extensionId, ); if (existing) { @@ -216,14 +204,20 @@ export function resolveAppNodeSpecs(options: { existing.params.disabled = Boolean(overrideParam.disabled); } order.set(extensionId, existing); - } else if (!allowUnknownExtensionConfig) { - throw new Error(`Extension ${extensionId} does not exist`); + } else { + collector.report({ + code: 'EXTENSION_CONFIG_UNKNOWN_EXTENSION', + message: `Extension ${extensionId} does not exist`, + context: { + extensionId, + }, + }); } } const orderedExtensions = [ ...order.values(), - ...configuredExtensions.filter(e => !order.has(e.extension.id)), + ...deduplicatedExtensions.filter(e => !order.has(e.extension.id)), ]; return orderedExtensions.map(param => ({ diff --git a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts index f1161b202e..79bdf81126 100644 --- a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts @@ -24,6 +24,18 @@ import { import { resolveAppTree } from './resolveAppTree'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; +import { createErrorCollector } from '../wiring/createErrorCollector'; + +const collector = createErrorCollector(); + +afterEach(() => { + const errors = collector.collectErrors(); + if (errors) { + throw new Error( + `Unexpected errors: ${errors.map(e => e.message).join(', ')}`, + ); + } +}); const extension = resolveExtensionDefinition( createExtension({ @@ -43,13 +55,13 @@ const baseSpec = { describe('buildAppTree', () => { it('should fail to create an empty tree', () => { - expect(() => resolveAppTree('app', [])).toThrow( + expect(() => resolveAppTree('app', [], collector)).toThrow( "No root node with id 'app' found in app tree", ); }); it('should create a tree with only one node', () => { - const tree = resolveAppTree('app', [{ ...baseSpec, id: 'app' }]); + const tree = resolveAppTree('app', [{ ...baseSpec, id: 'app' }], collector); expect(tree.root).toEqual({ spec: { ...baseSpec, id: 'app' }, edges: { attachments: new Map() }, @@ -59,15 +71,19 @@ describe('buildAppTree', () => { }); it('should create a tree', () => { - const tree = resolveAppTree('b', [ - { ...baseSpec, id: 'a' }, - { ...baseSpec, id: 'b' }, - { ...baseSpec, id: 'c' }, - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, - { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, - { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, - ]); + const tree = resolveAppTree( + 'b', + [ + { ...baseSpec, id: 'a' }, + { ...baseSpec, id: 'b' }, + { ...baseSpec, id: 'c' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, + { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, + { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, + ], + collector, + ); expect(Array.from(tree.nodes.keys())).toEqual([ 'a', @@ -122,26 +138,30 @@ describe('buildAppTree', () => { }); it('should create a tree with clones', () => { - const tree = resolveAppTree('a', [ - { ...baseSpec, id: 'a' }, - { ...baseSpec, id: 'b', attachTo: { id: 'a', input: 'x' } }, - { - ...baseSpec, - id: 'c', - attachTo: [ - { id: 'a', input: 'x' }, - { id: 'b', input: 'x' }, - ], - }, - { - ...baseSpec, - id: 'd', - attachTo: [ - { id: 'b', input: 'x' }, - { id: 'c', input: 'x' }, - ], - }, - ]); + const tree = resolveAppTree( + 'a', + [ + { ...baseSpec, id: 'a' }, + { ...baseSpec, id: 'b', attachTo: { id: 'a', input: 'x' } }, + { + ...baseSpec, + id: 'c', + attachTo: [ + { id: 'a', input: 'x' }, + { id: 'b', input: 'x' }, + ], + }, + { + ...baseSpec, + id: 'd', + attachTo: [ + { id: 'b', input: 'x' }, + { id: 'c', input: 'x' }, + ], + }, + ], + collector, + ); expect(Array.from(tree.nodes.keys())).toEqual(['a', 'b', 'c', 'd']); @@ -172,15 +192,19 @@ describe('buildAppTree', () => { }); it('should create a tree out of order', () => { - const tree = resolveAppTree('b', [ - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, - { ...baseSpec, id: 'a' }, - { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, - { ...baseSpec, id: 'b' }, - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, - { ...baseSpec, id: 'c' }, - { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, - ]); + const tree = resolveAppTree( + 'b', + [ + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, + { ...baseSpec, id: 'a' }, + { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, + { ...baseSpec, id: 'b' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, + { ...baseSpec, id: 'c' }, + { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, + ], + collector, + ); expect(Array.from(tree.nodes.keys())).toEqual([ 'bx2', @@ -214,13 +238,23 @@ describe('buildAppTree', () => { `); }); - it('throws an error when duplicated extensions are detected', () => { - expect(() => - resolveAppTree('app', [ + it('emits an error when duplicated extensions are detected', () => { + const tree = resolveAppTree( + 'a', + [ { ...baseSpec, id: 'a' }, { ...baseSpec, id: 'a' }, - ]), - ).toThrow("Unexpected duplicate extension id 'a'"); + ], + collector, + ); + expect(Array.from(tree.nodes.keys())).toEqual(['a']); + expect(collector.collectErrors()).toEqual([ + { + code: 'DUPLICATE_EXTENSION_ID', + message: "Unexpected duplicate extension id 'a'", + context: { spec: { ...baseSpec, id: 'a' } }, + }, + ]); }); describe('redirects', () => { @@ -253,12 +287,28 @@ describe('buildAppTree', () => { }), ) as Extension; - expect(() => - resolveAppTree('a', [ + const tree = resolveAppTree( + 'a', + [ { ...baseSpec, id: 'a', extension: e1 }, { ...baseSpec, id: 'b', extension: e2 }, - ]), - ).toThrow("Duplicate redirect target for input 'test' in extension 'b'"); + ], + collector, + ); + + expect(Array.from(tree.nodes.keys())).toEqual(['a', 'b']); + + expect(collector.collectErrors()).toEqual([ + { + code: 'DUPLICATE_REDIRECT_TARGET', + message: + "Duplicate redirect target for input 'test' in extension 'b'", + context: { + spec: { ...baseSpec, id: 'b', extension: e2 }, + inputName: 'test', + }, + }, + ]); }); it('should set the correct attachment point for a redirect', () => { @@ -285,22 +335,26 @@ describe('buildAppTree', () => { }), ) as Extension; - const tree = resolveAppTree('a', [ - { - attachTo: e1.attachTo, - id: 'a', - extension: e1, - disabled: false, - plugin: baseSpec.plugin, - }, - { - attachTo: e2.attachTo, - id: 'b', - extension: e2, - disabled: false, - plugin: baseSpec.plugin, - }, - ]); + const tree = resolveAppTree( + 'a', + [ + { + attachTo: e1.attachTo, + id: 'a', + extension: e1, + disabled: false, + plugin: baseSpec.plugin, + }, + { + attachTo: e2.attachTo, + id: 'b', + extension: e2, + disabled: false, + plugin: baseSpec.plugin, + }, + ], + collector, + ); expect(tree.root).toMatchInlineSnapshot(` { @@ -365,29 +419,33 @@ describe('buildAppTree', () => { }), ) as Extension; - const tree = resolveAppTree('test-2', [ - { - attachTo: e1.attachTo, - id: e1.id, - extension: e1, - disabled: false, - plugin: baseSpec.plugin, - }, - { - attachTo: e2.attachTo, - id: e2.id, - extension: e2, - disabled: false, - plugin: baseSpec.plugin, - }, - { - attachTo: e3.attachTo, - id: e3.id, - extension: e3, - disabled: false, - plugin: baseSpec.plugin, - }, - ]); + const tree = resolveAppTree( + 'test-2', + [ + { + attachTo: e1.attachTo, + id: e1.id, + extension: e1, + disabled: false, + plugin: baseSpec.plugin, + }, + { + attachTo: e2.attachTo, + id: e2.id, + extension: e2, + disabled: false, + plugin: baseSpec.plugin, + }, + { + attachTo: e3.attachTo, + id: e3.id, + extension: e3, + disabled: false, + plugin: baseSpec.plugin, + }, + ], + collector, + ); expect(tree.nodes.get('test-3')?.edges.attachedTo?.node).toBe( tree.nodes.get('test-2'), diff --git a/packages/frontend-app-api/src/tree/resolveAppTree.ts b/packages/frontend-app-api/src/tree/resolveAppTree.ts index dacd06407a..fc2c8f845e 100644 --- a/packages/frontend-app-api/src/tree/resolveAppTree.ts +++ b/packages/frontend-app-api/src/tree/resolveAppTree.ts @@ -23,6 +23,7 @@ import { // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtension } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; +import { ErrorCollector } from '../wiring/createErrorCollector'; function indent(str: string) { return str.replace(/^/gm, ' '); @@ -114,6 +115,7 @@ const isValidAttachmentPoint = ( export function resolveAppTree( rootNodeId: string, specs: AppNodeSpec[], + errorCollector: ErrorCollector, ): AppTree { const nodes = new Map(); @@ -122,7 +124,14 @@ export function resolveAppTree( for (const spec of specs) { // The main check with a more helpful error message happens in resolveAppNodeSpecs if (nodes.has(spec.id)) { - throw new Error(`Unexpected duplicate extension id '${spec.id}'`); + errorCollector.report({ + code: 'DUPLICATE_EXTENSION_ID', + message: `Unexpected duplicate extension id '${spec.id}'`, + context: { + spec, + }, + }); + continue; } const node = new SerializableAppNode(spec); @@ -134,9 +143,15 @@ export function resolveAppTree( for (const replace of input.replaces) { const key = makeRedirectKey(replace); if (redirectTargetsByKey.has(key)) { - throw new Error( - `Duplicate redirect target for input '${inputName}' in extension '${spec.id}'`, - ); + errorCollector.report({ + code: 'DUPLICATE_REDIRECT_TARGET', + message: `Duplicate redirect target for input '${inputName}' in extension '${spec.id}'`, + context: { + spec, + inputName, + }, + }); + continue; } redirectTargetsByKey.set(key, { id: spec.id, input: inputName }); } diff --git a/packages/frontend-app-api/src/wiring/createErrorCollector.ts b/packages/frontend-app-api/src/wiring/createErrorCollector.ts new file mode 100644 index 0000000000..65ef91ccfc --- /dev/null +++ b/packages/frontend-app-api/src/wiring/createErrorCollector.ts @@ -0,0 +1,69 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + AppNode, + AppNodeSpec, + FrontendPlugin, +} from '@backstage/frontend-plugin-api'; + +/** @public */ +export type AppError = { + code: string; + message: string; + context?: { + node?: AppNode; + spec?: AppNodeSpec; + plugin?: FrontendPlugin; + extensionId?: string; + inputName?: string; + dataRefId?: string; + }; +}; + +/** @internal */ +export interface ErrorCollector { + report(report: AppError): void; + child(context?: AppError['context']): ErrorCollector; + collectErrors(): AppError[] | undefined; +} + +/** @internal */ +export function createErrorCollector(context?: AppError['context']) { + const errors: AppError[] = []; + const children: ErrorCollector[] = []; + return { + report(report: AppError) { + errors.push({ ...report, context: { ...context, ...report.context } }); + }, + collectErrors() { + const allErrors = [ + ...errors, + ...children.flatMap(child => child.collectErrors() ?? []), + ]; + errors.length = 0; + if (allErrors.length === 0) { + return undefined; + } + return allErrors; + }, + child(childContext: AppError['context']) { + const child = createErrorCollector(childContext); + children.push(child); + return child; + }, + }; +} diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx index 01e0a6f657..bcb57ce7e7 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx @@ -83,6 +83,11 @@ import { FrontendPluginInfoResolver, } from './createPluginInfoAttacher'; import { createRouteAliasResolver } from '../routing/RouteAliasResolver'; +import { + AppError, + createErrorCollector, + ErrorCollector, +} from './createErrorCollector'; function deduplicateFeatures( allFeatures: FrontendFeature[], @@ -284,12 +289,15 @@ export type CreateSpecializedAppOptions = { export function createSpecializedApp(options?: CreateSpecializedAppOptions): { apis: ApiHolder; tree: AppTree; + errors?: AppError[]; } { const config = options?.config ?? new ConfigReader({}, 'empty-config'); const features = deduplicateFeatures(options?.features ?? []).map( createPluginInfoAttacher(config, options?.advanced?.pluginInfoResolver), ); + const collector = createErrorCollector(); + const tree = resolveAppTree( 'root', resolveAppNodeSpecs({ @@ -299,12 +307,12 @@ export function createSpecializedApp(options?: CreateSpecializedAppOptions): { ], parameters: readAppExtensionsConfig(config), forbidden: new Set(['root']), - allowUnknownExtensionConfig: - options?.advanced?.allowUnknownExtensionConfig, + collector, }), + collector, ); - const factories = createApiFactories({ tree }); + const factories = createApiFactories({ tree, collector }); const appBasePath = getBasePath(config); const appTreeApi = new AppTreeApiProxy(tree, appBasePath); @@ -353,6 +361,7 @@ export function createSpecializedApp(options?: CreateSpecializedAppOptions): { instantiateAppNodeTree( tree.root, apis, + collector, mergeExtensionFactoryMiddleware( options?.advanced?.extensionFactoryMiddleware, ), @@ -366,22 +375,32 @@ export function createSpecializedApp(options?: CreateSpecializedAppOptions): { routeResolutionApi.initialize(routeInfo, routeRefsById.routes); appTreeApi.initialize(routeInfo); - return { apis, tree }; + return { apis, tree, errors: collector.collectErrors() }; } -function createApiFactories(options: { tree: AppTree }): AnyApiFactory[] { +function createApiFactories(options: { + tree: AppTree; + collector: ErrorCollector; +}): AnyApiFactory[] { const emptyApiHolder = ApiRegistry.from([]); const factories = new Array(); for (const apiNode of options.tree.root.edges.attachments.get('apis') ?? []) { - instantiateAppNodeTree(apiNode, emptyApiHolder); - const apiFactory = apiNode.instance?.getData(ApiBlueprint.dataRefs.factory); - if (!apiFactory) { - throw new Error( - `No API factory found in for extension ${apiNode.spec.id}`, - ); + if (!instantiateAppNodeTree(apiNode, emptyApiHolder, options.collector)) { + continue; + } + const apiFactory = apiNode.instance?.getData(ApiBlueprint.dataRefs.factory); + if (apiFactory) { + factories.push(apiFactory); + } else { + options.collector.report({ + code: 'NO_API_FACTORY', + message: `API extension '${apiNode.spec.id}' did not output an API factory`, + context: { + node: apiNode, + }, + }); } - factories.push(apiFactory); } return factories; diff --git a/packages/frontend-app-api/src/wiring/index.ts b/packages/frontend-app-api/src/wiring/index.ts index 2571cadfbd..ce7574852a 100644 --- a/packages/frontend-app-api/src/wiring/index.ts +++ b/packages/frontend-app-api/src/wiring/index.ts @@ -19,3 +19,4 @@ export { type CreateSpecializedAppOptions, } from './createSpecializedApp'; export { type FrontendPluginInfoResolver } from './createPluginInfoAttacher'; +export { type AppError } from './createErrorCollector'; From 5a767f5c8abf8dbc5677e590d2385037ff6e72b4 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Thu, 4 Sep 2025 13:22:34 +0200 Subject: [PATCH 076/233] update docs for table component Signed-off-by: Emma Indal --- docs-ui/src/content/components/table.props.ts | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/docs-ui/src/content/components/table.props.ts b/docs-ui/src/content/components/table.props.ts index 87be472609..1b40055849 100644 --- a/docs-ui/src/content/components/table.props.ts +++ b/docs-ui/src/content/components/table.props.ts @@ -217,18 +217,18 @@ export const cellPropDefs: Record = { }; export const tablePaginationPropDefs: Record = { - pageIndex: { + offset: { type: 'number', - description: 'The current page index.', + description: 'The current offset (starting index) for pagination.', }, pageSize: { type: 'number', description: 'The number of items per page.', }, - setPageIndex: { + setOffset: { type: 'enum', - values: ['(pageIndex: number) => void'], - description: 'Handler that is called when the page index changes.', + values: ['(offset: number) => void'], + description: 'Handler that is called when the offset changes.', }, setPageSize: { type: 'enum', @@ -277,10 +277,7 @@ export const tableUsageSnippet = `import { Cell, ..., TableHeader, TablePaginati `; -export const tableBasicSnippet = `import { Table, TablePagination } from '@backstage/ui'; - -const [pageIndex, setPageIndex] = useState(0); -const [pageSize, setPageSize] = useState(5); +export const tableBasicSnippet = `import { Table, TableHeader, Column, TableBody, Row, Cell, CellProfile, TablePagination, useTable } from '@backstage/ui'; const data = [ { @@ -293,10 +290,13 @@ const data = [ // ... more data ]; -const newData = data4.slice( - pageIndex * pageSize, - (pageIndex + 1) * pageSize, -); +// Uncontrolled pagination (easiest) +const { data: paginatedData, paginationProps } = useTable({ + data, + pagination: { + defaultPageSize: 5, + }, +}); @@ -306,9 +306,9 @@ const newData = data4.slice( Albums - {newData.map(item => ( + {paginatedData?.map(item => ( -
-`; +`; From dd7b6d2f33cbe9a068424fa36ebfe293a03e7ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 4 Sep 2025 13:49:04 +0200 Subject: [PATCH 077/233] Fix getDefault for kubernetesFetcherExtensionPoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/heavy-lies-listen.md | 5 +++++ .../kubernetes-backend/src/service/KubernetesInitializer.ts | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .changeset/heavy-lies-listen.md diff --git a/.changeset/heavy-lies-listen.md b/.changeset/heavy-lies-listen.md new file mode 100644 index 0000000000..9f408b1ea8 --- /dev/null +++ b/.changeset/heavy-lies-listen.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Fix a bug where `getDefault` in the `kubernetesFetcherExtensionPoint` had the wrong `this` value diff --git a/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts b/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts index c32023235f..1b0b9a31e1 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts @@ -183,8 +183,9 @@ export class KubernetesInitializer { async init() { const fetcher = - (await this.opts.fetcher?.({ getDefault: this.defaultFetcher })) ?? - (await this.defaultFetcher()); + (await this.opts.fetcher?.({ + getDefault: () => this.defaultFetcher(), + })) ?? (await this.defaultFetcher()); const authStrategyMap = this.opts.authStrategyMap ?? (await this.defaultAuthStrategy()); From 3eeb8d395c6cd3e8deb59c0ff4d6be30f6a7ade2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 4 Sep 2025 15:40:10 +0200 Subject: [PATCH 078/233] fix api report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- plugins/scaffolder-backend-module-rails/report.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-rails/report.api.md b/plugins/scaffolder-backend-module-rails/report.api.md index a8abcfee1e..c7bbd19a72 100644 --- a/plugins/scaffolder-backend-module-rails/report.api.md +++ b/plugins/scaffolder-backend-module-rails/report.api.md @@ -48,6 +48,7 @@ export function createFetchRailsAction(options: { railsArguments?: | { template?: string | undefined; + api?: boolean | undefined; database?: | 'sqlite3' | 'mysql' @@ -59,7 +60,6 @@ export function createFetchRailsAction(options: { | 'jdbcpostgresql' | 'jdbc' | undefined; - api?: boolean | undefined; force?: boolean | undefined; minimal?: boolean | undefined; railsVersion?: 'edge' | 'master' | 'dev' | 'fromImage' | undefined; From da404afd70598dbe54dbc261ef14b3a6b2ad1425 Mon Sep 17 00:00:00 2001 From: Praphull <1288395+praphull-purohit@users.noreply.github.com> Date: Fri, 5 Sep 2025 09:20:53 +0200 Subject: [PATCH 079/233] Incorporate code review suggestion Signed-off-by: Praphull <1288395+praphull-purohit@users.noreply.github.com> --- .changeset/ripe-boxes-stand.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/ripe-boxes-stand.md b/.changeset/ripe-boxes-stand.md index 163f5ae545..f76f7b2516 100644 --- a/.changeset/ripe-boxes-stand.md +++ b/.changeset/ripe-boxes-stand.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-backend-module-github': minor --- -Add field blockCreations in github branch protection scaffolder actions +Add block creations field in github branch protection scaffolder actions From b9a87f45933adeef2f43e9e272c71d9a75704915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Fri, 5 Sep 2025 09:19:02 +0200 Subject: [PATCH 080/233] Fixed "simplified" in the catalog-graph, solving #30121 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- .changeset/dull-eagles-sin.md | 1 + .changeset/nine-socks-share.md | 5 + packages/core-components/report.api.md | 1 + .../src/components/DependencyGraph/types.ts | 4 + plugins/catalog-graph/report-alpha.api.md | 4 +- .../src/api/DefaultCatalogGraphApi.ts | 2 +- .../CatalogGraphCard/CatalogGraphCard.tsx | 3 +- .../CatalogGraphPage/CatalogGraphPage.tsx | 3 +- .../CatalogGraphPage/DirectionFilter.test.tsx | 2 +- .../CatalogGraphPage/DirectionFilter.tsx | 2 +- .../SelectedRelationsFilter.test.tsx | 2 +- .../useCatalogGraphPage.test.tsx | 2 +- .../CatalogGraphPage/useCatalogGraphPage.ts | 2 +- .../DefaultRenderLabel.tsx | 2 +- .../DefaultRenderNode.tsx | 2 +- .../EntityRelationsGraph.tsx | 4 +- .../components/EntityRelationsGraph/index.ts | 7 - .../useEntityRelationGraph.ts | 16 +- .../useEntityRelationNodesAndEdges.test.tsx | 74 +++----- .../useEntityRelationNodesAndEdges.ts | 178 +++++------------- .../EntityRelationsGraph/useEntityStore.ts | 4 +- .../catalog-graph/src/hooks/useRelations.ts | 20 +- plugins/catalog-graph/src/index.ts | 11 +- .../src/lib/graph-transformations/index.ts | 33 ++++ .../graph-transformations/merge-relations.ts | 50 +++++ .../graph-transformations/order-forward.ts | 33 ++++ .../lib/graph-transformations/reduce-edges.ts | 46 +++++ .../remove-backward-edges.ts | 53 ++++++ .../lib/graph-transformations/set-distance.ts | 98 ++++++++++ .../strip-distant-edges.ts | 78 ++++++++ .../src/lib/graph-transformations/types.ts | 36 ++++ .../src/lib/graph/build-graph.ts | 151 +++++++++++++++ plugins/catalog-graph/src/lib/graph/index.ts | 18 ++ .../types.ts => lib/types/graph.ts} | 0 .../src/{ => lib}/types/index.ts | 10 +- .../src/{ => lib}/types/relations.ts | 0 36 files changed, 738 insertions(+), 219 deletions(-) create mode 100644 .changeset/nine-socks-share.md create mode 100644 plugins/catalog-graph/src/lib/graph-transformations/index.ts create mode 100644 plugins/catalog-graph/src/lib/graph-transformations/merge-relations.ts create mode 100644 plugins/catalog-graph/src/lib/graph-transformations/order-forward.ts create mode 100644 plugins/catalog-graph/src/lib/graph-transformations/reduce-edges.ts create mode 100644 plugins/catalog-graph/src/lib/graph-transformations/remove-backward-edges.ts create mode 100644 plugins/catalog-graph/src/lib/graph-transformations/set-distance.ts create mode 100644 plugins/catalog-graph/src/lib/graph-transformations/strip-distant-edges.ts create mode 100644 plugins/catalog-graph/src/lib/graph-transformations/types.ts create mode 100644 plugins/catalog-graph/src/lib/graph/build-graph.ts create mode 100644 plugins/catalog-graph/src/lib/graph/index.ts rename plugins/catalog-graph/src/{components/EntityRelationsGraph/types.ts => lib/types/graph.ts} (100%) rename plugins/catalog-graph/src/{ => lib}/types/index.ts (84%) rename plugins/catalog-graph/src/{ => lib}/types/relations.ts (100%) diff --git a/.changeset/dull-eagles-sin.md b/.changeset/dull-eagles-sin.md index 864f38f582..8689ea3fc2 100644 --- a/.changeset/dull-eagles-sin.md +++ b/.changeset/dull-eagles-sin.md @@ -3,3 +3,4 @@ --- Support custom relations by using an API to define known relations and which to show by default +Fixes "simplified" bug (#30121) which caused graphs not to be simplified diff --git a/.changeset/nine-socks-share.md b/.changeset/nine-socks-share.md new file mode 100644 index 0000000000..82c96e554e --- /dev/null +++ b/.changeset/nine-socks-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': minor +--- + +Add optional `distance` property to `DependencyEdge` to reflect the distance to a root. diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index fff1d0365a..b16aecb3b1 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -291,6 +291,7 @@ export namespace DependencyGraphTypes { from: string; to: string; label?: string; + distance?: number; }; export type DependencyNode = T & { id: string; diff --git a/packages/core-components/src/components/DependencyGraph/types.ts b/packages/core-components/src/components/DependencyGraph/types.ts index e8fc9f6da3..a449637e49 100644 --- a/packages/core-components/src/components/DependencyGraph/types.ts +++ b/packages/core-components/src/components/DependencyGraph/types.ts @@ -46,6 +46,10 @@ export namespace DependencyGraphTypes { * Label assigned and rendered with the Edge */ label?: string; + /** + * Distance to a root entity + */ + distance?: number; }; /** diff --git a/plugins/catalog-graph/report-alpha.api.md b/plugins/catalog-graph/report-alpha.api.md index 729be9006b..17ef05722a 100644 --- a/plugins/catalog-graph/report-alpha.api.md +++ b/plugins/catalog-graph/report-alpha.api.md @@ -84,9 +84,9 @@ const _default: FrontendPlugin< relations?: string[] | undefined; maxDepth?: number | undefined; kinds?: string[] | undefined; - unidirectional?: boolean | undefined; mergeRelations?: boolean | undefined; relationPairs?: [string, string][] | undefined; + unidirectional?: boolean | undefined; } & { filter?: EntityPredicate | undefined; type?: 'content' | 'summary' | 'info' | undefined; @@ -157,9 +157,9 @@ const _default: FrontendPlugin< rootEntityRefs?: string[] | undefined; maxDepth?: number | undefined; kinds?: string[] | undefined; - unidirectional?: boolean | undefined; mergeRelations?: boolean | undefined; relationPairs?: [string, string][] | undefined; + unidirectional?: boolean | undefined; selectedRelations?: string[] | undefined; selectedKinds?: string[] | undefined; showFilters?: boolean | undefined; diff --git a/plugins/catalog-graph/src/api/DefaultCatalogGraphApi.ts b/plugins/catalog-graph/src/api/DefaultCatalogGraphApi.ts index f64dffb09b..edb7b8f509 100644 --- a/plugins/catalog-graph/src/api/DefaultCatalogGraphApi.ts +++ b/plugins/catalog-graph/src/api/DefaultCatalogGraphApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ALL_RELATION_PAIRS, ALL_RELATIONS, RelationPairs } from '../types'; +import { ALL_RELATION_PAIRS, ALL_RELATIONS, RelationPairs } from '../lib/types'; import { CatalogGraphApi, DefaultRelations, diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx index d0524668aa..2f07b89d3f 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx @@ -32,13 +32,12 @@ import { MouseEvent, ReactNode, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { catalogGraphRouteRef } from '../../routes'; import { - Direction, - EntityNode, EntityRelationsGraph, EntityRelationsGraphProps, } from '../EntityRelationsGraph'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { catalogGraphTranslationRef } from '../../translation'; +import { Direction, EntityNode } from '../../lib/types'; /** @public */ export type CatalogGraphCardClassKey = 'card' | 'graph'; diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx index 27d619c9c5..7e6420da1f 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx @@ -37,8 +37,6 @@ import ToggleButton from '@material-ui/lab/ToggleButton'; import { MouseEvent, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { - Direction, - EntityNode, EntityRelationsGraph, EntityRelationsGraphProps, } from '../EntityRelationsGraph'; @@ -51,6 +49,7 @@ import { SwitchFilter } from './SwitchFilter'; import { useCatalogGraphPage } from './useCatalogGraphPage'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { catalogGraphTranslationRef } from '../../translation'; +import { Direction, EntityNode } from '../../lib/types'; /** @public */ export type CatalogGraphPageClassKey = diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx index 1262797a43..d5cee68971 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx @@ -16,7 +16,7 @@ import { waitFor, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { Direction } from '../EntityRelationsGraph'; +import { Direction } from '../../lib/types'; import { DirectionFilter } from './DirectionFilter'; import { renderInTestApp } from '@backstage/test-utils'; diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.tsx index 5d21f61c6c..060a1d32c6 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.tsx @@ -16,7 +16,7 @@ import { Select, SelectedItems } from '@backstage/core-components'; import Box from '@material-ui/core/Box'; import { useCallback } from 'react'; -import { Direction } from '../EntityRelationsGraph'; +import { Direction } from '../../lib/types'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { catalogGraphTranslationRef } from '../../translation'; diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx index e97b817915..09e09ee822 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx @@ -23,7 +23,7 @@ import { } from '@backstage/catalog-model'; import { waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { ALL_RELATIONS } from '../../types'; +import { ALL_RELATIONS } from '../../lib/types'; import { SelectedRelationsFilter } from './SelectedRelationsFilter'; import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { catalogGraphApiRef, DefaultCatalogGraphApi } from '../../api'; diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.test.tsx index c255281ff0..fa3d67520d 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.test.tsx @@ -18,7 +18,7 @@ import { renderHook, waitFor } from '@testing-library/react'; import { ReactNode } from 'react'; import { act } from 'react-dom/test-utils'; import { BrowserRouter } from 'react-router-dom'; -import { Direction } from '../EntityRelationsGraph'; +import { Direction } from '../../lib/types'; import { useCatalogGraphPage } from './useCatalogGraphPage'; const wrapper = ({ children }: { children?: ReactNode }) => { diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts b/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts index 1f6fb74997..efc2170153 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts @@ -28,7 +28,7 @@ import { useState, } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import { Direction } from '../EntityRelationsGraph'; +import { Direction } from '../../lib/types'; export type CatalogGraphPageValue = { rootEntityNames: CompoundEntityRef[]; diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderLabel.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderLabel.tsx index a38f11fca3..c9ddddb4b3 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderLabel.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderLabel.tsx @@ -15,7 +15,7 @@ */ import { DependencyGraphTypes } from '@backstage/core-components'; import makeStyles from '@material-ui/core/styles/makeStyles'; -import { EntityEdgeData } from './types'; +import { EntityEdgeData } from '../../lib/types'; import classNames from 'classnames'; /** @public */ diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderNode.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderNode.tsx index 3541e4511e..97c17af3ec 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderNode.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderNode.tsx @@ -20,7 +20,7 @@ import { makeStyles } from '@material-ui/core/styles'; import classNames from 'classnames'; import { useLayoutEffect, useRef, useState } from 'react'; import { EntityIcon } from './EntityIcon'; -import { EntityNodeData } from './types'; +import { EntityNodeData } from '../../lib/types'; import { DEFAULT_NAMESPACE } from '@backstage/catalog-model'; /** @public */ diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx index 13bebbabb7..f77ca4b37d 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx @@ -30,8 +30,8 @@ import classNames from 'classnames'; import { MouseEvent, useEffect, useMemo } from 'react'; import { DefaultRenderLabel } from './DefaultRenderLabel'; import { DefaultRenderNode } from './DefaultRenderNode'; -import { RelationPairs } from '../../types'; -import { Direction, EntityEdge, EntityNode } from './types'; +import { RelationPairs } from '../../lib/types'; +import { Direction, EntityEdge, EntityNode } from '../../lib/types'; import { useEntityRelationNodesAndEdges } from './useEntityRelationNodesAndEdges'; /** @public */ diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts index 1c61e4abc9..4a325591ca 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts @@ -18,12 +18,5 @@ export type { EntityRelationsGraphProps, EntityRelationsGraphClassKey, } from './EntityRelationsGraph'; -export { Direction } from './types'; -export type { - EntityEdgeData, - EntityEdge, - EntityNodeData, - EntityNode, -} from './types'; export type { CustomLabelClassKey } from './DefaultRenderLabel'; export type { CustomNodeClassKey } from './DefaultRenderNode'; diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts index f52ac03f7e..1b99cfc074 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts @@ -106,13 +106,15 @@ export function useEntityRelationGraph({ requestEntities, ]); - const filteredEntities = useMemo( - () => - entityFilter - ? pickBy(entities, (value, _key) => entityFilter(value)) - : entities, - [entities, entityFilter], - ); + const filteredEntities = useMemo(() => { + if (loading) { + return {}; + } + + return entityFilter + ? pickBy(entities, (value, _key) => entityFilter(value)) + : entities; + }, [loading, entities, entityFilter]); return { entities: filteredEntities, diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.tsx index 4dfb612646..b896fe1ed3 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.tsx @@ -29,7 +29,7 @@ import { renderHook, waitFor } from '@testing-library/react'; import { filter, keyBy } from 'lodash'; import { useEntityRelationGraph as useEntityRelationGraphMocked } from './useEntityRelationGraph'; import { useEntityRelationNodesAndEdges } from './useEntityRelationNodesAndEdges'; -import { EntityNode } from './types'; +import { EntityNode } from '../../lib/types'; import { catalogGraphApiRef, DefaultCatalogGraphApi } from '../../api'; jest.mock('./useEntityRelationGraph'); @@ -256,24 +256,21 @@ describe('useEntityRelationNodesAndEdges', () => { ]); expect(edges).toEqual([ { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_OWNER_OF, RELATION_OWNED_BY], to: 'k:d/a1', }, { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_HAS_PART, RELATION_PART_OF], to: 'b:d/c1', }, { - from: 'b:d/c1', - label: 'visible', - relations: [RELATION_OWNER_OF, RELATION_OWNED_BY], - to: 'k:d/a1', - }, - { + distance: 2, from: 'b:d/c1', label: 'visible', relations: [RELATION_HAS_PART, RELATION_PART_OF], @@ -333,53 +330,26 @@ describe('useEntityRelationNodesAndEdges', () => { ]); expect(edges).toEqual([ { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_OWNER_OF], to: 'k:d/a1', }, { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_HAS_PART], to: 'b:d/c1', }, { - from: 'b:d/c1', - label: 'visible', - relations: [RELATION_PART_OF], - to: 'b:d/c', - }, - { - from: 'b:d/c1', - label: 'visible', - relations: [RELATION_OWNER_OF], - to: 'k:d/a1', - }, - { + distance: 2, from: 'b:d/c1', label: 'visible', relations: [RELATION_HAS_PART], to: 'b:d/c2', }, - { - from: 'b:d/c2', - label: 'visible', - relations: [RELATION_PART_OF], - to: 'b:d/c1', - }, - { - from: 'k:d/a1', - label: 'visible', - relations: [RELATION_OWNED_BY], - to: 'b:d/c', - }, - { - from: 'k:d/a1', - label: 'visible', - relations: [RELATION_OWNED_BY], - to: 'b:d/c1', - }, ]); }); @@ -434,24 +404,28 @@ describe('useEntityRelationNodesAndEdges', () => { ]); expect(edges).toEqual([ { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_OWNER_OF, RELATION_OWNED_BY], to: 'k:d/a1', }, { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_HAS_PART, RELATION_PART_OF], to: 'b:d/c1', }, { + distance: 2, from: 'b:d/c1', label: 'visible', relations: [RELATION_OWNER_OF, RELATION_OWNED_BY], to: 'k:d/a1', }, { + distance: 2, from: 'b:d/c1', label: 'visible', relations: [RELATION_HAS_PART, RELATION_PART_OF], @@ -511,48 +485,56 @@ describe('useEntityRelationNodesAndEdges', () => { ]); expect(edges).toEqual([ { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_OWNER_OF], to: 'k:d/a1', }, { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_HAS_PART], to: 'b:d/c1', }, { + distance: 1, from: 'b:d/c1', label: 'visible', relations: [RELATION_PART_OF], to: 'b:d/c', }, { + distance: 2, from: 'b:d/c1', label: 'visible', relations: [RELATION_OWNER_OF], to: 'k:d/a1', }, { + distance: 2, from: 'b:d/c1', label: 'visible', relations: [RELATION_HAS_PART], to: 'b:d/c2', }, { + distance: 2, from: 'b:d/c2', label: 'visible', relations: [RELATION_PART_OF], to: 'b:d/c1', }, { + distance: 1, from: 'k:d/a1', label: 'visible', relations: [RELATION_OWNED_BY], to: 'b:d/c', }, { + distance: 2, from: 'k:d/a1', label: 'visible', relations: [RELATION_OWNED_BY], @@ -610,24 +592,21 @@ describe('useEntityRelationNodesAndEdges', () => { ]); expect(edges).toEqual([ { - from: 'b:d/c1', + distance: 1, + from: 'b:d/c2', label: 'visible', - relations: ['hasPart', 'partOf'], - to: 'b:d/c2', + relations: ['partOf', 'hasPart'], + to: 'b:d/c1', }, { + distance: 1, from: 'b:d/c', label: 'visible', relations: ['hasPart', 'partOf'], to: 'b:d/c1', }, { - from: 'b:d/c1', - label: 'visible', - relations: ['ownerOf', 'ownedBy'], - to: 'k:d/a1', - }, - { + distance: 1, from: 'b:d/c', label: 'visible', relations: ['ownerOf', 'ownedBy'], @@ -686,6 +665,7 @@ describe('useEntityRelationNodesAndEdges', () => { ]); expect(edges).toEqual([ { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_OWNER_OF, RELATION_OWNED_BY], @@ -738,12 +718,14 @@ describe('useEntityRelationNodesAndEdges', () => { ]); expect(edges).toEqual([ { + distance: 1, from: 'b:d/c', label: 'visible', relations: [RELATION_HAS_PART, RELATION_PART_OF], to: 'b:d/c1', }, { + distance: 2, from: 'b:d/c1', label: 'visible', relations: [RELATION_HAS_PART, RELATION_PART_OF], diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts index 20b33ba14e..d65100567d 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts @@ -13,13 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { MouseEvent, useState } from 'react'; +import { MouseEvent, useMemo, useState } from 'react'; import useDebounce from 'react-use/esm/useDebounce'; -import { RelationPairs } from '../../types'; -import { EntityEdge, EntityNode } from './types'; +import { RelationPairs } from '../../lib/types'; +import { EntityEdge, EntityNode } from '../../lib/types'; import { useEntityRelationGraph } from './useEntityRelationGraph'; import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { useRelations } from '../../hooks/useRelations'; +import { buildGraph } from '../../lib/graph'; +import { + builtInTransformers, + TransformerContext, +} from '../../lib/graph-transformations'; /** * Generate nodes and edges to render the entity graph. @@ -69,6 +74,11 @@ export function useEntityRelationNodesAndEdges({ relationPairs: incomingRelationPairs, }); + const forwardRelations = useMemo( + () => relationPairs.map(pair => pair[0]), + [relationPairs], + ); + useDebounce( () => { if (!entities || Object.keys(entities).length === 0) { @@ -98,141 +108,46 @@ export function useEntityRelationNodesAndEdges({ return node; }); - const edges: EntityEdge[] = []; - const visitedNodes = new Set(); - const nodeQueue = [...rootEntityRefs]; + const edges = buildGraph({ + rootEntityRefs, + entities, + includeRelation, + kinds, + mergeRelations, + relationPairs, + unidirectional, + }); - const hasEdge = ( - fromRef: string, - toRef: string, - relation: [string, string?], - ): boolean => { - return edges.some( - edge => - fromRef === edge.from && - toRef === edge.to && - edge.relations.some( - rel => rel[0] === relation[0] && rel[1] === relation[1], - ), - ); + const transformerContext: TransformerContext = { + nodeDistances: new Map(), + edges, + + rootEntityRefs, + unidirectional, + maxDepth, + forwardRelations, }; - - while (nodeQueue.length > 0) { - const entityRef = nodeQueue.pop()!; - const entity = entities[entityRef]; - visitedNodes.add(entityRef); - - if (entity) { - entity?.relations?.forEach(rel => { - // Check if the related entity should be displayed, if not, ignore - // the relation too - if (!entities[rel.targetRef]) { - return; - } - - if (!includeRelation(rel.type)) { - return; - } - - if ( - kinds && - !kinds.some(kind => - rel.targetRef.startsWith(`${kind.toLocaleLowerCase('en-US')}:`), - ) - ) { - return; - } - - if (mergeRelations) { - const pair = relationPairs.find( - ([l, r]) => l === rel.type || r === rel.type, - ) ?? [rel.type]; - const [left] = pair; - const from = left === rel.type ? entityRef : rel.targetRef; - const to = left === rel.type ? rel.targetRef : entityRef; - if (!unidirectional || !hasEdge(from, to, pair)) { - edges.push({ - from, - to, - relations: pair, - label: 'visible', - }); - } - } else { - if ( - !unidirectional || - !hasEdge(entityRef, rel.targetRef, [rel.type]) - ) { - edges.push({ - from: entityRef, - to: rel.targetRef, - relations: [rel.type], - label: 'visible', - }); - } - } - - if (!visitedNodes.has(rel.targetRef)) { - nodeQueue.push(rel.targetRef); - visitedNodes.add(rel.targetRef); - } - - // if unidirectional add missing relations as entities are only visited once - if (unidirectional) { - const findIndex = edges.findIndex( - edge => - entityRef === edge.from && - rel.targetRef === edge.to && - !edge.relations.includes(rel.type), - ); - if (findIndex >= 0) { - if (mergeRelations) { - const pair = relationPairs.find( - ([l, r]) => l === rel.type || r === rel.type, - ) ?? [rel.type]; - edges[findIndex].relations = [ - ...edges[findIndex].relations, - ...pair, - ]; - } else { - edges[findIndex].relations = [ - ...edges[findIndex].relations, - rel.type, - ]; - } - } - } - }); - } + builtInTransformers['reduce-edges'](transformerContext); + builtInTransformers['set-distances'](transformerContext); + if (unidirectional) { + builtInTransformers['order-forward'](transformerContext); + builtInTransformers['strip-distant-edges'](transformerContext); + } + if (mergeRelations || unidirectional) { + // Merge relations even if only unidirectional, the next transformer + // 'remove-backward-edges' needs to know about all relations before it + // strips away the backward ones + builtInTransformers['merge-relations'](transformerContext); + } + if (unidirectional && !mergeRelations) { + builtInTransformers['remove-backward-edges'](transformerContext); } - // Reduce edges as the dependency graph anyway ignores duplicated edges regarding from / to - // Additionally, this will improve rendering speed for the dependency graph - const finalEdges = edges.reduce((previousEdges, currentEdge) => { - const indexFound = previousEdges.findIndex( - previousEdge => - previousEdge.from === currentEdge.from && - previousEdge.to === currentEdge.to, - ); - if (indexFound >= 0) { - previousEdges[indexFound] = { - ...previousEdges[indexFound], - relations: Array.from( - new Set([ - ...previousEdges[indexFound].relations, - ...currentEdge.relations, - ]), - ), - }; - return previousEdges; - } - return [...previousEdges, currentEdge]; - }, [] as EntityEdge[]); - - setNodesAndEdges({ nodes, edges: finalEdges }); + setNodesAndEdges({ nodes, edges: transformerContext.edges }); }, 100, [ + maxDepth, entities, rootEntityRefs, kinds, @@ -241,6 +156,7 @@ export function useEntityRelationNodesAndEdges({ mergeRelations, onNodeClick, relationPairs, + forwardRelations, ], ); diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts index c55becdd49..5880468317 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts @@ -16,7 +16,7 @@ import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { Dispatch, useCallback, useRef, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import useAsyncFn from 'react-use/esm/useAsyncFn'; // TODO: This is a good use case for a graphql API, once it is available in the @@ -29,7 +29,7 @@ export function useEntityStore(): { entities: { [ref: string]: Entity }; loading: boolean; error?: Error; - requestEntities: Dispatch; + requestEntities: (entityRefs: string[]) => void; } { const catalogClient = useApi(catalogApiRef); const state = useRef({ diff --git a/plugins/catalog-graph/src/hooks/useRelations.ts b/plugins/catalog-graph/src/hooks/useRelations.ts index 976f17735b..2c734a2cb1 100644 --- a/plugins/catalog-graph/src/hooks/useRelations.ts +++ b/plugins/catalog-graph/src/hooks/useRelations.ts @@ -19,7 +19,7 @@ import { useCallback, useMemo } from 'react'; import { useApi } from '@backstage/core-plugin-api'; import { catalogGraphApiRef } from '../api'; -import { RelationPairs } from '../types'; +import { RelationPairs } from '../lib/types'; export interface UseRelationsOptions { relations?: string[]; @@ -30,6 +30,7 @@ export interface UseRelationsResult { relations: string[]; relationPairs: RelationPairs; defaultRelations: string[]; + relationsToInclude: string[]; includeRelation: (type: string) => boolean; } @@ -48,15 +49,13 @@ export function useRelations( const relations = opts.relations ?? knownRelations; const relationPairs = opts.relationPairs ?? knownRelationPairs; + const relationsToInclude = opts.relations ?? defaultRelations; const includeRelation = useCallback( (type: string) => { - if (opts.relations) { - return opts.relations.includes(type); - } - return defaultRelations.includes(type); + return relationsToInclude.includes(type); }, - [opts.relations, defaultRelations], + [relationsToInclude], ); return useMemo( @@ -64,8 +63,15 @@ export function useRelations( relations, relationPairs, defaultRelations, + relationsToInclude, includeRelation, }), - [relations, relationPairs, defaultRelations, includeRelation], + [ + relations, + relationPairs, + defaultRelations, + relationsToInclude, + includeRelation, + ], ); } diff --git a/plugins/catalog-graph/src/index.ts b/plugins/catalog-graph/src/index.ts index fb12cda14e..68b84c2325 100644 --- a/plugins/catalog-graph/src/index.ts +++ b/plugins/catalog-graph/src/index.ts @@ -26,5 +26,12 @@ export { CatalogGraphPage, EntityCatalogGraphCard } from './extensions'; export * from './api'; export { catalogGraphPlugin } from './plugin'; export { catalogGraphRouteRef } from './routes'; -export { ALL_RELATIONS, ALL_RELATION_PAIRS } from './types'; -export type { RelationPairs } from './types'; +export { ALL_RELATIONS, ALL_RELATION_PAIRS } from './lib/types'; +export type { + RelationPairs, + EntityEdgeData, + EntityEdge, + EntityNodeData, + EntityNode, +} from './lib/types'; +export { Direction } from './lib/types'; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/index.ts b/plugins/catalog-graph/src/lib/graph-transformations/index.ts new file mode 100644 index 0000000000..948596ab3d --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph-transformations/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { GraphTransformer, TransformerContext } from './types'; + +import { reduceEdges } from './reduce-edges'; +import { setDistances } from './set-distance'; +import { orderForward } from './order-forward'; +import { stripDistantEdges } from './strip-distant-edges'; +import { mergeRelations } from './merge-relations'; +import { removeBackwardEdges } from './remove-backward-edges'; + +export const builtInTransformers = { + 'reduce-edges': reduceEdges, + 'set-distances': setDistances, + 'order-forward': orderForward, + 'strip-distant-edges': stripDistantEdges, + 'merge-relations': mergeRelations, + 'remove-backward-edges': removeBackwardEdges, +}; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/merge-relations.ts b/plugins/catalog-graph/src/lib/graph-transformations/merge-relations.ts new file mode 100644 index 0000000000..aed9d0dd85 --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph-transformations/merge-relations.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityEdge } from '../types'; +import { GraphTransformer } from './types'; + +function concatRelations(a: readonly string[], b: readonly string[]): string[] { + return Array.from(new Set([...a, ...b])); +} + +/** Merge relations in multiple edges into one edge */ +export const mergeRelations: GraphTransformer = options => { + // The key is on the form `from-to` + const mergedEdges = new Map(); + options.edges.forEach(edge => { + const keyForward = `${edge.from}-${edge.to}`; + const keyBackward = `${edge.to}-${edge.from}`; + + const edgeForward = mergedEdges.get(keyForward); + const edgeBackward = mergedEdges.get(keyBackward); + + if (edgeForward) { + edgeForward.relations = concatRelations( + edgeForward.relations, + edge.relations, + ); + } else if (edgeBackward) { + edgeBackward.relations = concatRelations( + edge.relations, + edgeBackward.relations, + ); + } else { + mergedEdges.set(keyForward, edge); + } + }); + options.edges = Array.from(mergedEdges.values()); +}; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/order-forward.ts b/plugins/catalog-graph/src/lib/graph-transformations/order-forward.ts new file mode 100644 index 0000000000..d832a1411d --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph-transformations/order-forward.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GraphTransformer } from './types'; + +/** Orders the edges direction so that the graph goes strictly forward */ +export const orderForward: GraphTransformer = ({ nodeDistances, edges }) => { + edges.forEach(edge => { + const fromDistance = nodeDistances.get(edge.from) ?? 0; + const toDistance = nodeDistances.get(edge.to) ?? 0; + + if (toDistance < fromDistance) { + // Reverse order + const { from, to } = edge; + edge.from = to; + edge.to = from; + edge.relations.reverse(); + } + }); +}; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/reduce-edges.ts b/plugins/catalog-graph/src/lib/graph-transformations/reduce-edges.ts new file mode 100644 index 0000000000..115aec438c --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph-transformations/reduce-edges.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityEdge } from '../types'; +import { GraphTransformer } from './types'; + +/** + * Reduce edges as the dependency graph anyway ignores duplicated edges + * regarding from / to + * Additionally, this will improve rendering speed for the dependency graph + */ +export const reduceEdges: GraphTransformer = ctx => { + ctx.edges = ctx.edges.reduce((previousEdges, currentEdge) => { + const indexFound = previousEdges.findIndex( + previousEdge => + previousEdge.from === currentEdge.from && + previousEdge.to === currentEdge.to, + ); + if (indexFound >= 0) { + previousEdges[indexFound] = { + ...previousEdges[indexFound], + relations: Array.from( + new Set([ + ...previousEdges[indexFound].relations, + ...currentEdge.relations, + ]), + ), + }; + return previousEdges; + } + return [...previousEdges, currentEdge]; + }, [] as EntityEdge[]); +}; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/remove-backward-edges.ts b/plugins/catalog-graph/src/lib/graph-transformations/remove-backward-edges.ts new file mode 100644 index 0000000000..1be4c0db01 --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph-transformations/remove-backward-edges.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityEdge } from '../types'; +import { GraphTransformer } from './types'; + +/** + * Remove edges going backwards, if unidirectional and we haven't already + * merged the relations + */ +export const removeBackwardEdges: GraphTransformer = ctx => { + const { forwardRelations, edges } = ctx; + + const edgesMapFrom = new Map(); + + edges.forEach(edge => { + const theseEdges = edgesMapFrom.get(edge.from) ?? []; + theseEdges.push(edge); + edgesMapFrom.set(edge.from, theseEdges); + }); + + ctx.edges = edges.filter(edge => { + if (edge.relations.length === 2) { + // If there are two relations, only keep the forward one + if (!forwardRelations.includes(edge.relations[1])) { + edge.relations.pop(); + } + if (!forwardRelations.includes(edge.relations[0])) { + edge.relations.pop(); + } + return edge.relations.length > 0; + } else if (edge.relations.length === 1) { + // If there is only one relation and it's backwards, remove it + if (!forwardRelations.includes(edge.relations[0])) { + return false; + } + } + return true; + }); +}; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/set-distance.ts b/plugins/catalog-graph/src/lib/graph-transformations/set-distance.ts new file mode 100644 index 0000000000..965f01fc14 --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph-transformations/set-distance.ts @@ -0,0 +1,98 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityEdge } from '../types'; +import { GraphTransformer } from './types'; + +function minDistance(a: number | undefined, b: number | undefined) { + if (typeof a === 'undefined') return b; + if (typeof b === 'undefined') return a; + return Math.min(a, b); +} + +/** Set the distance of each edge to the distance from a root entity */ +export const setDistances: GraphTransformer = ({ + nodeDistances, + edges, + rootEntityRefs, +}) => { + const edgeMap = new Map(); + edges.forEach(edge => { + const fromEdges = edgeMap.get(edge.from) ?? []; + edgeMap.set(edge.from, [...fromEdges, edge]); + + const toEdges = edgeMap.get(edge.to) ?? []; + edgeMap.set(edge.to, [...toEdges, edge]); + }); + + // Sets the distance on as many edges as possible. + // Returns true if all edges have a distance + const setEdgeDistances = () => { + return ( + edges.filter(edge => { + if ( + rootEntityRefs.includes(edge.from) || + rootEntityRefs.includes(edge.to) + ) { + edge.distance = 1; + return false; + } + + const distance = minDistance( + edgeMap + .get(edge.from)! + .reduce( + (prev, cur) => minDistance(prev, cur.distance), + undefined as number | undefined, + ), + edgeMap + .get(edge.to)! + .reduce( + (prev, cur) => minDistance(prev, cur.distance), + undefined as number | undefined, + ), + ); + + if (typeof distance !== 'undefined') { + edge.distance = minDistance(edge.distance, distance + 1); + } + return typeof edge.distance === 'undefined'; + }).length === 0 + ); + }; + + let distanceComplete = false; + while (!distanceComplete) { + distanceComplete = setEdgeDistances(); + } + + rootEntityRefs.forEach(rootEntityRef => { + nodeDistances.set(rootEntityRef, 0); + }); + edges.forEach(edge => { + const curFrom = nodeDistances.get(edge.from); + const curTo = nodeDistances.get(edge.to); + + const fromDistance = minDistance(curFrom, edge.distance); + const toDistance = minDistance(curTo, edge.distance); + if (typeof fromDistance !== 'undefined') { + nodeDistances.set(edge.from, fromDistance); + } + if (typeof toDistance !== 'undefined') { + nodeDistances.set(edge.to, toDistance); + } + }); +}; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/strip-distant-edges.ts b/plugins/catalog-graph/src/lib/graph-transformations/strip-distant-edges.ts new file mode 100644 index 0000000000..aeb7618e5e --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph-transformations/strip-distant-edges.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityEdge } from '../types'; +import { GraphTransformer } from './types'; + +/** + * Removes edges that are further away then other edges leading up to the root. + */ +export const stripDistantEdges: GraphTransformer = options => { + const entityEdges = new Map(); + options.edges.forEach(edge => { + let fromEdges = entityEdges.get(edge.from); + if (!fromEdges) { + fromEdges = []; + entityEdges.set(edge.from, fromEdges); + } + fromEdges.push(edge); + + let toEdges = entityEdges.get(edge.to); + if (!toEdges) { + toEdges = []; + entityEdges.set(edge.to, toEdges); + } + toEdges.push(edge); + }); + + // Remove edges that are further away than other edges leading up to the root + options.edges = options.edges.filter(edge => { + // Get all edges of the 'from' node except this (and similar) edge + const fromEdges = entityEdges + .get(edge.from)! + .filter( + fromEdge => + !(fromEdge.from === edge.from && fromEdge.to === edge.to) && + !(fromEdge.from === edge.to && fromEdge.to === edge.from), + ); + // Get all edges of the 'from' node except this (and similar) edge + const toEdges = entityEdges + .get(edge.to)! + .filter( + toEdge => + !(toEdge.from === edge.from && toEdge.to === edge.to) && + !(toEdge.from === edge.to && toEdge.to === edge.from), + ); + + if (fromEdges.length === 0 || toEdges.length === 0) { + return true; + } + + const shorterFrom = fromEdges.some( + fromEdge => (fromEdge.distance ?? 0) < (edge.distance ?? 0), + ); + const shorterTo = toEdges.some( + toEdge => (toEdge.distance ?? 0) < (edge.distance ?? 0), + ); + + if (shorterFrom && shorterTo) { + // There are shorter edges from both 'from' and 'to' leading to the root, + // exclude this edge + return false; + } + return true; + }); +}; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/types.ts b/plugins/catalog-graph/src/lib/graph-transformations/types.ts new file mode 100644 index 0000000000..6d8fc92392 --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph-transformations/types.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EntityEdge } from '../types'; + +export interface TransformerContext { + /** + * The distance from an entity node to a root entity + * + * NOTE: This is not set until the setDistances transformation is applied + */ + nodeDistances: Map; + + edges: EntityEdge[]; + + // Options: + rootEntityRefs: string[]; + unidirectional: boolean; + maxDepth: number; + forwardRelations: string[]; +} + +export type GraphTransformer = (options: TransformerContext) => void; diff --git a/plugins/catalog-graph/src/lib/graph/build-graph.ts b/plugins/catalog-graph/src/lib/graph/build-graph.ts new file mode 100644 index 0000000000..3a6bf57120 --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph/build-graph.ts @@ -0,0 +1,151 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; + +import type { EntityEdge, RelationPairs } from '../types'; + +export interface BuildGraphOptions { + rootEntityRefs: readonly string[]; + entities: { + [ref: string]: Entity; + }; + includeRelation: (type: string) => boolean; + kinds: readonly string[] | undefined; + mergeRelations: boolean; + relationPairs: RelationPairs; + unidirectional: boolean; +} + +export function buildGraph({ + rootEntityRefs, + entities, + includeRelation, + kinds, + mergeRelations, + relationPairs, + unidirectional, +}: BuildGraphOptions): EntityEdge[] { + const edges: EntityEdge[] = []; + const visitedNodes = new Set(); + const nodeQueue = [...rootEntityRefs]; + + const hasEdge = ( + fromRef: string, + toRef: string, + relation: [string, string?], + ): boolean => { + return edges.some( + edge => + fromRef === edge.from && + toRef === edge.to && + edge.relations.some( + rel => rel[0] === relation[0] && rel[1] === relation[1], + ), + ); + }; + + while (nodeQueue.length > 0) { + const entityRef = nodeQueue.pop()!; + const entity = entities[entityRef]; + visitedNodes.add(entityRef); + + if (entity) { + entity?.relations?.forEach(rel => { + // Check if the related entity should be displayed, if not, ignore + // the relation too + if (!entities[rel.targetRef]) { + return; + } + + if (!includeRelation(rel.type)) { + return; + } + + if ( + kinds && + !kinds.some(kind => + rel.targetRef.startsWith(`${kind.toLocaleLowerCase('en-US')}:`), + ) + ) { + return; + } + + if (mergeRelations) { + const pair = relationPairs.find( + ([l, r]) => l === rel.type || r === rel.type, + ) ?? [rel.type]; + const [left] = pair; + const from = left === rel.type ? entityRef : rel.targetRef; + const to = left === rel.type ? rel.targetRef : entityRef; + if (!unidirectional || !hasEdge(from, to, pair)) { + edges.push({ + from, + to, + relations: pair, + label: 'visible', + }); + } + } else { + if ( + !unidirectional || + !hasEdge(entityRef, rel.targetRef, [rel.type]) + ) { + edges.push({ + from: entityRef, + to: rel.targetRef, + relations: [rel.type], + label: 'visible', + }); + } + } + + if (!visitedNodes.has(rel.targetRef)) { + nodeQueue.push(rel.targetRef); + visitedNodes.add(rel.targetRef); + } + + // if unidirectional add missing relations as entities are only visited once + if (unidirectional) { + const findIndex = edges.findIndex( + edge => + entityRef === edge.from && + rel.targetRef === edge.to && + !edge.relations.includes(rel.type), + ); + if (findIndex >= 0) { + if (mergeRelations) { + const pair = relationPairs.find( + ([l, r]) => l === rel.type || r === rel.type, + ) ?? [rel.type]; + edges[findIndex].relations = [ + ...edges[findIndex].relations, + ...pair, + ]; + } else { + edges[findIndex].relations = [ + ...edges[findIndex].relations, + rel.type, + ]; + } + } + } + }); + } + } + + return edges; +} diff --git a/plugins/catalog-graph/src/lib/graph/index.ts b/plugins/catalog-graph/src/lib/graph/index.ts new file mode 100644 index 0000000000..77741aa7d8 --- /dev/null +++ b/plugins/catalog-graph/src/lib/graph/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { buildGraph } from './build-graph'; +export type { BuildGraphOptions } from './build-graph'; diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/types.ts b/plugins/catalog-graph/src/lib/types/graph.ts similarity index 100% rename from plugins/catalog-graph/src/components/EntityRelationsGraph/types.ts rename to plugins/catalog-graph/src/lib/types/graph.ts diff --git a/plugins/catalog-graph/src/types/index.ts b/plugins/catalog-graph/src/lib/types/index.ts similarity index 84% rename from plugins/catalog-graph/src/types/index.ts rename to plugins/catalog-graph/src/lib/types/index.ts index 63dfb5e3f2..ba567d679a 100644 --- a/plugins/catalog-graph/src/types/index.ts +++ b/plugins/catalog-graph/src/lib/types/index.ts @@ -14,5 +14,13 @@ * limitations under the License. */ -export { ALL_RELATIONS, ALL_RELATION_PAIRS } from './relations'; export type { RelationPairs } from './relations'; +export { ALL_RELATIONS, ALL_RELATION_PAIRS } from './relations'; + +export type { + EntityEdgeData, + EntityEdge, + EntityNodeData, + EntityNode, +} from './graph'; +export { Direction } from './graph'; diff --git a/plugins/catalog-graph/src/types/relations.ts b/plugins/catalog-graph/src/lib/types/relations.ts similarity index 100% rename from plugins/catalog-graph/src/types/relations.ts rename to plugins/catalog-graph/src/lib/types/relations.ts From c9c1d69f3904edf744dd96a6b7492b046edb11ff Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 6 Sep 2025 16:22:06 +0200 Subject: [PATCH 081/233] frontend-app-api: type safe error collection and context Signed-off-by: Patrik Oldsberg --- .../src/tree/instantiateAppNodeTree.ts | 11 +- .../src/wiring/createErrorCollector.ts | 100 ++++++++++++++---- 2 files changed, 86 insertions(+), 25 deletions(-) diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index a33c5943a1..cbf24a98fa 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -64,7 +64,7 @@ function resolveInputDataContainer( extensionData: Array, attachment: AppNode, inputName: string, - collector: ErrorCollector, + collector: ErrorCollector<'node' | 'inputName'>, ): ({ node: AppNode } & ExtensionDataContainer) | undefined { const dataMap = new Map(); @@ -211,7 +211,7 @@ function resolveV2Inputs( >; }, attachments: ReadonlyMap, - collector: ErrorCollector, + parentCollector: ErrorCollector<'node'>, ): | undefined | ResolvedExtensionInputs<{ @@ -223,6 +223,7 @@ function resolveV2Inputs( let failed = false; const resolvedInputs = mapValues(inputMap, (input, inputName) => { const attachedNodes = attachments.get(inputName) ?? []; + const collector = parentCollector.child({ inputName }); if (input.config.singleton) { if (attachedNodes.length > 1) { @@ -232,7 +233,6 @@ function resolveV2Inputs( message: `expected ${ input.config.optional ? 'at most' : 'exactly' } one '${inputName}' input but received multiple: '${attachedNodeIds}'`, - context: { inputName }, }); failed = true; return undefined; @@ -241,7 +241,6 @@ function resolveV2Inputs( collector.report({ code: 'EXTENSION_MISSING_REQUIRED_INPUT', message: `input '${inputName}' is required but was not received`, - context: { inputName }, }); failed = true; } @@ -481,7 +480,7 @@ export function createAppNodeInstance(options: { export function instantiateAppNodeTree( rootNode: AppNode, apis: ApiHolder, - errors: ErrorCollector, + collector: ErrorCollector, extensionFactoryMiddleware?: ExtensionFactoryMiddleware, ): boolean { function createInstance(node: AppNode): AppNodeInstance | undefined { @@ -512,7 +511,7 @@ export function instantiateAppNodeTree( node, apis, attachments: instantiatedAttachments, - collector: errors, + collector, }); return node.instance; diff --git a/packages/frontend-app-api/src/wiring/createErrorCollector.ts b/packages/frontend-app-api/src/wiring/createErrorCollector.ts index 65ef91ccfc..494e6c2d85 100644 --- a/packages/frontend-app-api/src/wiring/createErrorCollector.ts +++ b/packages/frontend-app-api/src/wiring/createErrorCollector.ts @@ -19,34 +19,96 @@ import { AppNodeSpec, FrontendPlugin, } from '@backstage/frontend-plugin-api'; +import { Expand } from '@backstage/types'; -/** @public */ -export type AppError = { - code: string; - message: string; - context?: { - node?: AppNode; - spec?: AppNodeSpec; - plugin?: FrontendPlugin; - extensionId?: string; - inputName?: string; - dataRefId?: string; - }; +type AppErrorTypes = { + // resolveAppNodeSpecs + EXTENSION_FORBIDDEN: 'plugin' | 'extensionId'; + EXTENSION_DUPLICATED: 'plugin' | 'extensionId'; + EXTENSION_CONFIG_FORBIDDEN: 'extensionId'; + EXTENSION_CONFIG_UNKNOWN_EXTENSION: 'extensionId'; + // resolveAppTree + DUPLICATE_EXTENSION_ID: 'spec'; + DUPLICATE_REDIRECT_TARGET: 'spec' | 'inputName'; + // instantiateAppNodeTree + EXTENSION_DUPLICATE_INPUT: 'node' | 'inputName'; + EXTENSION_MISSING_INPUT_DATA: 'node' | 'inputName'; + EXTENSION_TOO_MANY_ATTACHMENTS: 'node' | 'inputName'; + EXTENSION_MISSING_REQUIRED_INPUT: 'node' | 'inputName'; + INVALID_CONFIGURATION: 'node'; + EXTENSION_FACTORY_INVALID_OUTPUT: 'node'; + EXTENSION_FACTORY_DUPLICATE_OUTPUT: 'node' | 'dataRefId'; + EXTENSION_FACTORY_MISSING_REQUIRED_OUTPUT: 'node' | 'dataRefId'; + EXTENSION_FACTORY_UNEXPECTED_OUTPUT: 'node' | 'dataRefId'; + UNEXPECTED_EXTENSION_VERSION: 'node'; + FAILED_TO_INSTANTIATE_EXTENSION: 'node'; + // createSpecializedApp + NO_API_FACTORY: 'node'; }; +type AppErrorContext = { + node?: AppNode; + spec?: AppNodeSpec; + plugin?: FrontendPlugin; + extensionId?: string; + dataRefId?: string; + inputName?: string; +}; + +/** @public */ +export type AppError = + { + code: TCode; + message: string; + context: Expand< + AppErrorContext & + Pick< + Required, + keyof AppErrorTypes extends TCode ? never : AppErrorTypes[TCode] + > + >; + }; + /** @internal */ -export interface ErrorCollector { - report(report: AppError): void; - child(context?: AppError['context']): ErrorCollector; +export interface ErrorCollector< + TContext extends keyof AppErrorContext = never, +> { + // Type-only: here to make sure that all required keys are present + $$contextKeys: { [K in TContext]: K }; + report( + report: Exclude< + AppErrorTypes[TCode], + TContext + > extends infer IContext extends keyof AppErrorContext + ? [IContext] extends [never] + ? { + code: TCode; + message: string; + } + : { + code: TCode; + message: string; + context: Pick, IContext>; + } + : never, + ): void; + child( + context?: TAdditionalContext & AppErrorContext, + ): ErrorCollector< + (TContext | keyof TAdditionalContext) & keyof AppErrorContext + >; collectErrors(): AppError[] | undefined; } /** @internal */ -export function createErrorCollector(context?: AppError['context']) { +export function createErrorCollector( + context?: AppError['context'], +): ErrorCollector { const errors: AppError[] = []; const children: ErrorCollector[] = []; return { - report(report: AppError) { + $$contextKeys: null as any, + report(report) { errors.push({ ...report, context: { ...context, ...report.context } }); }, collectErrors() { @@ -60,10 +122,10 @@ export function createErrorCollector(context?: AppError['context']) { } return allErrors; }, - child(childContext: AppError['context']) { + child(childContext) { const child = createErrorCollector(childContext); children.push(child); - return child; + return child as ErrorCollector; }, }; } From 74ea3e3a86bab5c2a814904afb027df41848a216 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 6 Sep 2025 18:07:42 +0200 Subject: [PATCH 082/233] frontend-app-api: simplified control flow for extension instantiation + fixes Signed-off-by: Patrik Oldsberg --- .../src/tree/instantiateAppNodeTree.test.ts | 2 +- .../src/tree/instantiateAppNodeTree.ts | 253 +++++++++--------- .../src/wiring/createErrorCollector.ts | 2 +- 3 files changed, 131 insertions(+), 126 deletions(-) diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index 9ed0319045..4b086b58b5 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -1613,7 +1613,7 @@ describe('instantiateAppNodeTree', () => { code: 'EXTENSION_MISSING_INPUT_DATA', message: "extension 'app/test' could not be attached because its output data ('test') does not match what the input 'singleton' requires ('other')", - context: { node }, + context: { node, inputName: 'singleton' }, }, ]); }); diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index cbf24a98fa..212cb19bc5 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -29,6 +29,35 @@ import { toInternalExtension } from '../../../frontend-plugin-api/src/wiring/res import { createExtensionDataContainer } from '@internal/frontend'; import { ErrorCollector } from '../wiring/createErrorCollector'; +const INSTANTIATION_FAILED = new Error('Instantiation failed'); + +/** + * Like `array.map`, but if `INSTANTIATION_FAILED` is thrown, the iteration will continue but afterwards re-throw `INSTANTIATION_FAILED` + * @returns + */ +function mapWithFailures( + iterable: Iterable, + callback: (item: T) => U, +): U[] { + let failed = false; + const results = Array.from(iterable).map(item => { + try { + return callback(item); + } catch (error) { + if (error === INSTANTIATION_FAILED) { + failed = true; + } else { + throw error; + } + return null as any; + } + }); + if (failed) { + throw INSTANTIATION_FAILED; + } + return results; +} + type Mutable = { -readonly [P in keyof T]: T[P]; }; @@ -40,24 +69,26 @@ function resolveV1InputDataMap( attachment: AppNode, inputName: string, ) { - return mapValues(dataMap, ref => { - const value = attachment.instance?.getData(ref); - if (value === undefined && !ref.config.optional) { - const expected = Object.values(dataMap) - .filter(r => !r.config.optional) - .map(r => `'${r.id}'`) - .join(', '); + return Object.fromEntries( + mapWithFailures(Object.entries(dataMap), ([key, ref]) => { + const value = attachment.instance?.getData(ref); + if (value === undefined && !ref.config.optional) { + const expected = Object.values(dataMap) + .filter(r => !r.config.optional) + .map(r => `'${r.id}'`) + .join(', '); - const provided = [...(attachment.instance?.getDataRefs() ?? [])] - .map(r => `'${r.id}'`) - .join(', '); + const provided = [...(attachment.instance?.getDataRefs() ?? [])] + .map(r => `'${r.id}'`) + .join(', '); - throw new Error( - `extension '${attachment.spec.id}' could not be attached because its output data (${provided}) does not match what the input '${inputName}' requires (${expected})`, - ); - } - return value; - }); + throw new Error( + `extension '${attachment.spec.id}' could not be attached because its output data (${provided}) does not match what the input '${inputName}' requires (${expected})`, + ); + } + return [key, value]; + }), + ); } function resolveInputDataContainer( @@ -65,18 +96,18 @@ function resolveInputDataContainer( attachment: AppNode, inputName: string, collector: ErrorCollector<'node' | 'inputName'>, -): ({ node: AppNode } & ExtensionDataContainer) | undefined { +): { node: AppNode } & ExtensionDataContainer { const dataMap = new Map(); - let failed = false; - for (const ref of extensionData) { + mapWithFailures(extensionData, ref => { if (dataMap.has(ref.id)) { collector.report({ code: 'EXTENSION_DUPLICATE_INPUT', message: `Unexpected duplicate input data '${ref.id}'`, }); - continue; + return; } + const value = attachment.instance?.getData(ref); if (value === undefined && !ref.config.optional) { const expected = extensionData @@ -92,15 +123,11 @@ function resolveInputDataContainer( code: 'EXTENSION_MISSING_INPUT_DATA', message: `extension '${attachment.spec.id}' could not be attached because its output data (${provided}) does not match what the input '${inputName}' requires (${expected})`, }); - failed = true; + throw INSTANTIATION_FAILED; } dataMap.set(ref.id, value); - } - - if (failed) { - return undefined; - } + }); return { node: attachment, @@ -160,40 +187,52 @@ function resolveV1Inputs( }, attachments: ReadonlyMap, ) { - return mapValues(inputMap, (input, inputName) => { - const attachedNodes = attachments.get(inputName) ?? []; + return Object.fromEntries( + mapWithFailures(Object.entries(inputMap), ([inputName, input]) => { + const attachedNodes = attachments.get(inputName) ?? []; - if (input.config.singleton) { - if (attachedNodes.length > 1) { - const attachedNodeIds = attachedNodes.map(e => e.spec.id); - throw Error( - `expected ${ - input.config.optional ? 'at most' : 'exactly' - } one '${inputName}' input but received multiple: '${attachedNodeIds.join( - "', '", - )}'`, - ); - } else if (attachedNodes.length === 0) { - if (input.config.optional) { - return undefined; + if (input.config.singleton) { + if (attachedNodes.length > 1) { + const attachedNodeIds = attachedNodes.map(e => e.spec.id); + throw Error( + `expected ${ + input.config.optional ? 'at most' : 'exactly' + } one '${inputName}' input but received multiple: '${attachedNodeIds.join( + "', '", + )}'`, + ); + } else if (attachedNodes.length === 0) { + if (input.config.optional) { + return [inputName, undefined]; + } + throw Error(`input '${inputName}' is required but was not received`); } - throw Error(`input '${inputName}' is required but was not received`); - } - return { - node: attachedNodes[0], - output: resolveV1InputDataMap( - input.extensionData, - attachedNodes[0], + return [ inputName, - ), - }; - } + { + node: attachedNodes[0], + output: resolveV1InputDataMap( + input.extensionData, + attachedNodes[0], + inputName, + ), + }, + ]; + } - return attachedNodes.map(attachment => ({ - node: attachment, - output: resolveV1InputDataMap(input.extensionData, attachment, inputName), - })); - }) as { + return [ + inputName, + attachedNodes.map(attachment => ({ + node: attachment, + output: resolveV1InputDataMap( + input.extensionData, + attachment, + inputName, + ), + })), + ]; + }), + ) as { [inputName in string]: { node: AppNode; output: { @@ -212,16 +251,13 @@ function resolveV2Inputs( }, attachments: ReadonlyMap, parentCollector: ErrorCollector<'node'>, -): - | undefined - | ResolvedExtensionInputs<{ - [inputName in string]: ExtensionInput< - ExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }> { - let failed = false; - const resolvedInputs = mapValues(inputMap, (input, inputName) => { +): ResolvedExtensionInputs<{ + [inputName in string]: ExtensionInput< + ExtensionDataRef, + { optional: boolean; singleton: boolean } + >; +}> { + return mapValues(inputMap, (input, inputName) => { const attachedNodes = attachments.get(inputName) ?? []; const collector = parentCollector.child({ inputName }); @@ -234,55 +270,34 @@ function resolveV2Inputs( input.config.optional ? 'at most' : 'exactly' } one '${inputName}' input but received multiple: '${attachedNodeIds}'`, }); - failed = true; - return undefined; + throw INSTANTIATION_FAILED; } else if (attachedNodes.length === 0) { if (!input.config.optional) { collector.report({ code: 'EXTENSION_MISSING_REQUIRED_INPUT', message: `input '${inputName}' is required but was not received`, }); - failed = true; + throw INSTANTIATION_FAILED; } return undefined; } - const data = resolveInputDataContainer( + return resolveInputDataContainer( input.extensionData, attachedNodes[0], inputName, collector, ); - if (data === undefined) { - failed = true; - return undefined; - } - return data; } - if (failed) { - return undefined; - } - - return attachedNodes.map(attachment => { - const data = resolveInputDataContainer( + return mapWithFailures(attachedNodes, attachment => + resolveInputDataContainer( input.extensionData, attachment, inputName, collector, - ); - if (data === undefined) { - failed = true; - return undefined; - } - return data; - }); - }); - - if (failed) { - return undefined; - } - - return resolvedInputs as ResolvedExtensionInputs<{ + ), + ); + }) as ResolvedExtensionInputs<{ [inputName in string]: ExtensionInput< ExtensionDataRef, { optional: boolean; singleton: boolean } @@ -346,19 +361,15 @@ export function createAppNodeInstance(options: { extensionDataRefs.add(ref); } } else if (internalExtension.version === 'v2') { - const inputs = resolveV2Inputs( - internalExtension.inputs, - attachments, - collector, - ); - if (inputs === undefined) { - return undefined; - } const context = { node, apis, config: parsedConfig, - inputs, + inputs: resolveV2Inputs( + internalExtension.inputs, + attachments, + collector, + ), }; const outputDataValues = options.extensionFactoryMiddleware ? createExtensionDataContainer( @@ -385,13 +396,11 @@ export function createAppNodeInstance(options: { code: 'EXTENSION_FACTORY_INVALID_OUTPUT', message: 'extension factory did not provide an iterable object', }); - return undefined; + throw INSTANTIATION_FAILED; } - let failed = false; - const outputDataMap = new Map(); - for (const value of outputDataValues) { + mapWithFailures(outputDataValues, value => { if (outputDataMap.has(value.id)) { collector.report({ code: 'EXTENSION_FACTORY_DUPLICATE_OUTPUT', @@ -400,14 +409,11 @@ export function createAppNodeInstance(options: { dataRefId: value.id, }, }); - failed = true; + throw INSTANTIATION_FAILED; } else { outputDataMap.set(value.id, value.value); } - } - if (failed) { - return undefined; - } + }); for (const ref of internalExtension.output) { const value = outputDataMap.get(ref.id); @@ -421,16 +427,13 @@ export function createAppNodeInstance(options: { dataRefId: ref.id, }, }); - failed = true; + throw INSTANTIATION_FAILED; } } else { extensionData.set(ref.id, value); extensionDataRefs.add(ref); } } - if (failed) { - return undefined; - } if (outputDataMap.size > 0) { for (const dataRefId of outputDataMap.keys()) { @@ -451,15 +454,17 @@ export function createAppNodeInstance(options: { (internalExtension as any).version }'`, }); - return undefined; + throw INSTANTIATION_FAILED; } } catch (e) { - collector.report({ - code: 'FAILED_TO_INSTANTIATE_EXTENSION', - message: `Failed to instantiate extension '${id}'${ - e.name === 'Error' ? `, ${e.message}` : `; caused by ${e}` - }`, - }); + if (e !== INSTANTIATION_FAILED) { + collector.report({ + code: 'FAILED_TO_INSTANTIATE_EXTENSION', + message: `Failed to instantiate extension '${id}'${ + e.name === 'Error' ? `, ${e.message}` : `; caused by ${e}` + }`, + }); + } return undefined; } diff --git a/packages/frontend-app-api/src/wiring/createErrorCollector.ts b/packages/frontend-app-api/src/wiring/createErrorCollector.ts index 494e6c2d85..cc51f146ce 100644 --- a/packages/frontend-app-api/src/wiring/createErrorCollector.ts +++ b/packages/frontend-app-api/src/wiring/createErrorCollector.ts @@ -123,7 +123,7 @@ export function createErrorCollector( return allErrors; }, child(childContext) { - const child = createErrorCollector(childContext); + const child = createErrorCollector({ ...context, ...childContext }); children.push(child); return child as ErrorCollector; }, From 004557156bd6ce35c04dba1b34cf39faadd68c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Sat, 6 Sep 2025 23:00:26 +0200 Subject: [PATCH 083/233] Further adaptations to transformations to align almost identical with how renderings used to be MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- .../EntityRelationsGraph.tsx | 4 + .../useEntityRelationNodesAndEdges.ts | 66 +++++++++---- .../src/lib/graph-transformations/index.ts | 11 ++- .../graph-transformations/merge-relations.ts | 12 ++- .../graph-transformations/order-forward.ts | 71 ++++++++++++-- .../remove-backward-edges.ts | 21 +---- .../lib/graph-transformations/set-distance.ts | 92 +++++++------------ .../src/lib/graph-transformations/types.ts | 56 ++++++++++- 8 files changed, 217 insertions(+), 116 deletions(-) diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx index f77ca4b37d..47ce365cb6 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx @@ -33,6 +33,7 @@ import { DefaultRenderNode } from './DefaultRenderNode'; import { RelationPairs } from '../../lib/types'; import { Direction, EntityEdge, EntityNode } from '../../lib/types'; import { useEntityRelationNodesAndEdges } from './useEntityRelationNodesAndEdges'; +import { GraphTransformationDebugger } from '../../lib/graph-transformations'; /** @public */ export type EntityRelationsGraphClassKey = 'progress' | 'container' | 'graph'; @@ -91,6 +92,7 @@ export type EntityRelationsGraphProps = { renderLabel?: DependencyGraphTypes.RenderLabelFunction; curve?: 'curveStepBefore' | 'curveMonotoneX'; showArrowHeads?: boolean; + onPostTransformation?: GraphTransformationDebugger; }; /** @@ -116,6 +118,7 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => { renderLabel, curve, showArrowHeads, + onPostTransformation, } = props; const theme = useTheme(); @@ -139,6 +142,7 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => { entityFilter, onNodeClick, relationPairs, + onPostTransformation, }); useEffect(() => { diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts index d65100567d..2aa301fef9 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { MouseEvent, useMemo, useState } from 'react'; +import { MouseEvent, useState } from 'react'; import useDebounce from 'react-use/esm/useDebounce'; import { RelationPairs } from '../../lib/types'; import { EntityEdge, EntityNode } from '../../lib/types'; @@ -22,8 +22,12 @@ import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { useRelations } from '../../hooks/useRelations'; import { buildGraph } from '../../lib/graph'; import { - builtInTransformers, - TransformerContext, + BuiltInTransformations, + builtInTransformations, + cloneTransformationContext, + GraphTransformationDebugger, + GraphTransformer, + TransformationContext, } from '../../lib/graph-transformations'; /** @@ -39,6 +43,7 @@ export function useEntityRelationNodesAndEdges({ entityFilter, onNodeClick, relationPairs: incomingRelationPairs, + onPostTransformation, }: { rootEntityRefs: string[]; maxDepth?: number; @@ -49,6 +54,7 @@ export function useEntityRelationNodesAndEdges({ entityFilter?: (entity: Entity) => boolean; onNodeClick?: (value: EntityNode, event: MouseEvent) => void; relationPairs?: RelationPairs; + onPostTransformation?: GraphTransformationDebugger; }): { loading: boolean; nodes?: EntityNode[]; @@ -74,11 +80,6 @@ export function useEntityRelationNodesAndEdges({ relationPairs: incomingRelationPairs, }); - const forwardRelations = useMemo( - () => relationPairs.map(pair => pair[0]), - [relationPairs], - ); - useDebounce( () => { if (!entities || Object.keys(entities).length === 0) { @@ -118,32 +119,62 @@ export function useEntityRelationNodesAndEdges({ unidirectional, }); - const transformerContext: TransformerContext = { + const transformationContext: TransformationContext = { nodeDistances: new Map(), edges, + nodes, rootEntityRefs, unidirectional, maxDepth, - forwardRelations, }; - builtInTransformers['reduce-edges'](transformerContext); - builtInTransformers['set-distances'](transformerContext); + + const debugTransformation = ( + transformation: BuiltInTransformations | GraphTransformer | undefined, + ) => { + onPostTransformation?.( + typeof transformation === 'function' + ? transformation.name + : transformation, + transformationContext, + cloneTransformationContext(transformationContext), + ); + }; + + debugTransformation(undefined); + + const runTransformation = ( + transformation: BuiltInTransformations | GraphTransformer, + ) => { + if (typeof transformation === 'function') { + transformation(transformationContext); + } else { + builtInTransformations[transformation](transformationContext); + } + + debugTransformation(transformation); + }; + + runTransformation('reduce-edges'); + runTransformation('set-distances'); if (unidirectional) { - builtInTransformers['order-forward'](transformerContext); - builtInTransformers['strip-distant-edges'](transformerContext); + runTransformation('strip-distant-edges'); } if (mergeRelations || unidirectional) { // Merge relations even if only unidirectional, the next transformer // 'remove-backward-edges' needs to know about all relations before it // strips away the backward ones - builtInTransformers['merge-relations'](transformerContext); + runTransformation('merge-relations'); } if (unidirectional && !mergeRelations) { - builtInTransformers['remove-backward-edges'](transformerContext); + runTransformation('order-forward'); + runTransformation('remove-backward-edges'); } - setNodesAndEdges({ nodes, edges: transformerContext.edges }); + setNodesAndEdges({ + nodes: transformationContext.nodes, + edges: transformationContext.edges, + }); }, 100, [ @@ -156,7 +187,6 @@ export function useEntityRelationNodesAndEdges({ mergeRelations, onNodeClick, relationPairs, - forwardRelations, ], ); diff --git a/plugins/catalog-graph/src/lib/graph-transformations/index.ts b/plugins/catalog-graph/src/lib/graph-transformations/index.ts index 948596ab3d..3ed4cdba21 100644 --- a/plugins/catalog-graph/src/lib/graph-transformations/index.ts +++ b/plugins/catalog-graph/src/lib/graph-transformations/index.ts @@ -14,7 +14,12 @@ * limitations under the License. */ -export type { GraphTransformer, TransformerContext } from './types'; +export type { + GraphTransformer, + TransformationContext, + GraphTransformationDebugger, +} from './types'; +export { cloneTransformationContext } from './types'; import { reduceEdges } from './reduce-edges'; import { setDistances } from './set-distance'; @@ -23,7 +28,7 @@ import { stripDistantEdges } from './strip-distant-edges'; import { mergeRelations } from './merge-relations'; import { removeBackwardEdges } from './remove-backward-edges'; -export const builtInTransformers = { +export const builtInTransformations = { 'reduce-edges': reduceEdges, 'set-distances': setDistances, 'order-forward': orderForward, @@ -31,3 +36,5 @@ export const builtInTransformers = { 'merge-relations': mergeRelations, 'remove-backward-edges': removeBackwardEdges, }; + +export type BuiltInTransformations = keyof typeof builtInTransformations; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/merge-relations.ts b/plugins/catalog-graph/src/lib/graph-transformations/merge-relations.ts index aed9d0dd85..88d4bbcd10 100644 --- a/plugins/catalog-graph/src/lib/graph-transformations/merge-relations.ts +++ b/plugins/catalog-graph/src/lib/graph-transformations/merge-relations.ts @@ -17,14 +17,17 @@ import { EntityEdge } from '../types'; import { GraphTransformer } from './types'; -function concatRelations(a: readonly string[], b: readonly string[]): string[] { - return Array.from(new Set([...a, ...b])); +function concatRelations( + a: readonly string[] | undefined, + b: readonly string[] | undefined, +): string[] { + return Array.from(new Set([...(a ?? []), ...(b ?? [])])); } /** Merge relations in multiple edges into one edge */ export const mergeRelations: GraphTransformer = options => { - // The key is on the form `from-to` const mergedEdges = new Map(); + options.edges.forEach(edge => { const keyForward = `${edge.from}-${edge.to}`; const keyBackward = `${edge.to}-${edge.from}`; @@ -39,12 +42,13 @@ export const mergeRelations: GraphTransformer = options => { ); } else if (edgeBackward) { edgeBackward.relations = concatRelations( - edge.relations, edgeBackward.relations, + edge.relations, ); } else { mergedEdges.set(keyForward, edge); } }); + options.edges = Array.from(mergedEdges.values()); }; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/order-forward.ts b/plugins/catalog-graph/src/lib/graph-transformations/order-forward.ts index d832a1411d..964c55e352 100644 --- a/plugins/catalog-graph/src/lib/graph-transformations/order-forward.ts +++ b/plugins/catalog-graph/src/lib/graph-transformations/order-forward.ts @@ -14,20 +14,71 @@ * limitations under the License. */ +import { EntityEdge } from '../types'; import { GraphTransformer } from './types'; /** Orders the edges direction so that the graph goes strictly forward */ -export const orderForward: GraphTransformer = ({ nodeDistances, edges }) => { +export const orderForward: GraphTransformer = ({ + rootEntityRefs, + nodes, + edges, +}) => { + const entityEdges = new Map(); edges.forEach(edge => { - const fromDistance = nodeDistances.get(edge.from) ?? 0; - const toDistance = nodeDistances.get(edge.to) ?? 0; - - if (toDistance < fromDistance) { - // Reverse order - const { from, to } = edge; - edge.from = to; - edge.to = from; - edge.relations.reverse(); + let fromEdges = entityEdges.get(edge.from); + if (!fromEdges) { + fromEdges = []; + entityEdges.set(edge.from, fromEdges); } + fromEdges.push(edge); + + let toEdges = entityEdges.get(edge.to); + if (!toEdges) { + toEdges = []; + entityEdges.set(edge.to, toEdges); + } + toEdges.push(edge); + }); + + const visitedNodes = new Set(); + + for (const rootEntityRef of rootEntityRefs) { + let currentNodes: string[] = [rootEntityRef].filter( + node => !visitedNodes.has(node), + ); + + while (currentNodes.length > 0) { + for (const currentNode of currentNodes) { + visitedNodes.add(currentNode); + } + + const nextNodes: string[] = []; + + currentNodes.forEach(node => { + entityEdges.get(node)?.forEach(edge => { + if (edge.to === node && !visitedNodes.has(edge.from)) { + // Reverse direction + const { from, to } = edge; + edge.from = to; + edge.to = from; + edge.relations.reverse(); + } + + nextNodes.push(edge.from, edge.to); + }); + }); + + currentNodes = Array.from(new Set(nextNodes)).filter( + node => !visitedNodes.has(node), + ); + } + } + + const nodeOrder = Array.from(visitedNodes); + + nodes.sort((a, b) => { + const aOrder = nodeOrder.findIndex(node => node === a.id); + const bOrder = nodeOrder.findIndex(node => node === b.id); + return aOrder - bOrder; }); }; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/remove-backward-edges.ts b/plugins/catalog-graph/src/lib/graph-transformations/remove-backward-edges.ts index 1be4c0db01..2c6f634ba9 100644 --- a/plugins/catalog-graph/src/lib/graph-transformations/remove-backward-edges.ts +++ b/plugins/catalog-graph/src/lib/graph-transformations/remove-backward-edges.ts @@ -22,7 +22,7 @@ import { GraphTransformer } from './types'; * merged the relations */ export const removeBackwardEdges: GraphTransformer = ctx => { - const { forwardRelations, edges } = ctx; + const { edges } = ctx; const edgesMapFrom = new Map(); @@ -32,22 +32,9 @@ export const removeBackwardEdges: GraphTransformer = ctx => { edgesMapFrom.set(edge.from, theseEdges); }); - ctx.edges = edges.filter(edge => { - if (edge.relations.length === 2) { - // If there are two relations, only keep the forward one - if (!forwardRelations.includes(edge.relations[1])) { - edge.relations.pop(); - } - if (!forwardRelations.includes(edge.relations[0])) { - edge.relations.pop(); - } - return edge.relations.length > 0; - } else if (edge.relations.length === 1) { - // If there is only one relation and it's backwards, remove it - if (!forwardRelations.includes(edge.relations[0])) { - return false; - } + edges.forEach(edge => { + if (edge.relations.length > 1) { + edge.relations = edge.relations.slice(0, 1); } - return true; }); }; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/set-distance.ts b/plugins/catalog-graph/src/lib/graph-transformations/set-distance.ts index 965f01fc14..5986512305 100644 --- a/plugins/catalog-graph/src/lib/graph-transformations/set-distance.ts +++ b/plugins/catalog-graph/src/lib/graph-transformations/set-distance.ts @@ -17,12 +17,6 @@ import { EntityEdge } from '../types'; import { GraphTransformer } from './types'; -function minDistance(a: number | undefined, b: number | undefined) { - if (typeof a === 'undefined') return b; - if (typeof b === 'undefined') return a; - return Math.min(a, b); -} - /** Set the distance of each edge to the distance from a root entity */ export const setDistances: GraphTransformer = ({ nodeDistances, @@ -32,67 +26,45 @@ export const setDistances: GraphTransformer = ({ const edgeMap = new Map(); edges.forEach(edge => { const fromEdges = edgeMap.get(edge.from) ?? []; - edgeMap.set(edge.from, [...fromEdges, edge]); + fromEdges.push(edge); + edgeMap.set(edge.from, fromEdges); const toEdges = edgeMap.get(edge.to) ?? []; - edgeMap.set(edge.to, [...toEdges, edge]); + toEdges.push(edge); + edgeMap.set(edge.to, toEdges); }); - // Sets the distance on as many edges as possible. - // Returns true if all edges have a distance - const setEdgeDistances = () => { - return ( - edges.filter(edge => { - if ( - rootEntityRefs.includes(edge.from) || - rootEntityRefs.includes(edge.to) - ) { - edge.distance = 1; - return false; - } - - const distance = minDistance( - edgeMap - .get(edge.from)! - .reduce( - (prev, cur) => minDistance(prev, cur.distance), - undefined as number | undefined, - ), - edgeMap - .get(edge.to)! - .reduce( - (prev, cur) => minDistance(prev, cur.distance), - undefined as number | undefined, - ), - ); - - if (typeof distance !== 'undefined') { - edge.distance = minDistance(edge.distance, distance + 1); - } - return typeof edge.distance === 'undefined'; - }).length === 0 - ); - }; - - let distanceComplete = false; - while (!distanceComplete) { - distanceComplete = setEdgeDistances(); - } - rootEntityRefs.forEach(rootEntityRef => { nodeDistances.set(rootEntityRef, 0); }); - edges.forEach(edge => { - const curFrom = nodeDistances.get(edge.from); - const curTo = nodeDistances.get(edge.to); + const visitedNodes = new Set(rootEntityRefs); + let currentNodes = rootEntityRefs; + let distance = 0; - const fromDistance = minDistance(curFrom, edge.distance); - const toDistance = minDistance(curTo, edge.distance); - if (typeof fromDistance !== 'undefined') { - nodeDistances.set(edge.from, fromDistance); + while (currentNodes.length > 0) { + const thisDistance = ++distance; + + const nextNodes: string[] = []; + currentNodes.forEach(node => { + edgeMap.get(node)?.forEach(edge => { + edge.distance ??= thisDistance; + + const fromDistance = nodeDistances.get(edge.from) ?? thisDistance; + nodeDistances.set(edge.from, fromDistance); + nextNodes.push(edge.from); + + const toDistance = nodeDistances.get(edge.to) ?? thisDistance; + nodeDistances.set(edge.to, toDistance); + nextNodes.push(edge.to); + }); + }); + + for (const currentNode of currentNodes) { + visitedNodes.add(currentNode); } - if (typeof toDistance !== 'undefined') { - nodeDistances.set(edge.to, toDistance); - } - }); + + currentNodes = Array.from(new Set(nextNodes)).filter( + node => !visitedNodes.has(node), + ); + } }; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/types.ts b/plugins/catalog-graph/src/lib/graph-transformations/types.ts index 6d8fc92392..9e498ba027 100644 --- a/plugins/catalog-graph/src/lib/graph-transformations/types.ts +++ b/plugins/catalog-graph/src/lib/graph-transformations/types.ts @@ -14,23 +14,69 @@ * limitations under the License. */ -import { EntityEdge } from '../types'; +import { EntityEdge, EntityNode } from '../types'; -export interface TransformerContext { +/** + * Contextual information for a graph transformation. + * + * @public + */ +export interface TransformationContext { /** * The distance from an entity node to a root entity * - * NOTE: This is not set until the setDistances transformation is applied + * NOTE: This is empty until the 'set-distances' transformation is applied */ nodeDistances: Map; edges: EntityEdge[]; + nodes: EntityNode[]; // Options: rootEntityRefs: string[]; unidirectional: boolean; maxDepth: number; - forwardRelations: string[]; } -export type GraphTransformer = (options: TransformerContext) => void; +/** + * A function that transforms a graph. The function modifies `nodes` and `edges` + * in place. + * + * @public + */ +export type GraphTransformer = (context: TransformationContext) => void; + +/** + * A function that debugs a graph transformation. + * It is given the three arguments: + * * The transformation that was just applied (or undefined before the first + * transformation). If the transformation is a function, its `.name` property + * will be used. + * * The current state (context) of the graph, which _can_ be mutated + * * A cloned copy of the context, useful for logging (as transformations are + * made in place, the original context is modified and the logs will be + * confusing) + * + * @public + */ +export type GraphTransformationDebugger = ( + transformation: string | undefined, + transformationContext: TransformationContext, + clonedContext: TransformationContext, +) => void; + +/** @internal */ +export function cloneTransformationContext( + transformationContext: TransformationContext, +): TransformationContext { + const clonesContext = JSON.parse( + JSON.stringify(transformationContext), + ) as TransformationContext; + clonesContext.edges = clonesContext.edges.sort((a, b) => { + return a.from.localeCompare(b.from) || a.to.localeCompare(b.to); + }); + + clonesContext.nodeDistances = new Map(transformationContext.nodeDistances); + + return clonesContext; +} From f4f53566305a5fdfde966550dcadc7901f5876ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Sat, 6 Sep 2025 23:04:33 +0200 Subject: [PATCH 084/233] Updated api-report and exported missing types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/catalog-graph/report.api.md | 23 +++++++++++++++++++++++ plugins/catalog-graph/src/index.ts | 4 ++++ 2 files changed, 27 insertions(+) diff --git a/plugins/catalog-graph/report.api.md b/plugins/catalog-graph/report.api.md index fd1e403473..883c79acf4 100644 --- a/plugins/catalog-graph/report.api.md +++ b/plugins/catalog-graph/report.api.md @@ -181,8 +181,31 @@ export type EntityRelationsGraphProps = { renderLabel?: DependencyGraphTypes.RenderLabelFunction; curve?: 'curveStepBefore' | 'curveMonotoneX'; showArrowHeads?: boolean; + onPostTransformation?: GraphTransformationDebugger; }; +// @public +export type GraphTransformationDebugger = ( + transformation: string | undefined, + transformationContext: TransformationContext, + clonedContext: TransformationContext, +) => void; + // @public export type RelationPairs = [string, string][]; + +// @public +export interface TransformationContext { + // (undocumented) + edges: EntityEdge[]; + // (undocumented) + maxDepth: number; + nodeDistances: Map; + // (undocumented) + nodes: EntityNode[]; + // (undocumented) + rootEntityRefs: string[]; + // (undocumented) + unidirectional: boolean; +} ``` diff --git a/plugins/catalog-graph/src/index.ts b/plugins/catalog-graph/src/index.ts index 68b84c2325..d020e96989 100644 --- a/plugins/catalog-graph/src/index.ts +++ b/plugins/catalog-graph/src/index.ts @@ -35,3 +35,7 @@ export type { EntityNode, } from './lib/types'; export { Direction } from './lib/types'; +export type { + TransformationContext, + GraphTransformationDebugger, +} from './lib/graph-transformations'; From 169ae728b8d86364b5deb880300550a36a653b01 Mon Sep 17 00:00:00 2001 From: Owen Shartle Date: Sat, 6 Sep 2025 21:23:00 -0400 Subject: [PATCH 085/233] Moving the TechDocs entity annotation FAQ entry. Signed-off-by: Owen Shartle --- docs/features/techdocs/FAQ.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/features/techdocs/FAQ.md b/docs/features/techdocs/FAQ.md index 323489ddab..24777e5763 100644 --- a/docs/features/techdocs/FAQ.md +++ b/docs/features/techdocs/FAQ.md @@ -46,6 +46,10 @@ annotation should still be present in entity descriptor file (e.g. `catalog-info.yaml`) for Backstage to know that TechDocs is enabled for the entity. +#### What happens when you navigate to a TechDocs URL for an entity uses the `backstage.io/techdocs-entity` annotation? + +If you navigate to a TechDocs URL in the format `docs/{namespace}/{kind}/{name}` for an entity that has the `backstage.io/techdocs-entity` annotation (instead of the `backstage.io/techdocs-ref` annotation), then Backstage will redirect to the TechDocs page of the entity referenced in the value of that annotation. + #### Is it possible for users to suggest changes or provide feedback on a TechDocs page? This is supported for TechDocs sites whose source code is hosted in either @@ -57,7 +61,3 @@ your `mkdocs.yml` files per If the host name of your source code hosting URL does not include `github` or `gitlab`, an `integrations` entry in your `app-config.yaml` pointed at your source code provider is also needed (only the `host` key is necessary). - -#### What happens when you navigate to a TechDocs URL for an entity uses the `backstage.io/techdocs-entity` annotation? - -If you navigate to a TechDocs URL in the format `docs/{namespace}/{kind}/{name}` for an entity that has the `backstage.io/techdocs-entity` annotation (instead of the `backstage.io/techdocs-ref` annotation), then Backstage will redirect to the TechDocs page of the entity referenced in the value of that annotation. From d27c0327768917a1dbeb7b5b2ab0aef77b22a44d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 7 Sep 2025 11:45:40 +0200 Subject: [PATCH 086/233] frontend-app-api: refine app error types Signed-off-by: Patrik Oldsberg --- .../src/tree/instantiateAppNodeTree.test.ts | 56 +++++++++---------- .../src/tree/instantiateAppNodeTree.ts | 22 ++++---- .../src/tree/resolveAppNodeSpecs.test.ts | 6 +- .../src/tree/resolveAppNodeSpecs.ts | 16 +++--- .../src/tree/resolveAppTree.test.ts | 15 ++--- .../src/tree/resolveAppTree.ts | 13 +---- .../src/wiring/createErrorCollector.ts | 39 +++++-------- .../src/wiring/createSpecializedApp.tsx | 2 +- 8 files changed, 74 insertions(+), 95 deletions(-) diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index 4b086b58b5..4f1589d3a3 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -411,7 +411,7 @@ describe('instantiateAppNodeTree', () => { ).toBeUndefined(); expect(collector.collectErrors()).toEqual([ { - code: 'INVALID_CONFIGURATION', + code: 'EXTENSION_CONFIGURATION_INVALID', message: "Invalid configuration for extension 'app/test'; caused by Error: Expected number, received string at 'other'", context: { node }, @@ -443,7 +443,7 @@ describe('instantiateAppNodeTree', () => { ).toBeUndefined(); expect(collector.collectErrors()).toEqual([ { - code: 'FAILED_TO_INSTANTIATE_EXTENSION', + code: 'EXTENSION_FACTORY_ERROR', message: "Failed to instantiate extension 'app/test'; caused by NopeError: NOPE", context: { node }, @@ -476,7 +476,7 @@ describe('instantiateAppNodeTree', () => { ).toBeUndefined(); expect(collector.collectErrors()).toEqual([ { - code: 'FAILED_TO_INSTANTIATE_EXTENSION', + code: 'EXTENSION_FACTORY_ERROR', message: "Failed to instantiate extension 'app/test', duplicate extension data 'test' received via output 'test2'", context: { node }, @@ -508,7 +508,7 @@ describe('instantiateAppNodeTree', () => { ).toBeUndefined(); expect(collector.collectErrors()).toEqual([ { - code: 'FAILED_TO_INSTANTIATE_EXTENSION', + code: 'EXTENSION_FACTORY_ERROR', message: "Failed to instantiate extension 'app/test', unknown output provided via 'nonexistent'", context: { node }, @@ -544,7 +544,7 @@ describe('instantiateAppNodeTree', () => { ).toBeUndefined(); expect(collector.collectErrors()).toEqual([ { - code: 'FAILED_TO_INSTANTIATE_EXTENSION', + code: 'EXTENSION_FACTORY_ERROR', message: "Failed to instantiate extension 'app/test', input 'singleton' is required but was not received", context: { node }, @@ -669,7 +669,7 @@ describe('instantiateAppNodeTree', () => { ).toBeUndefined(); expect(collector.collectErrors()).toEqual([ { - code: 'FAILED_TO_INSTANTIATE_EXTENSION', + code: 'EXTENSION_FACTORY_ERROR', message: "Failed to instantiate extension 'app/test', expected exactly one 'singleton' input but received multiple: 'app/test', 'app/test'", context: { node }, @@ -713,7 +713,7 @@ describe('instantiateAppNodeTree', () => { ).toBeUndefined(); expect(collector.collectErrors()).toEqual([ { - code: 'FAILED_TO_INSTANTIATE_EXTENSION', + code: 'EXTENSION_FACTORY_ERROR', message: "Failed to instantiate extension 'app/test', expected at most one 'singleton' input but received multiple: 'app/test', 'app/test'", context: { node }, @@ -751,7 +751,7 @@ describe('instantiateAppNodeTree', () => { ).toBeUndefined(); expect(collector.collectErrors()).toEqual([ { - code: 'FAILED_TO_INSTANTIATE_EXTENSION', + code: 'EXTENSION_FACTORY_ERROR', message: "Failed to instantiate extension 'app/test', extension 'app/test' could not be attached because its output data ('test', 'other') does not match what the input 'singleton' requires ('other')", context: { node }, @@ -1073,7 +1073,7 @@ describe('instantiateAppNodeTree', () => { ).toBeUndefined(); expect(collector.collectErrors()).toEqual([ { - code: 'INVALID_CONFIGURATION', + code: 'EXTENSION_CONFIGURATION_INVALID', message: "Invalid configuration for extension 'app/test'; caused by Error: Expected number, received string at 'other'", context: { node }, @@ -1121,7 +1121,7 @@ describe('instantiateAppNodeTree', () => { ), ).toEqual([ { - code: 'EXTENSION_FACTORY_INVALID_OUTPUT', + code: 'EXTENSION_INVALID', message: 'extension factory did not provide an iterable object', context: { node: expect.anything() }, }, @@ -1138,8 +1138,8 @@ describe('instantiateAppNodeTree', () => { ), ).toEqual([ { - // TODO: This should probably be EXTENSION_FACTORY_INVALID_OUTPUT - code: 'FAILED_TO_INSTANTIATE_EXTENSION', + // TODO: This should probably be EXTENSION_INVALID + code: 'EXTENSION_FACTORY_ERROR', message: "Failed to instantiate extension 'test', extension factory override did not provide an iterable object", context: { node: expect.anything() }, @@ -1157,8 +1157,8 @@ describe('instantiateAppNodeTree', () => { ), ).toEqual([ { - // TODO: This should probably be EXTENSION_FACTORY_INVALID_OUTPUT - code: 'FAILED_TO_INSTANTIATE_EXTENSION', + // TODO: This should probably be EXTENSION_INVALID + code: 'EXTENSION_FACTORY_ERROR', message: "Failed to instantiate extension 'test', extension factory middleware did not provide an iterable object", context: { node: expect.anything() }, @@ -1175,7 +1175,7 @@ describe('instantiateAppNodeTree', () => { ), ).toEqual([ { - code: 'EXTENSION_FACTORY_INVALID_OUTPUT', + code: 'EXTENSION_INVALID', message: 'extension factory did not provide an iterable object', context: { node: expect.anything() }, }, @@ -1194,7 +1194,7 @@ describe('instantiateAppNodeTree', () => { ), ).toEqual([ { - code: 'EXTENSION_FACTORY_INVALID_OUTPUT', + code: 'EXTENSION_INVALID', message: 'extension factory did not provide an iterable object', context: { node: expect.anything() }, }, @@ -1214,8 +1214,8 @@ describe('instantiateAppNodeTree', () => { ), ).toEqual([ { - // TODO: This should probably be EXTENSION_FACTORY_INVALID_OUTPUT - code: 'FAILED_TO_INSTANTIATE_EXTENSION', + // TODO: This should probably be EXTENSION_INVALID + code: 'EXTENSION_FACTORY_ERROR', message: "Failed to instantiate extension 'test:test', extension factory did not provide an iterable object", context: { node: expect.anything() }, @@ -1236,8 +1236,8 @@ describe('instantiateAppNodeTree', () => { ), ).toEqual([ { - // TODO: This should probably be EXTENSION_FACTORY_INVALID_OUTPUT - code: 'FAILED_TO_INSTANTIATE_EXTENSION', + // TODO: This should probably be EXTENSION_INVALID + code: 'EXTENSION_FACTORY_ERROR', message: "Failed to instantiate extension 'test:test', original blueprint factory did not provide an iterable object", context: { node: expect.anything() }, @@ -1271,7 +1271,7 @@ describe('instantiateAppNodeTree', () => { ).toBeUndefined(); expect(collector.collectErrors()).toEqual([ { - code: 'FAILED_TO_INSTANTIATE_EXTENSION', + code: 'EXTENSION_FACTORY_ERROR', message: "Failed to instantiate extension 'app/test'; caused by NopeError: NOPE", context: { node }, @@ -1303,7 +1303,7 @@ describe('instantiateAppNodeTree', () => { ).toBeUndefined(); expect(collector.collectErrors()).toEqual([ { - code: 'EXTENSION_FACTORY_DUPLICATE_OUTPUT', + code: 'EXTENSION_OUTPUT_CONFLICT', message: "extension factory output duplicate data 'test'", context: { dataRefId: 'test', node }, }, @@ -1335,7 +1335,7 @@ describe('instantiateAppNodeTree', () => { ).toBeUndefined(); expect(collector.collectErrors()).toEqual([ { - code: 'EXTENSION_FACTORY_MISSING_REQUIRED_OUTPUT', + code: 'EXTENSION_OUTPUT_MISSING', message: "missing required extension data output 'test'", context: { dataRefId: 'test', @@ -1370,7 +1370,7 @@ describe('instantiateAppNodeTree', () => { ).toBeDefined(); expect(collector.collectErrors()).toEqual([ { - code: 'EXTENSION_FACTORY_UNEXPECTED_OUTPUT', + code: 'EXTENSION_OUTPUT_IGNORED', message: "unexpected output 'test'", context: { dataRefId: 'test', node }, }, @@ -1404,7 +1404,7 @@ describe('instantiateAppNodeTree', () => { ).toBeUndefined(); expect(collector.collectErrors()).toEqual([ { - code: 'EXTENSION_MISSING_REQUIRED_INPUT', + code: 'EXTENSION_ATTACHMENT_MISSING', message: "input 'singleton' is required but was not received", context: { inputName: 'singleton', node }, }, @@ -1529,7 +1529,7 @@ describe('instantiateAppNodeTree', () => { ).toBeUndefined(); expect(collector.collectErrors()).toEqual([ { - code: 'EXTENSION_TOO_MANY_ATTACHMENTS', + code: 'EXTENSION_ATTACHMENT_CONFLICT', message: "expected exactly one 'singleton' input but received multiple: 'app/test', 'app/test'", context: { inputName: 'singleton', node }, @@ -1573,7 +1573,7 @@ describe('instantiateAppNodeTree', () => { ).toBeUndefined(); expect(collector.collectErrors()).toEqual([ { - code: 'EXTENSION_TOO_MANY_ATTACHMENTS', + code: 'EXTENSION_ATTACHMENT_CONFLICT', message: "expected at most one 'singleton' input but received multiple: 'app/test', 'app/test'", context: { inputName: 'singleton', node }, @@ -1610,7 +1610,7 @@ describe('instantiateAppNodeTree', () => { ).toBeUndefined(); expect(collector.collectErrors()).toEqual([ { - code: 'EXTENSION_MISSING_INPUT_DATA', + code: 'EXTENSION_INPUT_DATA_MISSING', message: "extension 'app/test' could not be attached because its output data ('test') does not match what the input 'singleton' requires ('other')", context: { node, inputName: 'singleton' }, diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index 212cb19bc5..62bf64ebc7 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -102,7 +102,7 @@ function resolveInputDataContainer( mapWithFailures(extensionData, ref => { if (dataMap.has(ref.id)) { collector.report({ - code: 'EXTENSION_DUPLICATE_INPUT', + code: 'EXTENSION_INPUT_DATA_IGNORED', message: `Unexpected duplicate input data '${ref.id}'`, }); return; @@ -120,7 +120,7 @@ function resolveInputDataContainer( .join(', '); collector.report({ - code: 'EXTENSION_MISSING_INPUT_DATA', + code: 'EXTENSION_INPUT_DATA_MISSING', message: `extension '${attachment.spec.id}' could not be attached because its output data (${provided}) does not match what the input '${inputName}' requires (${expected})`, }); throw INSTANTIATION_FAILED; @@ -265,7 +265,7 @@ function resolveV2Inputs( if (attachedNodes.length > 1) { const attachedNodeIds = attachedNodes.map(e => e.spec.id).join("', '"); collector.report({ - code: 'EXTENSION_TOO_MANY_ATTACHMENTS', + code: 'EXTENSION_ATTACHMENT_CONFLICT', message: `expected ${ input.config.optional ? 'at most' : 'exactly' } one '${inputName}' input but received multiple: '${attachedNodeIds}'`, @@ -274,7 +274,7 @@ function resolveV2Inputs( } else if (attachedNodes.length === 0) { if (!input.config.optional) { collector.report({ - code: 'EXTENSION_MISSING_REQUIRED_INPUT', + code: 'EXTENSION_ATTACHMENT_MISSING', message: `input '${inputName}' is required but was not received`, }); throw INSTANTIATION_FAILED; @@ -326,7 +326,7 @@ export function createAppNodeInstance(options: { }; } catch (e) { collector.report({ - code: 'INVALID_CONFIGURATION', + code: 'EXTENSION_CONFIGURATION_INVALID', message: `Invalid configuration for extension '${id}'; caused by ${e}`, }); return undefined; @@ -393,7 +393,7 @@ export function createAppNodeInstance(options: { !outputDataValues?.[Symbol.iterator] ) { collector.report({ - code: 'EXTENSION_FACTORY_INVALID_OUTPUT', + code: 'EXTENSION_INVALID', message: 'extension factory did not provide an iterable object', }); throw INSTANTIATION_FAILED; @@ -403,7 +403,7 @@ export function createAppNodeInstance(options: { mapWithFailures(outputDataValues, value => { if (outputDataMap.has(value.id)) { collector.report({ - code: 'EXTENSION_FACTORY_DUPLICATE_OUTPUT', + code: 'EXTENSION_OUTPUT_CONFLICT', message: `extension factory output duplicate data '${value.id}'`, context: { dataRefId: value.id, @@ -421,7 +421,7 @@ export function createAppNodeInstance(options: { if (value === undefined) { if (!ref.config.optional) { collector.report({ - code: 'EXTENSION_FACTORY_MISSING_REQUIRED_OUTPUT', + code: 'EXTENSION_OUTPUT_MISSING', message: `missing required extension data output '${ref.id}'`, context: { dataRefId: ref.id, @@ -439,7 +439,7 @@ export function createAppNodeInstance(options: { for (const dataRefId of outputDataMap.keys()) { // TODO: Make this a warning collector.report({ - code: 'EXTENSION_FACTORY_UNEXPECTED_OUTPUT', + code: 'EXTENSION_OUTPUT_IGNORED', message: `unexpected output '${dataRefId}'`, context: { dataRefId: dataRefId, @@ -449,7 +449,7 @@ export function createAppNodeInstance(options: { } } else { collector.report({ - code: 'UNEXPECTED_EXTENSION_VERSION', + code: 'EXTENSION_INVALID', message: `unexpected extension version '${ (internalExtension as any).version }'`, @@ -459,7 +459,7 @@ export function createAppNodeInstance(options: { } catch (e) { if (e !== INSTANTIATION_FAILED) { collector.report({ - code: 'FAILED_TO_INSTANTIATE_EXTENSION', + code: 'EXTENSION_FACTORY_ERROR', message: `Failed to instantiate extension '${id}'${ e.name === 'Error' ? `, ${e.message}` : `; caused by ${e}` }`, diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts index 23217f697a..2eacd0ae67 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts @@ -440,7 +440,7 @@ describe('resolveAppNodeSpecs', () => { expect(collector.collectErrors()).toEqual([ { - code: 'EXTENSION_FORBIDDEN', + code: 'EXTENSION_IGNORED', message: "It is forbidden to override the 'test/forbidden' extension, attempted by the 'test' plugin", context: { @@ -473,7 +473,7 @@ describe('resolveAppNodeSpecs', () => { expect(collector.collectErrors()).toEqual([ { - code: 'EXTENSION_FORBIDDEN', + code: 'EXTENSION_IGNORED', message: "It is forbidden to override the 'forbidden' extension, attempted by the 'forbidden' plugin", context: { @@ -496,7 +496,7 @@ describe('resolveAppNodeSpecs', () => { expect(collector.collectErrors()).toEqual([ { - code: 'EXTENSION_CONFIG_FORBIDDEN', + code: 'INVALID_EXTENSION_CONFIG_KEY', message: "Configuration of the 'forbidden' extension is forbidden", context: { extensionId: 'forbidden', diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts index 23a1847b43..d3b4ad6723 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts @@ -56,7 +56,7 @@ export function resolveAppNodeSpecs(options: { ) => { if (forbidden.has(extension.id)) { collector.report({ - code: 'EXTENSION_FORBIDDEN', + code: 'EXTENSION_IGNORED', message: `It is forbidden to override the '${extension.id}' extension, attempted by the '${extension.plugin.id}' plugin`, context: { plugin: extension.plugin, @@ -153,13 +153,13 @@ export function resolveAppNodeSpecs(options: { } } - const seendExtensionIds = new Set(); + const seenExtensionIds = new Set(); const deduplicatedExtensions = configuredExtensions.filter( ({ extension, params }) => { - if (seendExtensionIds.has(extension.id)) { + if (seenExtensionIds.has(extension.id)) { collector.report({ - code: 'EXTENSION_DUPLICATED', - message: `The extension '${extension.id}' is duplicated`, + code: 'EXTENSION_IGNORED', + message: `The '${extension.id}' extension from the '${params.plugin.id}' plugin is a duplicate and will be ignored`, context: { plugin: params.plugin, extensionId: extension.id, @@ -167,7 +167,7 @@ export function resolveAppNodeSpecs(options: { }); return false; } - seendExtensionIds.add(extension.id); + seenExtensionIds.add(extension.id); return true; }, ); @@ -178,7 +178,7 @@ export function resolveAppNodeSpecs(options: { if (forbidden.has(extensionId)) { collector.report({ - code: 'EXTENSION_CONFIG_FORBIDDEN', + code: 'INVALID_EXTENSION_CONFIG_KEY', message: `Configuration of the '${extensionId}' extension is forbidden`, context: { extensionId, @@ -206,7 +206,7 @@ export function resolveAppNodeSpecs(options: { order.set(extensionId, existing); } else { collector.report({ - code: 'EXTENSION_CONFIG_UNKNOWN_EXTENSION', + code: 'INVALID_EXTENSION_CONFIG_KEY', message: `Extension ${extensionId} does not exist`, context: { extensionId, diff --git a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts index 79bdf81126..bffc156509 100644 --- a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts @@ -238,7 +238,7 @@ describe('buildAppTree', () => { `); }); - it('emits an error when duplicated extensions are detected', () => { + it('ignores duplicate extensions', () => { const tree = resolveAppTree( 'a', [ @@ -248,13 +248,6 @@ describe('buildAppTree', () => { collector, ); expect(Array.from(tree.nodes.keys())).toEqual(['a']); - expect(collector.collectErrors()).toEqual([ - { - code: 'DUPLICATE_EXTENSION_ID', - message: "Unexpected duplicate extension id 'a'", - context: { spec: { ...baseSpec, id: 'a' } }, - }, - ]); }); describe('redirects', () => { @@ -300,11 +293,13 @@ describe('buildAppTree', () => { expect(collector.collectErrors()).toEqual([ { - code: 'DUPLICATE_REDIRECT_TARGET', + code: 'EXTENSION_INPUT_REDIRECT_CONFLICT', message: "Duplicate redirect target for input 'test' in extension 'b'", context: { - spec: { ...baseSpec, id: 'b', extension: e2 }, + node: expect.objectContaining({ + spec: { ...baseSpec, id: 'b', extension: e2 }, + }), inputName: 'test', }, }, diff --git a/packages/frontend-app-api/src/tree/resolveAppTree.ts b/packages/frontend-app-api/src/tree/resolveAppTree.ts index fc2c8f845e..f2f31f8950 100644 --- a/packages/frontend-app-api/src/tree/resolveAppTree.ts +++ b/packages/frontend-app-api/src/tree/resolveAppTree.ts @@ -122,15 +122,8 @@ export function resolveAppTree( const redirectTargetsByKey = new Map(); for (const spec of specs) { - // The main check with a more helpful error message happens in resolveAppNodeSpecs + // The main check with a helpful error message happens in resolveAppNodeSpecs if (nodes.has(spec.id)) { - errorCollector.report({ - code: 'DUPLICATE_EXTENSION_ID', - message: `Unexpected duplicate extension id '${spec.id}'`, - context: { - spec, - }, - }); continue; } @@ -144,10 +137,10 @@ export function resolveAppTree( const key = makeRedirectKey(replace); if (redirectTargetsByKey.has(key)) { errorCollector.report({ - code: 'DUPLICATE_REDIRECT_TARGET', + code: 'EXTENSION_INPUT_REDIRECT_CONFLICT', message: `Duplicate redirect target for input '${inputName}' in extension '${spec.id}'`, context: { - spec, + node, inputName, }, }); diff --git a/packages/frontend-app-api/src/wiring/createErrorCollector.ts b/packages/frontend-app-api/src/wiring/createErrorCollector.ts index cc51f146ce..fd028f1f18 100644 --- a/packages/frontend-app-api/src/wiring/createErrorCollector.ts +++ b/packages/frontend-app-api/src/wiring/createErrorCollector.ts @@ -14,41 +14,32 @@ * limitations under the License. */ -import { - AppNode, - AppNodeSpec, - FrontendPlugin, -} from '@backstage/frontend-plugin-api'; +import { AppNode, FrontendPlugin } from '@backstage/frontend-plugin-api'; import { Expand } from '@backstage/types'; type AppErrorTypes = { // resolveAppNodeSpecs - EXTENSION_FORBIDDEN: 'plugin' | 'extensionId'; - EXTENSION_DUPLICATED: 'plugin' | 'extensionId'; - EXTENSION_CONFIG_FORBIDDEN: 'extensionId'; - EXTENSION_CONFIG_UNKNOWN_EXTENSION: 'extensionId'; + EXTENSION_IGNORED: 'plugin' | 'extensionId'; + INVALID_EXTENSION_CONFIG_KEY: 'extensionId'; // resolveAppTree - DUPLICATE_EXTENSION_ID: 'spec'; - DUPLICATE_REDIRECT_TARGET: 'spec' | 'inputName'; + EXTENSION_INPUT_REDIRECT_CONFLICT: 'node' | 'inputName'; // instantiateAppNodeTree - EXTENSION_DUPLICATE_INPUT: 'node' | 'inputName'; - EXTENSION_MISSING_INPUT_DATA: 'node' | 'inputName'; - EXTENSION_TOO_MANY_ATTACHMENTS: 'node' | 'inputName'; - EXTENSION_MISSING_REQUIRED_INPUT: 'node' | 'inputName'; - INVALID_CONFIGURATION: 'node'; - EXTENSION_FACTORY_INVALID_OUTPUT: 'node'; - EXTENSION_FACTORY_DUPLICATE_OUTPUT: 'node' | 'dataRefId'; - EXTENSION_FACTORY_MISSING_REQUIRED_OUTPUT: 'node' | 'dataRefId'; - EXTENSION_FACTORY_UNEXPECTED_OUTPUT: 'node' | 'dataRefId'; - UNEXPECTED_EXTENSION_VERSION: 'node'; - FAILED_TO_INSTANTIATE_EXTENSION: 'node'; + EXTENSION_INPUT_DATA_IGNORED: 'node' | 'inputName'; + EXTENSION_INPUT_DATA_MISSING: 'node' | 'inputName'; + EXTENSION_ATTACHMENT_CONFLICT: 'node' | 'inputName'; + EXTENSION_ATTACHMENT_MISSING: 'node' | 'inputName'; + EXTENSION_CONFIGURATION_INVALID: 'node'; + EXTENSION_INVALID: 'node'; + EXTENSION_OUTPUT_CONFLICT: 'node' | 'dataRefId'; + EXTENSION_OUTPUT_MISSING: 'node' | 'dataRefId'; + EXTENSION_OUTPUT_IGNORED: 'node' | 'dataRefId'; + EXTENSION_FACTORY_ERROR: 'node'; // createSpecializedApp - NO_API_FACTORY: 'node'; + API_EXTENSION_INVALID: 'node'; }; type AppErrorContext = { node?: AppNode; - spec?: AppNodeSpec; plugin?: FrontendPlugin; extensionId?: string; dataRefId?: string; diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx index bcb57ce7e7..a9b7e6864a 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx @@ -394,7 +394,7 @@ function createApiFactories(options: { factories.push(apiFactory); } else { options.collector.report({ - code: 'NO_API_FACTORY', + code: 'API_EXTENSION_INVALID', message: `API extension '${apiNode.spec.id}' did not output an API factory`, context: { node: apiNode, From 018de42a012b3d6cb150cda1c3acbc9c98779267 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 7 Sep 2025 13:07:22 +0200 Subject: [PATCH 087/233] frontend-app-api: use EXTENSION_FACTORY_ERROR for invalid return types Signed-off-by: Patrik Oldsberg --- .../src/tree/instantiateAppNodeTree.test.ts | 19 +++++++++---------- .../src/tree/instantiateAppNodeTree.ts | 6 +----- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index 4f1589d3a3..8f892afd56 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -1121,8 +1121,9 @@ describe('instantiateAppNodeTree', () => { ), ).toEqual([ { - code: 'EXTENSION_INVALID', - message: 'extension factory did not provide an iterable object', + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'test', extension factory did not provide an iterable object", context: { node: expect.anything() }, }, ]); @@ -1138,7 +1139,6 @@ describe('instantiateAppNodeTree', () => { ), ).toEqual([ { - // TODO: This should probably be EXTENSION_INVALID code: 'EXTENSION_FACTORY_ERROR', message: "Failed to instantiate extension 'test', extension factory override did not provide an iterable object", @@ -1157,7 +1157,6 @@ describe('instantiateAppNodeTree', () => { ), ).toEqual([ { - // TODO: This should probably be EXTENSION_INVALID code: 'EXTENSION_FACTORY_ERROR', message: "Failed to instantiate extension 'test', extension factory middleware did not provide an iterable object", @@ -1175,8 +1174,9 @@ describe('instantiateAppNodeTree', () => { ), ).toEqual([ { - code: 'EXTENSION_INVALID', - message: 'extension factory did not provide an iterable object', + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'test:test', extension factory did not provide an iterable object", context: { node: expect.anything() }, }, ]); @@ -1194,8 +1194,9 @@ describe('instantiateAppNodeTree', () => { ), ).toEqual([ { - code: 'EXTENSION_INVALID', - message: 'extension factory did not provide an iterable object', + code: 'EXTENSION_FACTORY_ERROR', + message: + "Failed to instantiate extension 'test:test', extension factory did not provide an iterable object", context: { node: expect.anything() }, }, ]); @@ -1214,7 +1215,6 @@ describe('instantiateAppNodeTree', () => { ), ).toEqual([ { - // TODO: This should probably be EXTENSION_INVALID code: 'EXTENSION_FACTORY_ERROR', message: "Failed to instantiate extension 'test:test', extension factory did not provide an iterable object", @@ -1236,7 +1236,6 @@ describe('instantiateAppNodeTree', () => { ), ).toEqual([ { - // TODO: This should probably be EXTENSION_INVALID code: 'EXTENSION_FACTORY_ERROR', message: "Failed to instantiate extension 'test:test', original blueprint factory did not provide an iterable object", diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index 62bf64ebc7..f8e18cdcbd 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -392,11 +392,7 @@ export function createAppNodeInstance(options: { typeof outputDataValues !== 'object' || !outputDataValues?.[Symbol.iterator] ) { - collector.report({ - code: 'EXTENSION_INVALID', - message: 'extension factory did not provide an iterable object', - }); - throw INSTANTIATION_FAILED; + throw new Error('extension factory did not provide an iterable object'); } const outputDataMap = new Map(); From 02fced51db0e4f3f8f88da0fe9eb02dbea18d1f6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 7 Sep 2025 13:41:16 +0200 Subject: [PATCH 088/233] frontend-app-api: rework app error types Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/report.api.md | 102 +++++++++++-- .../src/tree/instantiateAppNodeTree.test.ts | 2 +- .../src/tree/instantiateAppNodeTree.ts | 4 +- .../src/wiring/createErrorCollector.ts | 140 ++++++++++-------- packages/frontend-app-api/src/wiring/index.ts | 2 +- 5 files changed, 174 insertions(+), 76 deletions(-) diff --git a/packages/frontend-app-api/report.api.md b/packages/frontend-app-api/report.api.md index 1806f47b02..fca32ed38c 100644 --- a/packages/frontend-app-api/report.api.md +++ b/packages/frontend-app-api/report.api.md @@ -5,7 +5,6 @@ ```ts import { ApiHolder } from '@backstage/core-plugin-api'; import { AppNode } from '@backstage/frontend-plugin-api'; -import { AppNodeSpec } from '@backstage/frontend-plugin-api'; import { AppTree } from '@backstage/frontend-plugin-api'; import { ConfigApi } from '@backstage/core-plugin-api'; import { ExtensionFactoryMiddleware } from '@backstage/frontend-plugin-api'; @@ -18,16 +17,97 @@ import { RouteRef } from '@backstage/frontend-plugin-api'; import { SubRouteRef } from '@backstage/frontend-plugin-api'; // @public (undocumented) -export type AppError = { - code: string; - message: string; - context?: { - node?: AppNode; - spec?: AppNodeSpec; - plugin?: FrontendPlugin; - extensionId?: string; - inputName?: string; - dataRefId?: string; +export type AppError = + keyof AppErrorTypes extends infer ICode extends keyof AppErrorTypes + ? ICode extends any + ? { + code: ICode; + message: string; + context: AppErrorTypes[ICode]['context']; + } + : never + : never; + +// @public (undocumented) +export type AppErrorTypes = { + EXTENSION_IGNORED: { + context: { + plugin: FrontendPlugin; + extensionId: string; + }; + }; + INVALID_EXTENSION_CONFIG_KEY: { + context: { + extensionId: string; + }; + }; + EXTENSION_INPUT_REDIRECT_CONFLICT: { + context: { + node: AppNode; + inputName: string; + }; + }; + EXTENSION_INPUT_DATA_IGNORED: { + context: { + node: AppNode; + inputName: string; + }; + }; + EXTENSION_INPUT_DATA_MISSING: { + context: { + node: AppNode; + inputName: string; + }; + }; + EXTENSION_ATTACHMENT_CONFLICT: { + context: { + node: AppNode; + inputName: string; + }; + }; + EXTENSION_ATTACHMENT_MISSING: { + context: { + node: AppNode; + inputName: string; + }; + }; + EXTENSION_CONFIGURATION_INVALID: { + context: { + node: AppNode; + }; + }; + EXTENSION_INVALID: { + context: { + node: AppNode; + }; + }; + EXTENSION_OUTPUT_CONFLICT: { + context: { + node: AppNode; + dataRefId: string; + }; + }; + EXTENSION_OUTPUT_MISSING: { + context: { + node: AppNode; + dataRefId: string; + }; + }; + EXTENSION_OUTPUT_IGNORED: { + context: { + node: AppNode; + dataRefId: string; + }; + }; + EXTENSION_FACTORY_ERROR: { + context: { + node: AppNode; + }; + }; + API_EXTENSION_INVALID: { + context: { + node: AppNode; + }; }; }; diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index 8f892afd56..13ec6e8a59 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -1098,7 +1098,7 @@ describe('instantiateAppNodeTree', () => { }); const errors = collector.collectErrors(); for (const error of errors ?? []) { - expect(error.context?.node).toBe(node); + expect('node' in error.context && error.context.node).toBe(node); } return errors; } diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index f8e18cdcbd..39fc88c19a 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -95,7 +95,7 @@ function resolveInputDataContainer( extensionData: Array, attachment: AppNode, inputName: string, - collector: ErrorCollector<'node' | 'inputName'>, + collector: ErrorCollector<{ node: AppNode; inputName: string }>, ): { node: AppNode } & ExtensionDataContainer { const dataMap = new Map(); @@ -250,7 +250,7 @@ function resolveV2Inputs( >; }, attachments: ReadonlyMap, - parentCollector: ErrorCollector<'node'>, + parentCollector: ErrorCollector<{ node: AppNode }>, ): ResolvedExtensionInputs<{ [inputName in string]: ExtensionInput< ExtensionDataRef, diff --git a/packages/frontend-app-api/src/wiring/createErrorCollector.ts b/packages/frontend-app-api/src/wiring/createErrorCollector.ts index fd028f1f18..633febd945 100644 --- a/packages/frontend-app-api/src/wiring/createErrorCollector.ts +++ b/packages/frontend-app-api/src/wiring/createErrorCollector.ts @@ -15,63 +15,81 @@ */ import { AppNode, FrontendPlugin } from '@backstage/frontend-plugin-api'; -import { Expand } from '@backstage/types'; -type AppErrorTypes = { +/** + * @public + */ +export type AppErrorTypes = { // resolveAppNodeSpecs - EXTENSION_IGNORED: 'plugin' | 'extensionId'; - INVALID_EXTENSION_CONFIG_KEY: 'extensionId'; - // resolveAppTree - EXTENSION_INPUT_REDIRECT_CONFLICT: 'node' | 'inputName'; - // instantiateAppNodeTree - EXTENSION_INPUT_DATA_IGNORED: 'node' | 'inputName'; - EXTENSION_INPUT_DATA_MISSING: 'node' | 'inputName'; - EXTENSION_ATTACHMENT_CONFLICT: 'node' | 'inputName'; - EXTENSION_ATTACHMENT_MISSING: 'node' | 'inputName'; - EXTENSION_CONFIGURATION_INVALID: 'node'; - EXTENSION_INVALID: 'node'; - EXTENSION_OUTPUT_CONFLICT: 'node' | 'dataRefId'; - EXTENSION_OUTPUT_MISSING: 'node' | 'dataRefId'; - EXTENSION_OUTPUT_IGNORED: 'node' | 'dataRefId'; - EXTENSION_FACTORY_ERROR: 'node'; - // createSpecializedApp - API_EXTENSION_INVALID: 'node'; -}; - -type AppErrorContext = { - node?: AppNode; - plugin?: FrontendPlugin; - extensionId?: string; - dataRefId?: string; - inputName?: string; -}; - -/** @public */ -export type AppError = - { - code: TCode; - message: string; - context: Expand< - AppErrorContext & - Pick< - Required, - keyof AppErrorTypes extends TCode ? never : AppErrorTypes[TCode] - > - >; + EXTENSION_IGNORED: { + context: { plugin: FrontendPlugin; extensionId: string }; }; + INVALID_EXTENSION_CONFIG_KEY: { + context: { extensionId: string }; + }; + // resolveAppTree + EXTENSION_INPUT_REDIRECT_CONFLICT: { + context: { node: AppNode; inputName: string }; + }; + // instantiateAppNodeTree + EXTENSION_INPUT_DATA_IGNORED: { + context: { node: AppNode; inputName: string }; + }; + EXTENSION_INPUT_DATA_MISSING: { + context: { node: AppNode; inputName: string }; + }; + EXTENSION_ATTACHMENT_CONFLICT: { + context: { node: AppNode; inputName: string }; + }; + EXTENSION_ATTACHMENT_MISSING: { + context: { node: AppNode; inputName: string }; + }; + EXTENSION_CONFIGURATION_INVALID: { + context: { node: AppNode }; + }; + EXTENSION_INVALID: { + context: { node: AppNode }; + }; + EXTENSION_OUTPUT_CONFLICT: { + context: { node: AppNode; dataRefId: string }; + }; + EXTENSION_OUTPUT_MISSING: { + context: { node: AppNode; dataRefId: string }; + }; + EXTENSION_OUTPUT_IGNORED: { + context: { node: AppNode; dataRefId: string }; + }; + EXTENSION_FACTORY_ERROR: { + context: { node: AppNode }; + }; + // createSpecializedApp + API_EXTENSION_INVALID: { + context: { node: AppNode }; + }; +}; + +/** + * @public + */ +export type AppError = + keyof AppErrorTypes extends infer ICode extends keyof AppErrorTypes + ? ICode extends any + ? { + code: ICode; + message: string; + context: AppErrorTypes[ICode]['context']; + } + : never + : never; /** @internal */ -export interface ErrorCollector< - TContext extends keyof AppErrorContext = never, -> { - // Type-only: here to make sure that all required keys are present - $$contextKeys: { [K in TContext]: K }; +export interface ErrorCollector { report( - report: Exclude< - AppErrorTypes[TCode], - TContext - > extends infer IContext extends keyof AppErrorContext - ? [IContext] extends [never] + report: Omit< + AppErrorTypes[TCode]['context'], + keyof TContext + > extends infer IContext extends {} + ? [{}] extends [IContext] ? { code: TCode; message: string; @@ -79,28 +97,28 @@ export interface ErrorCollector< : { code: TCode; message: string; - context: Pick, IContext>; + context: IContext; } : never, ): void; - child( - context?: TAdditionalContext & AppErrorContext, - ): ErrorCollector< - (TContext | keyof TAdditionalContext) & keyof AppErrorContext - >; + child( + context: TAdditionalContext, + ): ErrorCollector; collectErrors(): AppError[] | undefined; } /** @internal */ export function createErrorCollector( - context?: AppError['context'], + context?: Partial, ): ErrorCollector { const errors: AppError[] = []; const children: ErrorCollector[] = []; return { - $$contextKeys: null as any, - report(report) { - errors.push({ ...report, context: { ...context, ...report.context } }); + report(report: { code: string; message: string; context?: {} }) { + errors.push({ + ...report, + context: { ...context, ...report.context }, + } as AppError); }, collectErrors() { const allErrors = [ diff --git a/packages/frontend-app-api/src/wiring/index.ts b/packages/frontend-app-api/src/wiring/index.ts index ce7574852a..1e18859ddd 100644 --- a/packages/frontend-app-api/src/wiring/index.ts +++ b/packages/frontend-app-api/src/wiring/index.ts @@ -19,4 +19,4 @@ export { type CreateSpecializedAppOptions, } from './createSpecializedApp'; export { type FrontendPluginInfoResolver } from './createPluginInfoAttacher'; -export { type AppError } from './createErrorCollector'; +export { type AppError, type AppErrorTypes } from './createErrorCollector'; From aca3cab2cc57aff6d70c47b336f71b32a005e743 Mon Sep 17 00:00:00 2001 From: JeevaRamanathan <64531160+JeevaRamanathan@users.noreply.github.com> Date: Sun, 7 Sep 2025 19:06:05 +0530 Subject: [PATCH 089/233] Improve readability in getting-started documentation Signed-off-by: JeevaRamanathan <64531160+JeevaRamanathan@users.noreply.github.com> --- docs/getting-started/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index cd86aa3343..611002bf02 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -31,7 +31,7 @@ This guide also assumes a basic understanding of working on a Linux based operat [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/) - A GNU-like build environment available at the command line. For example, on Debian/Ubuntu you will want to have the `make` and `build-essential` packages installed. - On macOS, you will want to have run `xcode-select --install` to get the XCode command line build tooling in place. + On macOS, you will want to run `xcode-select --install` to get the XCode command line build tooling in place. - An account with elevated rights to install the dependencies - `curl` or `wget` installed - Node.js [Active LTS Release](../overview/versioning-policy.md#nodejs-releases) installed using one of these @@ -58,9 +58,9 @@ The Backstage app we'll be creating will only have demo data until we set up int ::: -To install the Backstage Standalone app, we will make use of `npx`. `npx` is a tool that comes preinstalled with Node.js and lets you run commands straight from `npm` or other registries. Before we jump in to running the command, let's chat about what it does. +To install the Backstage Standalone app, we will make use of `npx`. `npx` is a tool that comes preinstalled with Node.js and lets you run commands straight from `npm` or other registries. Before we run the command, let's discuss what it does. -This command will create a new directory with a Backstage app inside. The wizard will ask you for the name of the app. This name will be created as sub directory in your current working directory. +This command will create a new directory with a Backstage app inside. The wizard will ask you for the name of the app. This name will be created as subdirectory in your current working directory. ![create app](../assets/getting-started/create-app-output.png) @@ -90,7 +90,7 @@ app don't add any npm dependencies here as they probably should be installed in the intended workspace rather than in the root._ - **packages/**: Lerna leaf packages or "workspaces". Everything here is going - to be a separate package, managed by lerna. + to be a separate package managed by Lerna. - **packages/app/**: A fully functioning Backstage frontend app that acts as a good starting point for you to get to know Backstage. - **packages/backend/**: We include a backend that helps power features such as From b24de19bb34d39556ba263b246a59e066779e487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Sun, 7 Sep 2025 21:45:16 +0200 Subject: [PATCH 090/233] Adapted test to new multi-root handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- .../useEntityRelationNodesAndEdges.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.tsx index b896fe1ed3..ad70111fa0 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.test.tsx @@ -593,10 +593,10 @@ describe('useEntityRelationNodesAndEdges', () => { expect(edges).toEqual([ { distance: 1, - from: 'b:d/c2', + from: 'b:d/c1', label: 'visible', - relations: ['partOf', 'hasPart'], - to: 'b:d/c1', + relations: ['hasPart', 'partOf'], + to: 'b:d/c2', }, { distance: 1, From 5798d2566f2d6898863568793de556cbc58c3e2d Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Mon, 8 Sep 2025 08:47:51 +0300 Subject: [PATCH 091/233] chore: change to BACKSTAGE_ENV as per review Signed-off-by: Hellgren Heikki --- .changeset/better-eagles-tickle.md | 2 +- docs/conf/index.md | 8 ++++---- .../config-loader/src/sources/ConfigSources.test.ts | 2 +- packages/config-loader/src/sources/ConfigSources.ts | 11 ++++------- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/.changeset/better-eagles-tickle.md b/.changeset/better-eagles-tickle.md index b199ae21d7..418f0bcae0 100644 --- a/.changeset/better-eagles-tickle.md +++ b/.changeset/better-eagles-tickle.md @@ -2,4 +2,4 @@ '@backstage/config-loader': patch --- -Allow using `BACKSTAGE_ENVIRONMENT` for loading environment specific config files +Allow using `BACKSTAGE_ENV` for loading environment specific config files diff --git a/docs/conf/index.md b/docs/conf/index.md index 0b2240e2de..029c5b88fd 100644 --- a/docs/conf/index.md +++ b/docs/conf/index.md @@ -17,15 +17,15 @@ allowing for customization. Configuration is stored in YAML files where the defaults are `app-config.yaml` and `app-config.local.yaml` for local overrides. Additionally, it is possible -to define environment based configuration files with `BACKSTAGE_ENVIRONMENT` -environment variable, which will load `app-config..yaml`. +to define environment based configuration files with `BACKSTAGE_ENV` +environment variable, which will load `app-config..yaml`. Loading order of these files is as follows: 1. `app-config.yaml` -2. `app-config..yaml` +2. `app-config..yaml` 3. `app-config.local.yaml` -4. `app-config..local.yaml` +4. `app-config..local.yaml` Other sets of files can by loaded by passing `--config ` flags. Read more about the configuration loading order in the diff --git a/packages/config-loader/src/sources/ConfigSources.test.ts b/packages/config-loader/src/sources/ConfigSources.test.ts index da56f5ccf9..a6b0648889 100644 --- a/packages/config-loader/src/sources/ConfigSources.test.ts +++ b/packages/config-loader/src/sources/ConfigSources.test.ts @@ -90,7 +90,7 @@ describe('ConfigSources', () => { { name: 'FileConfigSource', path: `${root}app-config.local.yaml` }, ]); - process.env = Object.assign(process.env, { BACKSTAGE_ENVIRONMENT: 'test' }); + process.env = Object.assign(process.env, { BACKSTAGE_ENV: 'test' }); expect( mergeSources( ConfigSources.defaultForTargets({ rootDir: '/', targets: [] }), diff --git a/packages/config-loader/src/sources/ConfigSources.ts b/packages/config-loader/src/sources/ConfigSources.ts index 1dacdc4752..18d0443986 100644 --- a/packages/config-loader/src/sources/ConfigSources.ts +++ b/packages/config-loader/src/sources/ConfigSources.ts @@ -184,11 +184,11 @@ export class ConfigSources { const localPath = resolvePath(rootDir, 'app-config.local.yaml'); const envPath = resolvePath( rootDir, - `app-config.${process.env.BACKSTAGE_ENVIRONMENT}.yaml`, + `app-config.${process.env.BACKSTAGE_ENV}.yaml`, ); const envLocalPath = resolvePath( rootDir, - `app-config.${process.env.BACKSTAGE_ENVIRONMENT}.local.yaml`, + `app-config.${process.env.BACKSTAGE_ENV}.local.yaml`, ); const alwaysIncludeDefaultConfigSource = !options.allowMissingDefaultConfig; @@ -203,7 +203,7 @@ export class ConfigSources { ); } - if (process.env.BACKSTAGE_ENVIRONMENT && fs.pathExistsSync(envPath)) { + if (process.env.BACKSTAGE_ENV && fs.pathExistsSync(envPath)) { argSources.push( FileConfigSource.create({ watch: options.watch, @@ -223,10 +223,7 @@ export class ConfigSources { ); } - if ( - process.env.BACKSTAGE_ENVIRONMENT && - fs.pathExistsSync(envLocalPath) - ) { + if (process.env.BACKSTAGE_ENV && fs.pathExistsSync(envLocalPath)) { argSources.push( FileConfigSource.create({ watch: options.watch, From 6c4904102aeb853283cb2de829e84114f4a620af Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Wed, 2 Jul 2025 13:27:00 +0200 Subject: [PATCH 092/233] chore: add migrations file Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- ...20250701120000_oidc_client_registration.js | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js diff --git a/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js b/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js new file mode 100644 index 0000000000..9f40831ae4 --- /dev/null +++ b/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js @@ -0,0 +1,174 @@ +/* + * 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. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + await knex.schema.createTable('oidc_clients', table => { + table.comment( + 'OIDC clients that are registered via dynamic client registration', + ); + + table + .string('client_id') + .primary() + .notNullable() + .comment('The unique client ID of the client'); + + table + .string('client_secret') + .notNullable() + .comment('The client secret of the client'); + + table + .string('client_name') + .notNullable() + .comment('The name of the client, should be human readable'); + + table + .timestamp('created_at', { useTz: false, precision: 0 }) + .notNullable() + .defaultTo(knex.fn.now()) + .comment('Client registration timestamp'); + + table + .timestamp('expires_at', { useTz: false, precision: 0 }) + .nullable() + .comment('Client registration expiration timestamp'); + + table + .text('response_types', 'longtext') + .notNullable() + .comment('JSON array of supported response types'); + + table + .text('grant_types', 'longtext') + .notNullable() + .comment('JSON array of supported grant types'); + + table + .text('scope') + .nullable() + .comment('Space-separated list of allowed scopes'); + + table + .text('metadata', 'longtext') + .nullable() + .comment('Additional client metadata as JSON'); + }); + + await knex.schema.createTable('oidc_authorization_codes', table => { + table.comment('Authorization codes for OIDC authorization code flow'); + + table.string('code').primary().notNullable().comment('Authorization code'); + + table + .string('client_id') + .notNullable() + .comment('Client ID that requested the code'); + + table + .string('user_entity_ref') + .notNullable() + .comment('User entity reference who authorized'); + + table + .text('redirect_uri') + .notNullable() + .comment('Redirect URI used in authorization request'); + + table.text('scope').nullable().comment('Requested scopes'); + + table.string('code_challenge').nullable().comment('PKCE code challenge'); + + table + .string('code_challenge_method') + .nullable() + .comment('PKCE code challenge method'); + + table.string('nonce').nullable().comment('Nonce value for ID token'); + + table + .timestamp('created_at', { useTz: false, precision: 0 }) + .notNullable() + .defaultTo(knex.fn.now()) + .comment('Code creation timestamp'); + + table + .timestamp('expires_at', { useTz: false, precision: 0 }) + .notNullable() + .comment('Code expiration timestamp'); + + table + .boolean('used') + .defaultTo(false) + .comment('Whether the code has been used'); + + table.foreign('client_id').references('client_id').inTable('oidc_clients'); + }); + + await knex.schema.createTable('oidc_access_tokens', table => { + table.comment('Access tokens issued by OIDC server'); + + table + .string('token_id') + .primary() + .notNullable() + .comment('Unique token identifier'); + + table + .string('client_id') + .notNullable() + .comment('Client ID that owns the token'); + + table + .string('user_entity_ref') + .notNullable() + .comment('User entity reference'); + + table.text('scope').nullable().comment('Token scopes'); + + table + .timestamp('created_at', { useTz: false, precision: 0 }) + .notNullable() + .defaultTo(knex.fn.now()) + .comment('Token creation timestamp'); + + table + .timestamp('expires_at', { useTz: false, precision: 0 }) + .notNullable() + .comment('Token expiration timestamp'); + + table + .boolean('revoked') + .defaultTo(false) + .comment('Whether the token has been revoked'); + + table.foreign('client_id').references('client_id').inTable('oidc_clients'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.dropTable('oidc_access_tokens'); + await knex.schema.dropTable('oidc_authorization_codes'); + await knex.schema.dropTable('oidc_clients'); +}; From 64dc5463ba2671faaae38d8f495bf772981b0cb2 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Wed, 2 Jul 2025 17:45:45 +0200 Subject: [PATCH 093/233] feat: started to add some tests for the oidc database Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- ...20250701120000_oidc_client_registration.js | 5 + .../src/database/OidcDatabase.test.ts | 199 +++++++++++++++ .../auth-backend/src/database/OidcDatabase.ts | 229 ++++++++++++++++++ 3 files changed, 433 insertions(+) create mode 100644 plugins/auth-backend/src/database/OidcDatabase.test.ts create mode 100644 plugins/auth-backend/src/database/OidcDatabase.ts diff --git a/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js b/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js index 9f40831ae4..d4863965de 100644 --- a/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js +++ b/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js @@ -41,6 +41,11 @@ exports.up = async function up(knex) { .notNullable() .comment('The name of the client, should be human readable'); + table + .text('redirect_uris', 'longtext') + .notNullable() + .comment('JSON array of valid redirect URIs'); + table .timestamp('created_at', { useTz: false, precision: 0 }) .notNullable() diff --git a/plugins/auth-backend/src/database/OidcDatabase.test.ts b/plugins/auth-backend/src/database/OidcDatabase.test.ts new file mode 100644 index 0000000000..bf00159b49 --- /dev/null +++ b/plugins/auth-backend/src/database/OidcDatabase.test.ts @@ -0,0 +1,199 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { AuthDatabase } from './AuthDatabase'; +import { OidcDatabase } from './OidcDatabase'; +import { resolvePackagePath } from '@backstage/backend-plugin-api'; + +describe('Oidc Database', () => { + const databases = TestDatabases.create(); + + async function createOidcDatabase(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + + await knex.migrate.latest({ + directory: resolvePackagePath( + '@backstage/plugin-auth-backend', + 'migrations', + ), + }); + + return { + oidc: await OidcDatabase.create({ + database: AuthDatabase.create({ + getClient: async () => knex, + }), + }), + }; + } + + describe.each(databases.eachSupportedId())('%p', databaseId => { + describe('Client', () => { + it('should create and return a client', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + await expect( + oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }), + ).resolves.toEqual({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: undefined, + expiresAt: undefined, + metadata: undefined, + createdAt: expect.any(String), + }); + }); + + it('should return the client thats created in a list', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + await expect( + oidc.getClient({ clientId: 'test-client' }), + ).resolves.toEqual(client); + }); + + it('should return null if the client does not exist', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + await expect( + oidc.getClient({ clientId: 'test-client' }), + ).resolves.toBeNull(); + }); + }); + + describe('Authorization Code', () => { + it('should create and return an authorization code', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const mockClient = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const authorizationCode = await oidc.createAuthorizationCode({ + code: 'test-code', + clientId: mockClient.clientId, + userEntityRef: 'user:default/blam', + redirectUri: 'https://example.com/callback', + scope: undefined, + codeChallenge: 'test-challenge', + codeChallengeMethod: 'S256', + nonce: 'test-nonce', + expiresAt: '2025-01-01', + }); + + await expect( + oidc.getAuthorizationCode({ code: 'test-code' }), + ).resolves.toEqual(authorizationCode); + }); + + it('should return null if the authorization code does not exist', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + await expect( + oidc.getAuthorizationCode({ code: 'test-code' }), + ).resolves.toBeNull(); + }); + + it('should return the authorization code when created', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const mockClient = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const authorizationCode = await oidc.createAuthorizationCode({ + code: 'test-code', + clientId: mockClient.clientId, + userEntityRef: 'user:default/blam', + redirectUri: 'https://example.com/callback', + scope: undefined, + codeChallenge: 'test-challenge', + codeChallengeMethod: 'S256', + nonce: 'test-nonce', + expiresAt: '2025-01-01', + }); + + await expect( + oidc.getAuthorizationCode({ code: 'test-code' }), + ).resolves.toEqual(authorizationCode); + }); + + it('should allow updating the authorization code', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const mockClient = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const authorizationCode = await oidc.createAuthorizationCode({ + code: 'test-code', + clientId: mockClient.clientId, + userEntityRef: 'user:default/blam', + redirectUri: 'https://example.com/callback', + codeChallenge: 'test-challenge', + codeChallengeMethod: 'S256', + nonce: 'test-nonce', + expiresAt: '2025-01-01', + }); + + await expect( + oidc.updateAuthorizationCode({ + code: 'test-code', + used: true, + }), + ).resolves.toEqual({ + ...authorizationCode, + used: true, + }); + }); + }); + }); +}); diff --git a/plugins/auth-backend/src/database/OidcDatabase.ts b/plugins/auth-backend/src/database/OidcDatabase.ts new file mode 100644 index 0000000000..ae3cfc42a6 --- /dev/null +++ b/plugins/auth-backend/src/database/OidcDatabase.ts @@ -0,0 +1,229 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Knex } from 'knex'; +import { AuthDatabase } from './AuthDatabase'; + +import { DateTime } from 'luxon'; + +type OidcClientRow = { + client_id: string; + client_secret: string; + client_name: string; + created_at: string; + expires_at: string | null; + response_types: string; + grant_types: string; + redirect_uris: string; + scope: string | null; + metadata: string | null; +}; + +type OidcAuthorizationCodeRow = { + code: string; + client_id: string; + user_entity_ref: string; + redirect_uri: string; + scope: string | null; + code_challenge: string | null; + code_challenge_method: string | null; + nonce: string | null; + created_at: string; + expires_at: string; + used?: boolean; +}; + +type Client = { + clientId: string; + clientName: string; + clientSecret: string; + redirectUris: string[]; + responseTypes: string[]; + grantTypes: string[]; + scope?: string; + expiresAt?: string; + metadata?: Record; + createdAt: string; +}; + +type AuthorizationCode = { + code: string; + clientId: string; + userEntityRef: string; + redirectUri: string; + scope?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + nonce?: string; + createdAt: string; + expiresAt: string; + used: boolean; +}; + +export class OidcDatabase { + private constructor(private readonly db: Knex) {} + + static async create(options: { database: AuthDatabase }) { + const client = await options.database.get(); + return new OidcDatabase(client); + } + + async createClient(client: Omit) { + const now = DateTime.now().toString(); + + await this.db('oidc_clients').insert({ + client_id: client.clientId, + client_secret: client.clientSecret, + client_name: client.clientName, + created_at: now, + expires_at: client.expiresAt, + response_types: JSON.stringify(client.responseTypes), + grant_types: JSON.stringify(client.grantTypes), + redirect_uris: JSON.stringify(client.redirectUris), + scope: client.scope, + metadata: JSON.stringify(client.metadata), + }); + + return { + ...client, + createdAt: now, + }; + } + + async getClient({ clientId }: { clientId: string }) { + const client = await this.db('oidc_clients') + .where('client_id', clientId) + .first(); + + if (!client) { + return null; + } + + return this.rowToClient(client) as Client; + } + + async createAuthorizationCode( + authorizationCode: Omit, + ) { + const now = DateTime.now().toString(); + + await this.db('oidc_authorization_codes').insert({ + code: authorizationCode.code, + client_id: authorizationCode.clientId, + user_entity_ref: authorizationCode.userEntityRef, + redirect_uri: authorizationCode.redirectUri, + scope: authorizationCode.scope, + code_challenge: authorizationCode.codeChallenge, + code_challenge_method: authorizationCode.codeChallengeMethod, + nonce: authorizationCode.nonce, + expires_at: authorizationCode.expiresAt, + created_at: now, + used: false, + }); + + return { + ...authorizationCode, + createdAt: now, + used: false, + }; + } + + async getAuthorizationCode({ code }: { code: string }) { + const authorizationCode = await this.db( + 'oidc_authorization_codes', + ) + .where('code', code) + .first(); + + if (!authorizationCode) { + return null; + } + + return this.rowToAuthorizationCode(authorizationCode) as AuthorizationCode; + } + + async updateAuthorizationCode( + authorizationCode: Partial & { code: string }, + ) { + const row = this.authorizationCodeToRow(authorizationCode); + const updatedFields = Object.fromEntries( + Object.entries(row).filter(([_, value]) => value !== undefined), + ); + console.log(updatedFields); + const updated = await this.db( + 'oidc_authorization_codes', + ) + .where('code', authorizationCode.code) + .update(updatedFields) + .returning('*'); + + return this.rowToAuthorizationCode(updated[0]) as AuthorizationCode; + } + + private rowToClient(row: Partial): Partial { + return { + clientId: row.client_id, + clientName: row.client_name, + clientSecret: row.client_secret, + redirectUris: row.redirect_uris + ? JSON.parse(row.redirect_uris) + : undefined, + responseTypes: row.response_types + ? JSON.parse(row.response_types) + : undefined, + grantTypes: row.grant_types ? JSON.parse(row.grant_types) : undefined, + scope: row.scope ?? undefined, + expiresAt: row.expires_at ?? undefined, + metadata: row.metadata ? JSON.parse(row.metadata) : undefined, + createdAt: row.created_at, + }; + } + + private authorizationCodeToRow( + authorizationCode: Partial, + ): Partial { + return { + code: authorizationCode.code, + client_id: authorizationCode.clientId, + user_entity_ref: authorizationCode.userEntityRef, + redirect_uri: authorizationCode.redirectUri, + scope: authorizationCode.scope, + code_challenge: authorizationCode.codeChallenge, + code_challenge_method: authorizationCode.codeChallengeMethod, + nonce: authorizationCode.nonce, + created_at: authorizationCode.createdAt, + expires_at: authorizationCode.expiresAt, + used: authorizationCode.used, + }; + } + + private rowToAuthorizationCode( + row: Partial, + ): Partial { + return { + code: row.code, + clientId: row.client_id, + userEntityRef: row.user_entity_ref, + redirectUri: row.redirect_uri, + scope: row.scope ?? undefined, + codeChallenge: row.code_challenge ?? undefined, + codeChallengeMethod: row.code_challenge_method ?? undefined, + nonce: row.nonce ?? undefined, + createdAt: row.created_at, + expiresAt: row.expires_at, + used: Boolean(row.used), + }; + } +} From ac54ac21d315864d5ed2872a21d45aa97455d36e Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Wed, 2 Jul 2025 19:30:25 +0200 Subject: [PATCH 094/233] chore: implementing access token management Signed-off-by: benjdlambert --- .../src/database/OidcDatabase.test.ts | 95 +++++++++++++++- .../auth-backend/src/database/OidcDatabase.ts | 107 +++++++++++++++++- 2 files changed, 197 insertions(+), 5 deletions(-) diff --git a/plugins/auth-backend/src/database/OidcDatabase.test.ts b/plugins/auth-backend/src/database/OidcDatabase.test.ts index bf00159b49..65f9e9824c 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.test.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.test.ts @@ -41,7 +41,7 @@ describe('Oidc Database', () => { } describe.each(databases.eachSupportedId())('%p', databaseId => { - describe('Client', () => { + describe('Clients', () => { it('should create and return a client', async () => { const { oidc } = await createOidcDatabase(databaseId); @@ -94,7 +94,7 @@ describe('Oidc Database', () => { }); }); - describe('Authorization Code', () => { + describe('Authorization Codes', () => { it('should create and return an authorization code', async () => { const { oidc } = await createOidcDatabase(databaseId); @@ -195,5 +195,96 @@ describe('Oidc Database', () => { }); }); }); + + describe('Access Tokens', () => { + it('should create and return an access token', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const mockClient = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const accessToken = await oidc.createAccessToken({ + tokenId: 'test-token', + clientId: mockClient.clientId, + userEntityRef: 'user:default/blam', + expiresAt: '2025-01-01', + revoked: false, + }); + + await expect( + oidc.getAccessToken({ tokenId: 'test-token' }), + ).resolves.toEqual(accessToken); + }); + + it('should return null if the access token does not exist', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + await expect( + oidc.getAccessToken({ tokenId: 'test-token' }), + ).resolves.toBeNull(); + }); + + it('should return the access token when created', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const mockClient = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const accessToken = await oidc.createAccessToken({ + tokenId: 'test-token', + clientId: mockClient.clientId, + userEntityRef: 'user:default/blam', + expiresAt: '2025-01-01', + revoked: false, + }); + + await expect( + oidc.getAccessToken({ tokenId: 'test-token' }), + ).resolves.toEqual(accessToken); + }); + + it('should allow updating the access token', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const mockClient = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const accessToken = await oidc.createAccessToken({ + tokenId: 'test-token', + clientId: mockClient.clientId, + userEntityRef: 'user:default/blam', + expiresAt: '2025-01-01', + revoked: false, + }); + + await expect( + oidc.updateAccessToken({ + tokenId: 'test-token', + revoked: true, + }), + ).resolves.toEqual({ + ...accessToken, + revoked: true, + }); + }); + }); }); }); diff --git a/plugins/auth-backend/src/database/OidcDatabase.ts b/plugins/auth-backend/src/database/OidcDatabase.ts index ae3cfc42a6..cbaf101ed9 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.ts @@ -45,6 +45,16 @@ type OidcAuthorizationCodeRow = { used?: boolean; }; +type OidcAccessTokenRow = { + token_id: string; + client_id: string; + user_entity_ref: string; + scope: string | null; + created_at: string; + expires_at: string; + revoked?: boolean; +}; + type Client = { clientId: string; clientName: string; @@ -72,6 +82,23 @@ type AuthorizationCode = { used: boolean; }; +type AccessToken = { + tokenId: string; + clientId: string; + userEntityRef: string; + scope?: string; + createdAt: string; + expiresAt: string; + revoked?: boolean; +}; + +/** + * This class is an implementation for the Database operations that power the OIDC sign-in flow. + * + * This class provides database operations for OpenID Connect (OIDC) authentication flows. + * It manages OIDC clients, authorization codes, and access tokens in the database, as well as the consent requests + * for the frontend plugin to accept. + */ export class OidcDatabase { private constructor(private readonly db: Knex) {} @@ -161,15 +188,61 @@ export class OidcDatabase { const updatedFields = Object.fromEntries( Object.entries(row).filter(([_, value]) => value !== undefined), ); - console.log(updatedFields); - const updated = await this.db( + + const [updated] = await this.db( 'oidc_authorization_codes', ) .where('code', authorizationCode.code) .update(updatedFields) .returning('*'); - return this.rowToAuthorizationCode(updated[0]) as AuthorizationCode; + return this.rowToAuthorizationCode(updated) as AuthorizationCode; + } + + async createAccessToken(accessToken: Omit) { + const now = DateTime.now().toString(); + + await this.db('oidc_access_tokens').insert({ + token_id: accessToken.tokenId, + client_id: accessToken.clientId, + user_entity_ref: accessToken.userEntityRef, + scope: accessToken.scope, + created_at: now, + expires_at: accessToken.expiresAt, + revoked: accessToken.revoked ?? false, + }); + + return { + ...accessToken, + createdAt: now, + }; + } + + async getAccessToken({ tokenId }: { tokenId: string }) { + const accessToken = await this.db('oidc_access_tokens') + .where('token_id', tokenId) + .first(); + + if (!accessToken) { + return null; + } + + return this.rowToAccessToken(accessToken) as AccessToken; + } + + async updateAccessToken( + accessToken: Partial & { tokenId: string }, + ) { + const row = this.accessTokenToRow(accessToken); + const updatedFields = Object.fromEntries( + Object.entries(row).filter(([_, value]) => value !== undefined), + ); + const [updated] = await this.db('oidc_access_tokens') + .where('token_id', accessToken.tokenId) + .update(updatedFields) + .returning('*'); + + return this.rowToAccessToken(updated) as AccessToken; } private rowToClient(row: Partial): Partial { @@ -226,4 +299,32 @@ export class OidcDatabase { used: Boolean(row.used), }; } + + private accessTokenToRow( + accessToken: Partial, + ): Partial { + return { + token_id: accessToken.tokenId, + client_id: accessToken.clientId, + user_entity_ref: accessToken.userEntityRef, + scope: accessToken.scope, + created_at: accessToken.createdAt, + expires_at: accessToken.expiresAt, + revoked: accessToken.revoked, + }; + } + + private rowToAccessToken( + row: Partial, + ): Partial { + return { + tokenId: row.token_id, + clientId: row.client_id, + userEntityRef: row.user_entity_ref, + scope: row.scope ?? undefined, + createdAt: row.created_at, + expiresAt: row.expires_at, + revoked: Boolean(row.revoked), + }; + } } From bbda7485f6439938d5f5933f5a07fd09a922be18 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Thu, 3 Jul 2025 10:24:53 +0200 Subject: [PATCH 095/233] feat: adding client register Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- .../src/service/OidcRouter.test.ts | 2 ++ .../auth-backend/src/service/OidcRouter.ts | 28 ++++++++++++++++++- .../auth-backend/src/service/OidcService.ts | 28 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/service/OidcRouter.test.ts b/plugins/auth-backend/src/service/OidcRouter.test.ts index dbc6c88f93..8e53abbcaf 100644 --- a/plugins/auth-backend/src/service/OidcRouter.test.ts +++ b/plugins/auth-backend/src/service/OidcRouter.test.ts @@ -37,6 +37,8 @@ describe('OidcRouter', () => { }), } as unknown as UserInfoDatabase; + const mockOidc = {}; + const { server } = await startTestBackend({ features: [ createBackendPlugin({ diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index 6ada071c44..219e026a53 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -15,10 +15,12 @@ */ import Router from 'express-promise-router'; import { OidcService } from './OidcService'; -import { AuthenticationError } from '@backstage/errors'; +import { AuthenticationError, isError } from '@backstage/errors'; import { AuthService } from '@backstage/backend-plugin-api'; import { TokenIssuer } from '../identity/types'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; +import { rest } from 'lodash'; +import { OidcDatabase } from '../database/OidcDatabase'; export class OidcRouter { private constructor(private readonly oidc: OidcService) {} @@ -28,6 +30,7 @@ export class OidcRouter { tokenIssuer: TokenIssuer; baseUrl: string; userInfo: UserInfoDatabase; + oidc: OidcDatabase; }) { return new OidcRouter(OidcService.create(options)); } @@ -68,6 +71,29 @@ export class OidcRouter { res.json(userInfo); }); + router.get('/v1/register', async (req, res) => { + // todo(blam): maybe add zod types for validating input + const registrationRequest = req.body; + if (!registrationRequest.redirect_uris?.length) { + res.status(400).json({ + error: 'invalid_request', + error_description: 'redirect_uris is required', + }); + return; + } + + try { + res.json(await this.oidc.registerClient(registrationRequest)); + } catch (e) { + res.status(500).json({ + error: 'server_error', + error_description: `Failed to register client: ${ + isError(e) ? e.message : 'Unknown error' + }`, + }); + } + }); + return router; } } diff --git a/plugins/auth-backend/src/service/OidcService.ts b/plugins/auth-backend/src/service/OidcService.ts index 5024b2cc8c..919c0dac64 100644 --- a/plugins/auth-backend/src/service/OidcService.ts +++ b/plugins/auth-backend/src/service/OidcService.ts @@ -18,6 +18,8 @@ import { TokenIssuer } from '../identity/types'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; import { InputError } from '@backstage/errors'; import { decodeJwt } from 'jose'; +import crypto from 'crypto'; +import { OidcDatabase } from '../database/OidcDatabase'; export class OidcService { private constructor( @@ -25,6 +27,7 @@ export class OidcService { private readonly tokenIssuer: TokenIssuer, private readonly baseUrl: string, private readonly userInfo: UserInfoDatabase, + private readonly oidc: OidcDatabase, ) {} static create(options: { @@ -32,12 +35,14 @@ export class OidcService { tokenIssuer: TokenIssuer; baseUrl: string; userInfo: UserInfoDatabase; + oidc: OidcDatabase; }) { return new OidcService( options.auth, options.tokenIssuer, options.baseUrl, options.userInfo, + options.oidc, ); } @@ -65,6 +70,8 @@ export class OidcService { token_endpoint_auth_methods_supported: [], claims_supported: ['sub', 'ent'], grant_types_supported: [], + authorization_endpoint: `${this.baseUrl}/v1/authorize`, + registration_endpoint: `${this.baseUrl}/v1/register`, }; } @@ -89,4 +96,25 @@ export class OidcService { } return await this.userInfo.getUserInfo(userEntityRef); } + + public async registerClient(opts: { + responseTypes?: string[]; + grantTypes?: string[]; + clientName: string; + redirectUris?: string[]; + scope?: string; + }) { + const generatedClientId = crypto.randomUUID(); + const generatedClientSecret = crypto.randomUUID(); + + return await this.oidc.createClient({ + clientId: generatedClientId, + clientName: opts.clientName, + clientSecret: generatedClientSecret, + redirectUris: opts.redirectUris ?? [], + responseTypes: opts.responseTypes ?? ['code'], + grantTypes: opts.grantTypes ?? ['authorization_code'], + scope: opts.scope, + }); + } } From 628322d19bbd1284195ed6a7cac6743c0192e6cc Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Thu, 3 Jul 2025 10:46:06 +0200 Subject: [PATCH 096/233] chore: issue a token for guest entity ref Signed-off-by: benjdlambert --- .../src/service/OidcRouter.test.ts | 17 +- .../auth-backend/src/service/OidcRouter.ts | 116 +++++++++- .../auth-backend/src/service/OidcService.ts | 200 +++++++++++++++++- plugins/auth-backend/src/service/router.ts | 4 + 4 files changed, 327 insertions(+), 10 deletions(-) diff --git a/plugins/auth-backend/src/service/OidcRouter.test.ts b/plugins/auth-backend/src/service/OidcRouter.test.ts index 8e53abbcaf..ccc2426035 100644 --- a/plugins/auth-backend/src/service/OidcRouter.test.ts +++ b/plugins/auth-backend/src/service/OidcRouter.test.ts @@ -23,6 +23,7 @@ import Router from 'express-promise-router'; import request from 'supertest'; import { OidcRouter } from './OidcRouter'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; +import { OidcDatabase } from '../database/OidcDatabase'; describe('OidcRouter', () => { describe('/v1/userinfo', () => { @@ -37,7 +38,12 @@ describe('OidcRouter', () => { }), } as unknown as UserInfoDatabase; - const mockOidc = {}; + const mockOidc = { + createClient: jest.fn().mockResolvedValue({ + clientId: 'test', + clientSecret: 'test', + }), + } as unknown as OidcDatabase; const { server } = await startTestBackend({ features: [ @@ -55,6 +61,7 @@ describe('OidcRouter', () => { tokenIssuer: {} as any, baseUrl: 'http://localhost:7000', userInfo: mockUserInfo, + oidc: mockOidc, }).getRouter(), ); httpRouter.use(router); @@ -101,6 +108,13 @@ describe('OidcRouter', () => { }), } as unknown as UserInfoDatabase; + const mockOidc = { + createClient: jest.fn().mockResolvedValue({ + clientId: 'test', + clientSecret: 'test', + }), + } as unknown as OidcDatabase; + const { server } = await startTestBackend({ features: [ createBackendPlugin({ @@ -117,6 +131,7 @@ describe('OidcRouter', () => { tokenIssuer: {} as any, baseUrl: 'http://localhost:7000', userInfo: mockUserInfo, + oidc: mockOidc, }).getRouter(), ); httpRouter.use(router); diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index 219e026a53..721dedf8c3 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -19,7 +19,6 @@ import { AuthenticationError, isError } from '@backstage/errors'; import { AuthService } from '@backstage/backend-plugin-api'; import { TokenIssuer } from '../identity/types'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; -import { rest } from 'lodash'; import { OidcDatabase } from '../database/OidcDatabase'; export class OidcRouter { @@ -47,11 +46,118 @@ export class OidcRouter { res.json({ keys }); }); - router.get('/v1/token', (_req, res) => { - res.status(501).send('Not Implemented'); + router.get('/v1/authorize', async (req, res) => { + // todo(blam): maybe add zod types for validating input + const { + client_id: clientId, + redirect_uri: redirectUri, + response_type: responseType, + scope, + state, + nonce, + code_challenge: codeChallenge, + code_challenge_method: codeChallengeMethod, + } = req.query; + + if (!clientId || !redirectUri || !responseType) { + return res.status(400).json({ + error: 'invalid_request', + error_description: + 'Missing required parameters: client_id, redirect_uri, response_type', + }); + } + + try { + // For simplicity, we'll use a default user entity ref for now + // In a real implementation, this should be obtained from the authenticated user + const userEntityRef = 'user:default/guest'; + + const { redirectUrl } = await this.oidc.authorize({ + clientId: clientId as string, + redirectUri: redirectUri as string, + responseType: responseType as string, + scope: scope as string, + state: state as string, + nonce: nonce as string, + codeChallenge: codeChallenge as string, + codeChallengeMethod: codeChallengeMethod as string, + userEntityRef, + }); + + return res.redirect(redirectUrl); + } catch (error) { + const errorParams = new URLSearchParams(); + errorParams.append( + 'error', + isError(error) ? error.name : 'server_error', + ); + errorParams.append( + 'error_description', + isError(error) ? error.message : 'Unknown error', + ); + if (state) { + errorParams.append('state', state as string); + } + + const redirectUrl = new URL(redirectUri as string); + redirectUrl.search = errorParams.toString(); + return res.redirect(redirectUrl.toString()); + } }); - // This endpoint doesn't use the regular HttpAuthoidc, since the contract + router.post('/v1/token', async (req, res) => { + // todo(blam): maybe add zod types for validating input + const { + grant_type: grantType, + code, + client_id: clientId, + client_secret: clientSecret, + redirect_uri: redirectUri, + code_verifier: codeVerifier, + } = req.body; + + if (!grantType || !code || !clientId || !clientSecret || !redirectUri) { + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing required parameters', + }); + } + + try { + const result = await this.oidc.exchangeCodeForToken({ + code, + clientId, + clientSecret, + redirectUri, + codeVerifier, + grantType, + }); + + return res.json(result); + } catch (error) { + if (isError(error)) { + if (error.name === 'AuthenticationError') { + return res.status(401).json({ + error: 'invalid_client', + error_description: error.message, + }); + } + if (error.name === 'InputError') { + return res.status(400).json({ + error: 'invalid_request', + error_description: error.message, + }); + } + } + + return res.status(500).json({ + error: 'server_error', + error_description: isError(error) ? error.message : 'Unknown error', + }); + } + }); + + // This endpoint doesn't use the regular HttpAuth, since the contract // is specifically for the header to be communicated in the Authorization // header, regardless of token type router.get('/v1/userinfo', async (req, res) => { @@ -71,7 +177,7 @@ export class OidcRouter { res.json(userInfo); }); - router.get('/v1/register', async (req, res) => { + router.post('/v1/register', async (req, res) => { // todo(blam): maybe add zod types for validating input const registrationRequest = req.body; if (!registrationRequest.redirect_uris?.length) { diff --git a/plugins/auth-backend/src/service/OidcService.ts b/plugins/auth-backend/src/service/OidcService.ts index 919c0dac64..104c2290ae 100644 --- a/plugins/auth-backend/src/service/OidcService.ts +++ b/plugins/auth-backend/src/service/OidcService.ts @@ -16,10 +16,11 @@ import { AuthService } from '@backstage/backend-plugin-api'; import { TokenIssuer } from '../identity/types'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; -import { InputError } from '@backstage/errors'; +import { InputError, AuthenticationError } from '@backstage/errors'; import { decodeJwt } from 'jose'; import crypto from 'crypto'; import { OidcDatabase } from '../database/OidcDatabase'; +import { DateTime } from 'luxon'; export class OidcService { private constructor( @@ -52,7 +53,7 @@ export class OidcService { token_endpoint: `${this.baseUrl}/v1/token`, userinfo_endpoint: `${this.baseUrl}/v1/userinfo`, jwks_uri: `${this.baseUrl}/.well-known/jwks.json`, - response_types_supported: ['id_token'], + response_types_supported: ['code', 'id_token'], subject_types_supported: ['public'], id_token_signing_alg_values_supported: [ 'RS256', @@ -67,11 +68,15 @@ export class OidcService { 'EdDSA', ], scopes_supported: ['openid'], - token_endpoint_auth_methods_supported: [], + token_endpoint_auth_methods_supported: [ + 'client_secret_basic', + 'client_secret_post', + ], claims_supported: ['sub', 'ent'], - grant_types_supported: [], + grant_types_supported: ['authorization_code'], authorization_endpoint: `${this.baseUrl}/v1/authorize`, registration_endpoint: `${this.baseUrl}/v1/register`, + code_challenge_methods_supported: ['S256', 'plain'], }; } @@ -117,4 +122,191 @@ export class OidcService { scope: opts.scope, }); } + + public async authorize(opts: { + clientId: string; + redirectUri: string; + responseType: string; + scope?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + userEntityRef: string; + }) { + const { + clientId, + redirectUri, + responseType, + scope, + state, + nonce, + codeChallenge, + codeChallengeMethod, + userEntityRef, + } = opts; + + if (responseType !== 'code') { + throw new InputError('Only authorization code flow is supported'); + } + + const client = await this.oidc.getClient({ clientId }); + if (!client) { + throw new InputError('Invalid client_id'); + } + + if (!client.redirectUris.includes(redirectUri)) { + throw new InputError('Invalid redirect_uri'); + } + + if (codeChallenge) { + if ( + !codeChallengeMethod || + !['S256', 'plain'].includes(codeChallengeMethod) + ) { + throw new InputError('Invalid code_challenge_method'); + } + } + + const authorizationCode = crypto.randomBytes(32).toString('base64url'); + const expiresAt = DateTime.now().plus({ minutes: 10 }).toISO(); + + await this.oidc.createAuthorizationCode({ + code: authorizationCode, + clientId, + userEntityRef, + redirectUri, + scope, + codeChallenge, + codeChallengeMethod, + nonce, + expiresAt, + }); + + const redirectUrl = new URL(redirectUri); + redirectUrl.searchParams.append('code', authorizationCode); + if (state) { + redirectUrl.searchParams.append('state', state); + } + + return { + redirectUrl: redirectUrl.toString(), + }; + } + + public async exchangeCodeForToken(params: { + code: string; + clientId: string; + clientSecret: string; + redirectUri: string; + codeVerifier?: string; + grantType: string; + }) { + const { + code, + clientId, + clientSecret, + redirectUri, + codeVerifier, + grantType, + } = params; + + if (grantType !== 'authorization_code') { + throw new InputError('Unsupported grant type'); + } + + const client = await this.oidc.getClient({ clientId }); + if (!client) { + throw new AuthenticationError('Invalid client'); + } + + if (client.clientSecret !== clientSecret) { + throw new AuthenticationError('Invalid client credentials'); + } + + const authCode = await this.oidc.getAuthorizationCode({ code }); + if (!authCode) { + throw new AuthenticationError('Invalid authorization code'); + } + + if (DateTime.fromISO(authCode.expiresAt) < DateTime.now()) { + throw new AuthenticationError('Authorization code expired'); + } + + if (authCode.used) { + throw new AuthenticationError('Authorization code already used'); + } + + if (authCode.clientId !== clientId) { + throw new AuthenticationError('Client ID mismatch'); + } + + if (authCode.redirectUri !== redirectUri) { + throw new AuthenticationError('Redirect URI mismatch'); + } + + if (authCode.codeChallenge) { + if (!codeVerifier) { + throw new AuthenticationError('Code verifier required for PKCE'); + } + + if ( + !this.verifyPkce( + authCode.codeChallenge, + codeVerifier, + authCode.codeChallengeMethod, + ) + ) { + throw new AuthenticationError('Invalid code verifier'); + } + } + + await this.oidc.updateAuthorizationCode({ + code, + used: true, + }); + + const accessTokenId = crypto.randomUUID(); + const expiresAt = DateTime.now().plus({ hours: 1 }).toISO(); + + await this.oidc.createAccessToken({ + tokenId: accessTokenId, + clientId, + userEntityRef: authCode.userEntityRef, + scope: authCode.scope, + expiresAt, + }); + + const { token } = await this.tokenIssuer.issueToken({ + claims: { + sub: authCode.userEntityRef, + }, + }); + + return { + access_token: token, + token_type: 'Bearer', + expires_in: 3600, + id_token: token, + scope: authCode.scope || 'openid', + }; + } + + private verifyPkce( + codeChallenge: string, + codeVerifier: string, + method?: string, + ): boolean { + if (!method || method === 'plain') { + return codeChallenge === codeVerifier; + } + + if (method === 'S256') { + const hash = crypto.createHash('sha256').update(codeVerifier).digest(); + const base64urlHash = hash.toString('base64url'); + return codeChallenge === base64urlHash; + } + + return false; + } } diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 5012c90106..f81390daf8 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -40,6 +40,7 @@ import { StaticTokenIssuer } from '../identity/StaticTokenIssuer'; import { StaticKeyStore } from '../identity/StaticKeyStore'; import { bindProviderRouters, ProviderFactories } from '../providers/router'; import { OidcRouter } from './OidcRouter'; +import { OidcDatabase } from '../database/OidcDatabase'; interface RouterOptions { logger: LoggerService; @@ -147,11 +148,14 @@ export async function createRouter( userInfo, }); + const oidc = await OidcDatabase.create({ database }); + const oidcRouter = OidcRouter.create({ auth: options.auth, tokenIssuer, baseUrl: authUrl, userInfo, + oidc, }); router.use(oidcRouter.getRouter()); From 0d142d95ec0e770ca1d255d14ccfe7cfb8770e75 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Thu, 3 Jul 2025 11:16:51 +0200 Subject: [PATCH 097/233] chore: implementing the register and code exchange Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- .../auth-backend/src/database/OidcDatabase.ts | 18 +++++-- .../auth-backend/src/service/OidcRouter.ts | 49 +++++++++++++++---- plugins/auth-backend/src/service/router.ts | 1 + plugins/mcp-actions-backend/src/plugin.ts | 27 +++++++++- 4 files changed, 80 insertions(+), 15 deletions(-) diff --git a/plugins/auth-backend/src/database/OidcDatabase.ts b/plugins/auth-backend/src/database/OidcDatabase.ts index cbaf101ed9..2eeacb7ffe 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.ts @@ -93,11 +93,8 @@ type AccessToken = { }; /** - * This class is an implementation for the Database operations that power the OIDC sign-in flow. - * * This class provides database operations for OpenID Connect (OIDC) authentication flows. - * It manages OIDC clients, authorization codes, and access tokens in the database, as well as the consent requests - * for the frontend plugin to accept. + * It manages OIDC clients, authorization codes, and access tokens in the database. */ export class OidcDatabase { private constructor(private readonly db: Knex) {} @@ -109,7 +106,18 @@ export class OidcDatabase { async createClient(client: Omit) { const now = DateTime.now().toString(); - + console.log({ + client_id: client.clientId, + client_secret: client.clientSecret, + client_name: client.clientName, + created_at: now, + expires_at: client.expiresAt, + response_types: JSON.stringify(client.responseTypes), + grant_types: JSON.stringify(client.grantTypes), + redirect_uris: JSON.stringify(client.redirectUris), + scope: client.scope, + metadata: JSON.stringify(client.metadata), + }); await this.db('oidc_clients').insert({ client_id: client.clientId, client_secret: client.clientSecret, diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index 721dedf8c3..9706554ba5 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -16,27 +16,34 @@ import Router from 'express-promise-router'; import { OidcService } from './OidcService'; import { AuthenticationError, isError } from '@backstage/errors'; -import { AuthService } from '@backstage/backend-plugin-api'; +import { AuthService, LoggerService } from '@backstage/backend-plugin-api'; import { TokenIssuer } from '../identity/types'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; import { OidcDatabase } from '../database/OidcDatabase'; +import { json } from 'express'; export class OidcRouter { - private constructor(private readonly oidc: OidcService) {} + private constructor( + private readonly oidc: OidcService, + private readonly logger: LoggerService, + ) {} static create(options: { auth: AuthService; tokenIssuer: TokenIssuer; baseUrl: string; + logger: LoggerService; userInfo: UserInfoDatabase; oidc: OidcDatabase; }) { - return new OidcRouter(OidcService.create(options)); + return new OidcRouter(OidcService.create(options), options.logger); } public getRouter() { const router = Router(); + router.use(json()); + router.get('/.well-known/openid-configuration', (_req, res) => { res.json(this.oidc.getConfiguration()); }); @@ -60,6 +67,7 @@ export class OidcRouter { } = req.query; if (!clientId || !redirectUri || !responseType) { + this.logger.error(`Failed to authorize: Missing required parameters`); return res.status(400).json({ error: 'invalid_request', error_description: @@ -68,8 +76,8 @@ export class OidcRouter { } try { - // For simplicity, we'll use a default user entity ref for now - // In a real implementation, this should be obtained from the authenticated user + // use default user entity ref for now, as we need a redirect to the frontend plugin + // for the consent flow in order to issue the right token for the right user. const userEntityRef = 'user:default/guest'; const { redirectUrl } = await this.oidc.authorize({ @@ -117,6 +125,9 @@ export class OidcRouter { } = req.body; if (!grantType || !code || !clientId || !clientSecret || !redirectUri) { + this.logger.error( + `Failed to exchange code for token: Missing required parameters`, + ); return res.status(400).json({ error: 'invalid_request', error_description: 'Missing required parameters', @@ -135,6 +146,12 @@ export class OidcRouter { return res.json(result); } catch (error) { + const description = isError(error) ? error.message : 'Unknown error'; + this.logger.error( + `Failed to exchange code for token: ${description}`, + error, + ); + if (isError(error)) { if (error.name === 'AuthenticationError') { return res.status(401).json({ @@ -180,6 +197,7 @@ export class OidcRouter { router.post('/v1/register', async (req, res) => { // todo(blam): maybe add zod types for validating input const registrationRequest = req.body; + if (!registrationRequest.redirect_uris?.length) { res.status(400).json({ error: 'invalid_request', @@ -189,13 +207,26 @@ export class OidcRouter { } try { - res.json(await this.oidc.registerClient(registrationRequest)); + const client = await this.oidc.registerClient({ + clientName: registrationRequest.client_name, + redirectUris: registrationRequest.redirect_uris, + responseTypes: registrationRequest.response_types, + grantTypes: registrationRequest.grant_types, + scope: registrationRequest.scope, + }); + + res.status(201).json({ + client_id: client.clientId, + redirect_uris: client.redirectUris, + client_secret: client.clientSecret, + }); } catch (e) { + const description = isError(e) ? e.message : 'Unknown error'; + this.logger.error(`Failed to register client: ${description}`, e); + res.status(500).json({ error: 'server_error', - error_description: `Failed to register client: ${ - isError(e) ? e.message : 'Unknown error' - }`, + error_description: `Failed to register client: ${description}`, }); } }); diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index f81390daf8..5eb3450451 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -156,6 +156,7 @@ export async function createRouter( baseUrl: authUrl, userInfo, oidc, + logger, }); router.use(oidcRouter.getRouter()); diff --git a/plugins/mcp-actions-backend/src/plugin.ts b/plugins/mcp-actions-backend/src/plugin.ts index f1e89417e9..2e29847964 100644 --- a/plugins/mcp-actions-backend/src/plugin.ts +++ b/plugins/mcp-actions-backend/src/plugin.ts @@ -42,8 +42,17 @@ export const mcpPlugin = createBackendPlugin({ httpRouter: coreServices.httpRouter, actions: actionsServiceRef, registry: actionsRegistryServiceRef, + rootRouter: coreServices.rootHttpRouter, + discovery: coreServices.discovery, }, - async init({ actions, logger, httpRouter, httpAuth }) { + async init({ + actions, + logger, + httpRouter, + httpAuth, + rootRouter, + discovery, + }) { const mcpService = await McpService.create({ actions, }); @@ -66,6 +75,22 @@ export const mcpPlugin = createBackendPlugin({ router.use('/v1', streamableRouter); httpRouter.use(router); + + // todo(blam): there's probably a better way to proxy this, but it's required + // for mcp auth spec that it lives on the root of the mcp entrypoint server. + const authRouter = Router(); + authRouter.use('/', async (_, res) => { + const authBaseUrl = await discovery.getBaseUrl('auth'); + + const oidcResponse = await fetch( + `${authBaseUrl}/.well-known/openid-configuration`, + ); + + const oidcResponseJson = await oidcResponse.json(); + + res.json(oidcResponseJson); + }); + rootRouter.use('/.well-known/oauth-authorization-server', authRouter); }, }); }, From e0473b52e9bcbffec466c3fa8bf5be4d36be5d14 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Thu, 3 Jul 2025 11:28:11 +0200 Subject: [PATCH 098/233] chore: little cleanup Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- plugins/auth-backend/src/database/OidcDatabase.ts | 13 +------------ plugins/auth-backend/src/service/OidcRouter.ts | 8 +++++++- plugins/auth-backend/src/service/OidcService.ts | 8 ++++---- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/plugins/auth-backend/src/database/OidcDatabase.ts b/plugins/auth-backend/src/database/OidcDatabase.ts index 2eeacb7ffe..12554fba57 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.ts @@ -106,18 +106,7 @@ export class OidcDatabase { async createClient(client: Omit) { const now = DateTime.now().toString(); - console.log({ - client_id: client.clientId, - client_secret: client.clientSecret, - client_name: client.clientName, - created_at: now, - expires_at: client.expiresAt, - response_types: JSON.stringify(client.responseTypes), - grant_types: JSON.stringify(client.grantTypes), - redirect_uris: JSON.stringify(client.redirectUris), - scope: client.scope, - metadata: JSON.stringify(client.metadata), - }); + await this.db('oidc_clients').insert({ client_id: client.clientId, client_secret: client.clientSecret, diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index 9706554ba5..0e14b91d79 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -144,7 +144,13 @@ export class OidcRouter { grantType, }); - return res.json(result); + return res.json({ + access_token: result.accessToken, + token_type: result.tokenType, + expires_in: result.expiresIn, + id_token: result.idToken, + scope: result.scope, + }); } catch (error) { const description = isError(error) ? error.message : 'Unknown error'; this.logger.error( diff --git a/plugins/auth-backend/src/service/OidcService.ts b/plugins/auth-backend/src/service/OidcService.ts index 104c2290ae..cc60b2b432 100644 --- a/plugins/auth-backend/src/service/OidcService.ts +++ b/plugins/auth-backend/src/service/OidcService.ts @@ -284,10 +284,10 @@ export class OidcService { }); return { - access_token: token, - token_type: 'Bearer', - expires_in: 3600, - id_token: token, + accessToken: token, + tokenType: 'Bearer', + expiresIn: 3600, + idToken: token, scope: authCode.scope || 'openid', }; } From b50e18b25fb4b264ac66945ea60f760eb3963fef Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Thu, 3 Jul 2025 19:32:37 +0200 Subject: [PATCH 099/233] chore: reworking the migrations to simplify the tables structure Signed-off-by: benjdlambert --- ...20250701120000_oidc_client_registration.js | 158 +++++++++++------- 1 file changed, 97 insertions(+), 61 deletions(-) diff --git a/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js b/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js index d4863965de..bf8ee521f0 100644 --- a/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js +++ b/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { + // These tables make up the OIDC client registration flow. + // Clients are the top of the tree, that are created by the client registration flow. await knex.schema.createTable('oidc_clients', table => { table.comment( 'OIDC clients that are registered via dynamic client registration', @@ -41,36 +43,27 @@ exports.up = async function up(knex) { .notNullable() .comment('The name of the client, should be human readable'); - table - .text('redirect_uris', 'longtext') - .notNullable() - .comment('JSON array of valid redirect URIs'); - - table - .timestamp('created_at', { useTz: false, precision: 0 }) - .notNullable() - .defaultTo(knex.fn.now()) - .comment('Client registration timestamp'); - table .timestamp('expires_at', { useTz: false, precision: 0 }) .nullable() .comment('Client registration expiration timestamp'); table - .text('response_types', 'longtext') + .text('response_types') .notNullable() .comment('JSON array of supported response types'); table - .text('grant_types', 'longtext') + .text('grant_types') .notNullable() .comment('JSON array of supported grant types'); table - .text('scope') - .nullable() - .comment('Space-separated list of allowed scopes'); + .text('redirect_uris', 'longtext') + .notNullable() + .comment('Allowed redirect URIs as JSON array'); + + table.text('scope').nullable().comment('Default scopes for the client'); table .text('metadata', 'longtext') @@ -78,27 +71,29 @@ exports.up = async function up(knex) { .comment('Additional client metadata as JSON'); }); - await knex.schema.createTable('oidc_authorization_codes', table => { - table.comment('Authorization codes for OIDC authorization code flow'); - - table.string('code').primary().notNullable().comment('Authorization code'); + await knex.schema.createTable('oauth_authorization_sessions', table => { + table.comment('Core OAuth authorization sessions with shared context'); table - .string('client_id') + .string('id') + .primary() .notNullable() - .comment('Client ID that requested the code'); + .comment('Unique session identifier'); + + table.string('client_id').notNullable().comment('OIDC client identifier'); table .string('user_entity_ref') - .notNullable() - .comment('User entity reference who authorized'); + .nullable() + .comment('Backstage user entity reference'); - table - .text('redirect_uri') - .notNullable() - .comment('Redirect URI used in authorization request'); + table.text('redirect_uri').notNullable().comment('Client redirect URI'); - table.text('scope').nullable().comment('Requested scopes'); + table.text('scope').nullable().comment('Requested scopes space-separated'); + + table.string('state').nullable().comment('Client state parameter'); + + table.string('response_type').notNullable().comment('OAuth2 response type'); table.string('code_challenge').nullable().comment('PKCE code challenge'); @@ -107,65 +102,104 @@ exports.up = async function up(knex) { .nullable() .comment('PKCE code challenge method'); - table.string('nonce').nullable().comment('Nonce value for ID token'); + table.string('nonce').nullable().comment('OIDC nonce parameter'); table - .timestamp('created_at', { useTz: false, precision: 0 }) - .notNullable() - .defaultTo(knex.fn.now()) - .comment('Code creation timestamp'); + .enum('status', ['pending', 'approved', 'rejected', 'expired']) + .defaultTo('pending') + .comment('Authorization session status'); table .timestamp('expires_at', { useTz: false, precision: 0 }) .notNullable() - .comment('Code expiration timestamp'); + .comment('Session expiration timestamp'); + + table.foreign('client_id').references('client_id').inTable('oidc_clients'); + table.index(['client_id', 'user_entity_ref']); + table.index(['status', 'expires_at']); + }); + + await knex.schema.createTable('oidc_consent_requests', table => { + table.comment('User consent requests for OAuth authorization'); + + table + .string('id') + .primary() + .notNullable() + .comment('Unique consent request identifier'); + + table + .string('session_id') + .notNullable() + .comment('Authorization session identifier'); + + table + .timestamp('expires_at', { useTz: false, precision: 0 }) + .notNullable() + .comment('Consent request expiration timestamp'); + + table + .foreign('session_id') + .references('id') + .inTable('oauth_authorization_sessions') + .onDelete('CASCADE'); + }); + + await knex.schema.createTable('oidc_authorization_codes', table => { + table.comment('OAuth authorization codes for code exchange flow'); + + table + .string('code') + .primary() + .notNullable() + .comment('Unique authorization code'); + + table + .string('session_id') + .notNullable() + .comment('Authorization session identifier'); + + table + .timestamp('expires_at', { useTz: false, precision: 0 }) + .notNullable() + .comment('Authorization code expiration timestamp'); table .boolean('used') .defaultTo(false) - .comment('Whether the code has been used'); + .comment('Whether the authorization code has been used'); - table.foreign('client_id').references('client_id').inTable('oidc_clients'); + table + .foreign('session_id') + .references('id') + .inTable('oauth_authorization_sessions') + .onDelete('CASCADE'); }); await knex.schema.createTable('oidc_access_tokens', table => { - table.comment('Access tokens issued by OIDC server'); + table.comment('OAuth access tokens for API access'); table .string('token_id') .primary() .notNullable() - .comment('Unique token identifier'); + .comment('Unique access token identifier'); table - .string('client_id') + .string('session_id') .notNullable() - .comment('Client ID that owns the token'); - - table - .string('user_entity_ref') - .notNullable() - .comment('User entity reference'); - - table.text('scope').nullable().comment('Token scopes'); - - table - .timestamp('created_at', { useTz: false, precision: 0 }) - .notNullable() - .defaultTo(knex.fn.now()) - .comment('Token creation timestamp'); + .comment('Authorization session identifier'); table .timestamp('expires_at', { useTz: false, precision: 0 }) .notNullable() - .comment('Token expiration timestamp'); + .comment('Access token expiration timestamp'); table - .boolean('revoked') - .defaultTo(false) - .comment('Whether the token has been revoked'); - - table.foreign('client_id').references('client_id').inTable('oidc_clients'); + .foreign('session_id') + .references('id') + .inTable('oauth_authorization_sessions') + .onDelete('CASCADE'); }); }; @@ -175,5 +209,7 @@ exports.up = async function up(knex) { exports.down = async function down(knex) { await knex.schema.dropTable('oidc_access_tokens'); await knex.schema.dropTable('oidc_authorization_codes'); + await knex.schema.dropTable('oidc_consent_requests'); + await knex.schema.dropTable('oauth_authorization_sessions'); await knex.schema.dropTable('oidc_clients'); }; From 5b084e7223fc46df914bec322731ac5b88a45f4d Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Thu, 3 Jul 2025 19:33:15 +0200 Subject: [PATCH 100/233] chore: reworking the API for oidc-database Signed-off-by: benjdlambert --- .../src/database/OidcDatabase.test.ts | 375 ++++++++++++------ .../auth-backend/src/database/OidcDatabase.ts | 321 +++++++++------ 2 files changed, 448 insertions(+), 248 deletions(-) diff --git a/plugins/auth-backend/src/database/OidcDatabase.test.ts b/plugins/auth-backend/src/database/OidcDatabase.test.ts index 65f9e9824c..f9c2b83511 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.test.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.test.ts @@ -64,7 +64,6 @@ describe('Oidc Database', () => { scope: undefined, expiresAt: undefined, metadata: undefined, - createdAt: expect.any(String), }); }); @@ -94,11 +93,195 @@ describe('Oidc Database', () => { }); }); + describe('Authorization Sessions', () => { + it('should create and return an authorization session', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + userEntityRef: 'user:default/blam', + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + codeChallenge: 'test-challenge', + codeChallengeMethod: 'S256', + nonce: 'test-nonce', + expiresAt: '2025-01-01T00:00:00Z', + }); + + expect(session).toEqual( + expect.objectContaining({ + id: 'test-session', + clientId: client.clientId, + userEntityRef: 'user:default/blam', + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + codeChallenge: 'test-challenge', + codeChallengeMethod: 'S256', + nonce: 'test-nonce', + expiresAt: '2025-01-01T00:00:00Z', + status: 'pending', + }), + ); + }); + + it('should allow updating the authorization session', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + expiresAt: '2025-01-01T00:00:00Z', + }); + + await expect( + oidc.updateAuthorizationSession({ + id: 'test-session', + userEntityRef: 'user:default/blam', + status: 'approved', + }), + ).resolves.toEqual({ + ...session, + userEntityRef: 'user:default/blam', + status: 'approved', + }); + }); + }); + + describe('Consent Requests', () => { + it('should create and return a consent request', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + expiresAt: '2025-01-01T00:00:00Z', + }); + + const consentRequest = await oidc.createConsentRequest({ + id: 'test-consent', + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', + }); + + await expect( + oidc.getConsentRequest({ id: 'test-consent' }), + ).resolves.toEqual(consentRequest); + }); + + it('should return consent request with session data', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + expiresAt: '2025-01-01T00:00:00Z', + }); + + const consentRequest = await oidc.createConsentRequest({ + id: 'test-consent', + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', + }); + + const consentFromDb = await oidc.getConsentRequest({ + id: 'test-consent', + }); + const sessionFromDb = await oidc.getAuthorizationSession({ + id: consentFromDb!.sessionId, + }); + + expect(consentFromDb).toEqual(consentRequest); + expect(sessionFromDb).toEqual(session); + }); + + it('should delete consent request', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + expiresAt: '2025-01-01T00:00:00Z', + }); + + await oidc.createConsentRequest({ + id: 'test-consent', + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', + }); + + await oidc.deleteConsentRequest({ id: 'test-consent' }); + + await expect( + oidc.getConsentRequest({ id: 'test-consent' }), + ).resolves.toBeNull(); + }); + }); + describe('Authorization Codes', () => { it('should create and return an authorization code', async () => { const { oidc } = await createOidcDatabase(databaseId); - const mockClient = await oidc.createClient({ + const client = await oidc.createClient({ clientId: 'test-client', clientName: 'Test Client', clientSecret: 'test-secret', @@ -107,35 +290,33 @@ describe('Oidc Database', () => { grantTypes: ['authorization_code'], }); - const authorizationCode = await oidc.createAuthorizationCode({ - code: 'test-code', - clientId: mockClient.clientId, - userEntityRef: 'user:default/blam', + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, redirectUri: 'https://example.com/callback', - scope: undefined, - codeChallenge: 'test-challenge', - codeChallengeMethod: 'S256', - nonce: 'test-nonce', - expiresAt: '2025-01-01', + responseType: 'code', + expiresAt: '2025-01-01T00:00:00Z', }); - await expect( - oidc.getAuthorizationCode({ code: 'test-code' }), - ).resolves.toEqual(authorizationCode); + const authCode = await oidc.createAuthorizationCode({ + code: 'test-code', + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', + }); + + expect(authCode).toEqual( + expect.objectContaining({ + code: 'test-code', + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', + }), + ); }); - it('should return null if the authorization code does not exist', async () => { + it('should return authorization code with session data', async () => { const { oidc } = await createOidcDatabase(databaseId); - await expect( - oidc.getAuthorizationCode({ code: 'test-code' }), - ).resolves.toBeNull(); - }); - - it('should return the authorization code when created', async () => { - const { oidc } = await createOidcDatabase(databaseId); - - const mockClient = await oidc.createClient({ + const client = await oidc.createClient({ clientId: 'test-client', clientName: 'Test Client', clientSecret: 'test-secret', @@ -144,27 +325,40 @@ describe('Oidc Database', () => { grantTypes: ['authorization_code'], }); - const authorizationCode = await oidc.createAuthorizationCode({ - code: 'test-code', - clientId: mockClient.clientId, + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, userEntityRef: 'user:default/blam', redirectUri: 'https://example.com/callback', - scope: undefined, + responseType: 'code', + scope: 'openid', codeChallenge: 'test-challenge', codeChallengeMethod: 'S256', nonce: 'test-nonce', - expiresAt: '2025-01-01', + expiresAt: '2025-01-01T00:00:00Z', }); - await expect( - oidc.getAuthorizationCode({ code: 'test-code' }), - ).resolves.toEqual(authorizationCode); + const authCode = await oidc.createAuthorizationCode({ + code: 'test-code', + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', + }); + + const authCodeFromDb = await oidc.getAuthorizationCode({ + code: 'test-code', + }); + const sessionFromDb = await oidc.getAuthorizationSession({ + id: authCodeFromDb!.sessionId, + }); + + expect(authCodeFromDb).toEqual(authCode); + expect(sessionFromDb).toEqual(session); }); it('should allow updating the authorization code', async () => { const { oidc } = await createOidcDatabase(databaseId); - const mockClient = await oidc.createClient({ + const client = await oidc.createClient({ clientId: 'test-client', clientName: 'Test Client', clientSecret: 'test-secret', @@ -173,24 +367,27 @@ describe('Oidc Database', () => { grantTypes: ['authorization_code'], }); - const authorizationCode = await oidc.createAuthorizationCode({ - code: 'test-code', - clientId: mockClient.clientId, - userEntityRef: 'user:default/blam', + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, redirectUri: 'https://example.com/callback', - codeChallenge: 'test-challenge', - codeChallengeMethod: 'S256', - nonce: 'test-nonce', - expiresAt: '2025-01-01', + responseType: 'code', + expiresAt: '2025-01-01T00:00:00Z', }); - await expect( - oidc.updateAuthorizationCode({ - code: 'test-code', - used: true, - }), - ).resolves.toEqual({ - ...authorizationCode, + const authCode = await oidc.createAuthorizationCode({ + code: 'test-code', + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', + }); + + const updatedAuthCode = await oidc.updateAuthorizationCode({ + code: 'test-code', + used: true, + }); + + expect(updatedAuthCode).toEqual({ + ...authCode, used: true, }); }); @@ -200,7 +397,7 @@ describe('Oidc Database', () => { it('should create and return an access token', async () => { const { oidc } = await createOidcDatabase(databaseId); - const mockClient = await oidc.createClient({ + const client = await oidc.createClient({ clientId: 'test-client', clientName: 'Test Client', clientSecret: 'test-secret', @@ -209,81 +406,27 @@ describe('Oidc Database', () => { grantTypes: ['authorization_code'], }); - const accessToken = await oidc.createAccessToken({ - tokenId: 'test-token', - clientId: mockClient.clientId, - userEntityRef: 'user:default/blam', - expiresAt: '2025-01-01', - revoked: false, - }); - - await expect( - oidc.getAccessToken({ tokenId: 'test-token' }), - ).resolves.toEqual(accessToken); - }); - - it('should return null if the access token does not exist', async () => { - const { oidc } = await createOidcDatabase(databaseId); - - await expect( - oidc.getAccessToken({ tokenId: 'test-token' }), - ).resolves.toBeNull(); - }); - - it('should return the access token when created', async () => { - const { oidc } = await createOidcDatabase(databaseId); - - const mockClient = await oidc.createClient({ - clientId: 'test-client', - clientName: 'Test Client', - clientSecret: 'test-secret', - redirectUris: ['https://example.com/callback'], - responseTypes: ['code'], - grantTypes: ['authorization_code'], + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + expiresAt: '2025-01-01T00:00:00Z', }); const accessToken = await oidc.createAccessToken({ tokenId: 'test-token', - clientId: mockClient.clientId, - userEntityRef: 'user:default/blam', - expiresAt: '2025-01-01', - revoked: false, + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', }); - await expect( - oidc.getAccessToken({ tokenId: 'test-token' }), - ).resolves.toEqual(accessToken); - }); - - it('should allow updating the access token', async () => { - const { oidc } = await createOidcDatabase(databaseId); - - const mockClient = await oidc.createClient({ - clientId: 'test-client', - clientName: 'Test Client', - clientSecret: 'test-secret', - redirectUris: ['https://example.com/callback'], - responseTypes: ['code'], - grantTypes: ['authorization_code'], - }); - - const accessToken = await oidc.createAccessToken({ - tokenId: 'test-token', - clientId: mockClient.clientId, - userEntityRef: 'user:default/blam', - expiresAt: '2025-01-01', - revoked: false, - }); - - await expect( - oidc.updateAccessToken({ + expect(accessToken).toEqual( + expect.objectContaining({ tokenId: 'test-token', - revoked: true, + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', }), - ).resolves.toEqual({ - ...accessToken, - revoked: true, - }); + ); }); }); }); diff --git a/plugins/auth-backend/src/database/OidcDatabase.ts b/plugins/auth-backend/src/database/OidcDatabase.ts index 12554fba57..0c75290f12 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.ts @@ -16,13 +16,10 @@ import { Knex } from 'knex'; import { AuthDatabase } from './AuthDatabase'; -import { DateTime } from 'luxon'; - type OidcClientRow = { client_id: string; client_secret: string; client_name: string; - created_at: string; expires_at: string | null; response_types: string; grant_types: string; @@ -31,31 +28,41 @@ type OidcClientRow = { metadata: string | null; }; -type OidcAuthorizationCodeRow = { - code: string; +type OAuthAuthorizationSessionRow = { + id: string; client_id: string; - user_entity_ref: string; + user_entity_ref: string | null; redirect_uri: string; scope: string | null; + state: string | null; + response_type: string; code_challenge: string | null; code_challenge_method: string | null; nonce: string | null; - created_at: string; + status: 'pending' | 'approved' | 'rejected' | 'expired'; expires_at: string; - used?: boolean; +}; + +type OidcConsentRequestRow = { + id: string; + session_id: string; + expires_at: string; +}; + +type OidcAuthorizationCodeRow = { + code: string; + session_id: string; + expires_at: string; + used: boolean; }; type OidcAccessTokenRow = { token_id: string; - client_id: string; - user_entity_ref: string; - scope: string | null; - created_at: string; + session_id: string; expires_at: string; - revoked?: boolean; }; -type Client = { +export type Client = { clientId: string; clientName: string; clientSecret: string; @@ -65,31 +72,40 @@ type Client = { scope?: string; expiresAt?: string; metadata?: Record; - createdAt: string; }; -type AuthorizationCode = { - code: string; +export type AuthorizationSession = { + id: string; clientId: string; - userEntityRef: string; + userEntityRef?: string; redirectUri: string; scope?: string; + state?: string; + responseType: string; codeChallenge?: string; codeChallengeMethod?: string; nonce?: string; - createdAt: string; + status: 'pending' | 'approved' | 'rejected' | 'expired'; + expiresAt: string; +}; + +export type ConsentRequest = { + id: string; + sessionId: string; + expiresAt: string; +}; + +export type AuthorizationCode = { + code: string; + sessionId: string; expiresAt: string; used: boolean; }; -type AccessToken = { +export type AccessToken = { tokenId: string; - clientId: string; - userEntityRef: string; - scope?: string; - createdAt: string; + sessionId: string; expiresAt: string; - revoked?: boolean; }; /** @@ -104,14 +120,11 @@ export class OidcDatabase { return new OidcDatabase(client); } - async createClient(client: Omit) { - const now = DateTime.now().toString(); - + async createClient(client: Client) { await this.db('oidc_clients').insert({ client_id: client.clientId, client_secret: client.clientSecret, client_name: client.clientName, - created_at: now, expires_at: client.expiresAt, response_types: JSON.stringify(client.responseTypes), grant_types: JSON.stringify(client.grantTypes), @@ -120,10 +133,7 @@ export class OidcDatabase { metadata: JSON.stringify(client.metadata), }); - return { - ...client, - createdAt: now, - }; + return client; } async getClient({ clientId }: { clientId: string }) { @@ -138,44 +148,122 @@ export class OidcDatabase { return this.rowToClient(client) as Client; } - async createAuthorizationCode( - authorizationCode: Omit, + async createAuthorizationSession( + session: Omit, ) { - const now = DateTime.now().toString(); + await this.db( + 'oauth_authorization_sessions', + ).insert({ + id: session.id, + client_id: session.clientId, + user_entity_ref: session.userEntityRef, + redirect_uri: session.redirectUri, + scope: session.scope, + state: session.state, + response_type: session.responseType, + code_challenge: session.codeChallenge, + code_challenge_method: session.codeChallengeMethod, + nonce: session.nonce, + status: 'pending', + expires_at: session.expiresAt, + }); + return { + ...session, + status: 'pending', + }; + } + + async updateAuthorizationSession( + session: Partial & { id: string }, + ) { + const row = this.authorizationSessionToRow(session); + const updatedFields = Object.fromEntries( + Object.entries(row).filter(([_, value]) => value !== undefined), + ); + + const [updated] = await this.db( + 'oauth_authorization_sessions', + ) + .where('id', session.id) + .update(updatedFields) + .returning('*'); + + return this.rowToAuthorizationSession(updated) as AuthorizationSession; + } + + async createConsentRequest(consentRequest: ConsentRequest) { + await this.db('oidc_consent_requests').insert({ + id: consentRequest.id, + session_id: consentRequest.sessionId, + expires_at: consentRequest.expiresAt, + }); + + return consentRequest; + } + + async getConsentRequest({ id }: { id: string }) { + const consentRequest = await this.db( + 'oidc_consent_requests', + ) + .where('id', id) + .first(); + + if (!consentRequest) { + return null; + } + + return this.rowToConsentRequest(consentRequest) as ConsentRequest; + } + + async getAuthorizationSession({ id }: { id: string }) { + const session = await this.db( + 'oauth_authorization_sessions', + ) + .where('id', id) + .first(); + + if (!session) { + return null; + } + + return this.rowToAuthorizationSession(session) as AuthorizationSession; + } + + async deleteConsentRequest({ id }: { id: string }) { + await this.db('oidc_consent_requests') + .where('id', id) + .delete(); + } + + async createAuthorizationCode( + authorizationCode: Omit, + ) { await this.db('oidc_authorization_codes').insert({ code: authorizationCode.code, - client_id: authorizationCode.clientId, - user_entity_ref: authorizationCode.userEntityRef, - redirect_uri: authorizationCode.redirectUri, - scope: authorizationCode.scope, - code_challenge: authorizationCode.codeChallenge, - code_challenge_method: authorizationCode.codeChallengeMethod, - nonce: authorizationCode.nonce, + session_id: authorizationCode.sessionId, expires_at: authorizationCode.expiresAt, - created_at: now, used: false, }); return { ...authorizationCode, - createdAt: now, used: false, }; } async getAuthorizationCode({ code }: { code: string }) { - const authorizationCode = await this.db( + const authCode = await this.db( 'oidc_authorization_codes', ) .where('code', code) .first(); - if (!authorizationCode) { + if (!authCode) { return null; } - return this.rowToAuthorizationCode(authorizationCode) as AuthorizationCode; + return this.rowToAuthorizationCode(authCode) as AuthorizationCode; } async updateAuthorizationCode( @@ -196,50 +284,14 @@ export class OidcDatabase { return this.rowToAuthorizationCode(updated) as AuthorizationCode; } - async createAccessToken(accessToken: Omit) { - const now = DateTime.now().toString(); - + async createAccessToken(accessToken: AccessToken) { await this.db('oidc_access_tokens').insert({ token_id: accessToken.tokenId, - client_id: accessToken.clientId, - user_entity_ref: accessToken.userEntityRef, - scope: accessToken.scope, - created_at: now, + session_id: accessToken.sessionId, expires_at: accessToken.expiresAt, - revoked: accessToken.revoked ?? false, }); - return { - ...accessToken, - createdAt: now, - }; - } - - async getAccessToken({ tokenId }: { tokenId: string }) { - const accessToken = await this.db('oidc_access_tokens') - .where('token_id', tokenId) - .first(); - - if (!accessToken) { - return null; - } - - return this.rowToAccessToken(accessToken) as AccessToken; - } - - async updateAccessToken( - accessToken: Partial & { tokenId: string }, - ) { - const row = this.accessTokenToRow(accessToken); - const updatedFields = Object.fromEntries( - Object.entries(row).filter(([_, value]) => value !== undefined), - ); - const [updated] = await this.db('oidc_access_tokens') - .where('token_id', accessToken.tokenId) - .update(updatedFields) - .returning('*'); - - return this.rowToAccessToken(updated) as AccessToken; + return accessToken; } private rowToClient(row: Partial): Partial { @@ -257,7 +309,54 @@ export class OidcDatabase { scope: row.scope ?? undefined, expiresAt: row.expires_at ?? undefined, metadata: row.metadata ? JSON.parse(row.metadata) : undefined, - createdAt: row.created_at, + }; + } + + private authorizationSessionToRow( + session: Partial, + ): Partial { + return { + id: session.id, + client_id: session.clientId, + user_entity_ref: session.userEntityRef, + redirect_uri: session.redirectUri, + scope: session.scope, + state: session.state, + response_type: session.responseType, + code_challenge: session.codeChallenge, + code_challenge_method: session.codeChallengeMethod, + nonce: session.nonce, + status: session.status, + expires_at: session.expiresAt, + }; + } + + private rowToAuthorizationSession( + row: Partial, + ): Partial { + return { + id: row.id, + clientId: row.client_id, + userEntityRef: row.user_entity_ref ?? undefined, + redirectUri: row.redirect_uri, + scope: row.scope ?? undefined, + state: row.state ?? undefined, + responseType: row.response_type, + codeChallenge: row.code_challenge ?? undefined, + codeChallengeMethod: row.code_challenge_method ?? undefined, + nonce: row.nonce ?? undefined, + status: row.status, + expiresAt: row.expires_at, + }; + } + + private rowToConsentRequest( + row: Partial, + ): Partial { + return { + id: row.id, + sessionId: row.session_id, + expiresAt: row.expires_at, }; } @@ -266,14 +365,7 @@ export class OidcDatabase { ): Partial { return { code: authorizationCode.code, - client_id: authorizationCode.clientId, - user_entity_ref: authorizationCode.userEntityRef, - redirect_uri: authorizationCode.redirectUri, - scope: authorizationCode.scope, - code_challenge: authorizationCode.codeChallenge, - code_challenge_method: authorizationCode.codeChallengeMethod, - nonce: authorizationCode.nonce, - created_at: authorizationCode.createdAt, + session_id: authorizationCode.sessionId, expires_at: authorizationCode.expiresAt, used: authorizationCode.used, }; @@ -284,44 +376,9 @@ export class OidcDatabase { ): Partial { return { code: row.code, - clientId: row.client_id, - userEntityRef: row.user_entity_ref, - redirectUri: row.redirect_uri, - scope: row.scope ?? undefined, - codeChallenge: row.code_challenge ?? undefined, - codeChallengeMethod: row.code_challenge_method ?? undefined, - nonce: row.nonce ?? undefined, - createdAt: row.created_at, + sessionId: row.session_id, expiresAt: row.expires_at, used: Boolean(row.used), }; } - - private accessTokenToRow( - accessToken: Partial, - ): Partial { - return { - token_id: accessToken.tokenId, - client_id: accessToken.clientId, - user_entity_ref: accessToken.userEntityRef, - scope: accessToken.scope, - created_at: accessToken.createdAt, - expires_at: accessToken.expiresAt, - revoked: accessToken.revoked, - }; - } - - private rowToAccessToken( - row: Partial, - ): Partial { - return { - tokenId: row.token_id, - clientId: row.client_id, - userEntityRef: row.user_entity_ref, - scope: row.scope ?? undefined, - createdAt: row.created_at, - expiresAt: row.expires_at, - revoked: Boolean(row.revoked), - }; - } } From eb2297fe6d792dd03aa766b97802adee7d1f18b7 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Thu, 3 Jul 2025 19:36:00 +0200 Subject: [PATCH 101/233] chore: updating the oidc service to handle consent Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- .../auth-backend/src/service/OidcService.ts | 249 ++++++++++++++++-- 1 file changed, 234 insertions(+), 15 deletions(-) diff --git a/plugins/auth-backend/src/service/OidcService.ts b/plugins/auth-backend/src/service/OidcService.ts index cc60b2b432..4214f53a8c 100644 --- a/plugins/auth-backend/src/service/OidcService.ts +++ b/plugins/auth-backend/src/service/OidcService.ts @@ -112,6 +112,9 @@ export class OidcService { const generatedClientId = crypto.randomUUID(); const generatedClientSecret = crypto.randomUUID(); + // todo(blam): add validation for redirectUris here. + // should be a list of urls and / or allowed schemes or something. + return await this.oidc.createClient({ clientId: generatedClientId, clientName: opts.clientName, @@ -123,6 +126,194 @@ export class OidcService { }); } + public async createConsentRequest(opts: { + clientId: string; + redirectUri: string; + responseType: string; + scope?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + }) { + const { + clientId, + redirectUri, + responseType, + scope, + state, + nonce, + codeChallenge, + codeChallengeMethod, + } = opts; + + if (responseType !== 'code') { + throw new InputError('Only authorization code flow is supported'); + } + + const client = await this.oidc.getClient({ clientId }); + if (!client) { + throw new InputError('Invalid client_id'); + } + + if (!client.redirectUris.includes(redirectUri)) { + throw new InputError('Invalid redirect_uri'); + } + + if (codeChallenge) { + if ( + !codeChallengeMethod || + !['S256', 'plain'].includes(codeChallengeMethod) + ) { + throw new InputError('Invalid code_challenge_method'); + } + } + + const sessionId = crypto.randomUUID(); + const sessionExpiresAt = DateTime.now().plus({ hours: 1 }).toISO(); + + await this.oidc.createAuthorizationSession({ + id: sessionId, + clientId, + redirectUri, + responseType, + scope, + state, + codeChallenge, + codeChallengeMethod, + nonce, + expiresAt: sessionExpiresAt, + }); + + const consentRequestId = crypto.randomUUID(); + const consentExpiresAt = DateTime.now().plus({ minutes: 30 }).toISO(); + + await this.oidc.createConsentRequest({ + id: consentRequestId, + sessionId, + expiresAt: consentExpiresAt, + }); + + return { + consentRequestId, + clientName: client.clientName, + scope, + redirectUri, + }; + } + + public async approveConsentRequest(opts: { + consentRequestId: string; + userEntityRef: string; + }) { + const { consentRequestId, userEntityRef } = opts; + + const consentRequest = await this.oidc.getConsentRequest({ + id: consentRequestId, + }); + if (!consentRequest) { + throw new InputError('Invalid consent request'); + } + + if (DateTime.fromISO(consentRequest.expiresAt) < DateTime.now()) { + throw new InputError('Consent request expired'); + } + + const session = await this.oidc.getAuthorizationSession({ + id: consentRequest.sessionId, + }); + if (!session) { + throw new InputError('Invalid authorization session'); + } + + if (DateTime.fromISO(session.expiresAt) < DateTime.now()) { + throw new InputError('Authorization session expired'); + } + + await this.oidc.updateAuthorizationSession({ + id: session.id, + userEntityRef, + status: 'approved', + }); + + const authorizationCode = crypto.randomBytes(32).toString('base64url'); + const codeExpiresAt = DateTime.now().plus({ minutes: 10 }).toISO(); + + await this.oidc.createAuthorizationCode({ + code: authorizationCode, + sessionId: session.id, + expiresAt: codeExpiresAt, + }); + + await this.oidc.deleteConsentRequest({ id: consentRequestId }); + + const redirectUrl = new URL(session.redirectUri); + redirectUrl.searchParams.append('code', authorizationCode); + if (session.state) { + redirectUrl.searchParams.append('state', session.state); + } + + return { + redirectUrl: redirectUrl.toString(), + }; + } + + public async getConsentRequest(opts: { consentRequestId: string }) { + const consentRequest = await this.oidc.getConsentRequest({ + id: opts.consentRequestId, + }); + if (!consentRequest) { + throw new InputError('Invalid consent request'); + } + + if (DateTime.fromISO(consentRequest.expiresAt) < DateTime.now()) { + throw new InputError('Consent request expired'); + } + + const session = await this.oidc.getAuthorizationSession({ + id: consentRequest.sessionId, + }); + + if (!session) { + throw new InputError('Invalid authorization session'); + } + + const client = await this.oidc.getClient({ clientId: session.clientId }); + if (!client) { + throw new InputError('Invalid client_id'); + } + + return { + id: consentRequest.id, + clientId: session.clientId, + clientName: client.clientName, + redirectUri: session.redirectUri, + scope: session.scope, + state: session.state, + responseType: session.responseType, + codeChallenge: session.codeChallenge, + codeChallengeMethod: session.codeChallengeMethod, + nonce: session.nonce, + expiresAt: consentRequest.expiresAt, + }; + } + + public async deleteConsentRequest(opts: { consentRequestId: string }) { + const consentRequest = await this.oidc.getConsentRequest({ + id: opts.consentRequestId, + }); + if (!consentRequest) { + return; + } + + await this.oidc.updateAuthorizationSession({ + id: consentRequest.sessionId, + status: 'rejected', + }); + + await this.oidc.deleteConsentRequest({ id: opts.consentRequestId }); + } + public async authorize(opts: { clientId: string; redirectUri: string; @@ -168,19 +359,35 @@ export class OidcService { } } - const authorizationCode = crypto.randomBytes(32).toString('base64url'); - const expiresAt = DateTime.now().plus({ minutes: 10 }).toISO(); + const sessionId = crypto.randomUUID(); + const sessionExpiresAt = DateTime.now().plus({ hours: 1 }).toISO(); - await this.oidc.createAuthorizationCode({ - code: authorizationCode, + await this.oidc.createAuthorizationSession({ + id: sessionId, clientId, userEntityRef, redirectUri, + responseType, scope, + state, codeChallenge, codeChallengeMethod, nonce, - expiresAt, + expiresAt: sessionExpiresAt, + }); + + await this.oidc.updateAuthorizationSession({ + id: sessionId, + status: 'approved', + }); + + const authorizationCode = crypto.randomBytes(32).toString('base64url'); + const codeExpiresAt = DateTime.now().plus({ minutes: 10 }).toISO(); + + await this.oidc.createAuthorizationCode({ + code: authorizationCode, + sessionId, + expiresAt: codeExpiresAt, }); const redirectUrl = new URL(redirectUri); @@ -237,24 +444,38 @@ export class OidcService { throw new AuthenticationError('Authorization code already used'); } - if (authCode.clientId !== clientId) { + const session = await this.oidc.getAuthorizationSession({ + id: authCode.sessionId, + }); + if (!session) { + throw new AuthenticationError('Invalid authorization session'); + } + if (session.clientId !== clientId) { throw new AuthenticationError('Client ID mismatch'); } - if (authCode.redirectUri !== redirectUri) { + if (session.redirectUri !== redirectUri) { throw new AuthenticationError('Redirect URI mismatch'); } - if (authCode.codeChallenge) { + if (session.status !== 'approved') { + throw new AuthenticationError('Authorization not approved'); + } + + if (!session.userEntityRef) { + throw new AuthenticationError('No user associated with authorization'); + } + + if (session.codeChallenge) { if (!codeVerifier) { throw new AuthenticationError('Code verifier required for PKCE'); } if ( !this.verifyPkce( - authCode.codeChallenge, + session.codeChallenge, codeVerifier, - authCode.codeChallengeMethod, + session.codeChallengeMethod, ) ) { throw new AuthenticationError('Invalid code verifier'); @@ -271,15 +492,13 @@ export class OidcService { await this.oidc.createAccessToken({ tokenId: accessTokenId, - clientId, - userEntityRef: authCode.userEntityRef, - scope: authCode.scope, + sessionId: session.id, expiresAt, }); const { token } = await this.tokenIssuer.issueToken({ claims: { - sub: authCode.userEntityRef, + sub: session.userEntityRef, }, }); @@ -288,7 +507,7 @@ export class OidcService { tokenType: 'Bearer', expiresIn: 3600, idToken: token, - scope: authCode.scope || 'openid', + scope: session.scope || 'openid', }; } From ff251064ae57fce729d3cf7a0164523e28e89ffb Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Thu, 3 Jul 2025 19:36:24 +0200 Subject: [PATCH 102/233] chore: implementing the routers Signed-off-by: benjdlambert --- .../src/service/OidcRouter.test.ts | 4 + .../auth-backend/src/service/OidcRouter.ts | 183 ++++++++++++++++-- plugins/auth-backend/src/service/router.ts | 1 + 3 files changed, 177 insertions(+), 11 deletions(-) diff --git a/plugins/auth-backend/src/service/OidcRouter.test.ts b/plugins/auth-backend/src/service/OidcRouter.test.ts index ccc2426035..c0f1543487 100644 --- a/plugins/auth-backend/src/service/OidcRouter.test.ts +++ b/plugins/auth-backend/src/service/OidcRouter.test.ts @@ -60,6 +60,8 @@ describe('OidcRouter', () => { auth, tokenIssuer: {} as any, baseUrl: 'http://localhost:7000', + appUrl: 'http://localhost:3000', + logger: mockServices.logger.mock(), userInfo: mockUserInfo, oidc: mockOidc, }).getRouter(), @@ -130,6 +132,8 @@ describe('OidcRouter', () => { auth, tokenIssuer: {} as any, baseUrl: 'http://localhost:7000', + appUrl: 'http://localhost:3000', + logger: mockServices.logger.mock(), userInfo: mockUserInfo, oidc: mockOidc, }).getRouter(), diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index 0e14b91d79..ba2f2ccef9 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -26,17 +26,25 @@ export class OidcRouter { private constructor( private readonly oidc: OidcService, private readonly logger: LoggerService, + private readonly auth: AuthService, + private readonly appUrl: string, ) {} static create(options: { auth: AuthService; tokenIssuer: TokenIssuer; baseUrl: string; + appUrl: string; logger: LoggerService; userInfo: UserInfoDatabase; oidc: OidcDatabase; }) { - return new OidcRouter(OidcService.create(options), options.logger); + return new OidcRouter( + OidcService.create(options), + options.logger, + options.auth, + options.appUrl, + ); } public getRouter() { @@ -44,15 +52,25 @@ export class OidcRouter { router.use(json()); + // OpenID Provider Configuration endpoint + // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig + // Returns the OpenID Provider Configuration document containing metadata about the provider router.get('/.well-known/openid-configuration', (_req, res) => { res.json(this.oidc.getConfiguration()); }); + // JSON Web Key Set endpoint + // https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.10.1.1 + // Returns the public keys used to verify JWTs issued by this provider router.get('/.well-known/jwks.json', async (_req, res) => { const { keys } = await this.oidc.listPublicKeys(); res.json({ keys }); }); + // Authorization endpoint + // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + // Handles the initial authorization request from the client, validates parameters, + // and redirects to the consent page for user approval router.get('/v1/authorize', async (req, res) => { // todo(blam): maybe add zod types for validating input const { @@ -76,11 +94,7 @@ export class OidcRouter { } try { - // use default user entity ref for now, as we need a redirect to the frontend plugin - // for the consent flow in order to issue the right token for the right user. - const userEntityRef = 'user:default/guest'; - - const { redirectUrl } = await this.oidc.authorize({ + const result = await this.oidc.createConsentRequest({ clientId: clientId as string, redirectUri: redirectUri as string, responseType: responseType as string, @@ -89,10 +103,14 @@ export class OidcRouter { nonce: nonce as string, codeChallenge: codeChallenge as string, codeChallengeMethod: codeChallengeMethod as string, - userEntityRef, }); - return res.redirect(redirectUrl); + // todo(blam): maybe this URL could be overridable by config if + // the plugin is mounted somewhere else? + const consentUrl = new URL('/oidc/consent', this.appUrl); + consentUrl.searchParams.append('consent_id', result.consentRequestId); + + return res.redirect(consentUrl.toString()); } catch (error) { const errorParams = new URLSearchParams(); errorParams.append( @@ -113,6 +131,146 @@ export class OidcRouter { } }); + // Consent request details endpoint + // Returns consent request details for the frontend consent page + router.get('/v1/consent/:consentId', async (req, res) => { + const { consentId } = req.params; + + if (!consentId) { + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing consent ID', + }); + } + + try { + const consentRequest = await this.oidc.getConsentRequest({ + consentRequestId: consentId, + }); + + return res.json({ + id: consentRequest.id, + clientName: consentRequest.clientName, + scope: consentRequest.scope, + redirectUri: consentRequest.redirectUri, + }); + } catch (error) { + this.logger.error( + `Failed to get consent request: ${ + isError(error) ? error.message : 'Unknown error' + }`, + error, + ); + return res.status(404).json({ + error: 'not_found', + error_description: 'Consent request not found or expired', + }); + } + }); + + // Consent approval endpoint + // Handles user approval of consent requests and generates authorization codes + router.post('/v1/consent/:consentId/approve', async (req, res) => { + const { consentId } = req.params; + + if (!consentId) { + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing consent ID', + }); + } + + try { + const authHeader = req.headers.authorization; + if (!authHeader?.startsWith('Bearer ')) { + return res.status(401).json({ + error: 'unauthorized', + error_description: 'Bearer token required', + }); + } + + const token = authHeader.substring(7); + const credentials = await this.auth.authenticate(token); + if (!this.auth.isPrincipal(credentials, 'user')) { + return res.status(401).json({ + error: 'unauthorized', + error_description: 'Authentication required', + }); + } + + const userEntityRef = credentials.principal.userEntityRef; + + const result = await this.oidc.approveConsentRequest({ + consentRequestId: consentId, + userEntityRef, + }); + + return res.json({ + redirectUrl: result.redirectUrl, + }); + } catch (error) { + this.logger.error( + `Failed to approve consent: ${ + isError(error) ? error.message : 'Unknown error' + }`, + error, + ); + return res.status(400).json({ + error: 'invalid_request', + error_description: isError(error) ? error.message : 'Unknown error', + }); + } + }); + + // Consent rejection endpoint + // Handles user rejection of consent requests and redirects with error + router.post('/v1/consent/:consentId/reject', async (req, res) => { + const { consentId } = req.params; + + if (!consentId) { + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing consent ID', + }); + } + + try { + const consentRequest = await this.oidc.getConsentRequest({ + consentRequestId: consentId, + }); + + await this.oidc.deleteConsentRequest({ consentRequestId: consentId }); + + const errorParams = new URLSearchParams(); + errorParams.append('error', 'access_denied'); + errorParams.append('error_description', 'User denied the request'); + if (consentRequest.state) { + errorParams.append('state', consentRequest.state); + } + + const redirectUrl = new URL(consentRequest.redirectUri); + redirectUrl.search = errorParams.toString(); + + return res.json({ + redirectUrl: redirectUrl.toString(), + }); + } catch (error) { + this.logger.error( + `Failed to reject consent: ${ + isError(error) ? error.message : 'Unknown error' + }`, + error, + ); + return res.status(400).json({ + error: 'invalid_request', + error_description: isError(error) ? error.message : 'Unknown error', + }); + } + }); + + // Token endpoint + // https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest + // Exchanges authorization codes for access tokens and ID tokens router.post('/v1/token', async (req, res) => { // todo(blam): maybe add zod types for validating input const { @@ -180,9 +338,9 @@ export class OidcRouter { } }); - // This endpoint doesn't use the regular HttpAuth, since the contract - // is specifically for the header to be communicated in the Authorization - // header, regardless of token type + // UserInfo endpoint + // https://openid.net/specs/openid-connect-core-1_0.html#UserInfo + // Returns claims about the authenticated user using an access token router.get('/v1/userinfo', async (req, res) => { const matches = req.headers.authorization?.match(/^Bearer[ ]+(\S+)$/i); const token = matches?.[1]; @@ -200,6 +358,9 @@ export class OidcRouter { res.json(userInfo); }); + // Dynamic Client Registration endpoint + // https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration + // Allows clients to register themselves dynamically with the provider router.post('/v1/register', async (req, res) => { // todo(blam): maybe add zod types for validating input const registrationRequest = req.body; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 5eb3450451..28f218c749 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -154,6 +154,7 @@ export async function createRouter( auth: options.auth, tokenIssuer, baseUrl: authUrl, + appUrl, userInfo, oidc, logger, From e31a1e2c0c71dbe5998bff45bd84d30267581094 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Thu, 3 Jul 2025 19:47:41 +0200 Subject: [PATCH 103/233] chore: fixing redirect path Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- plugins/auth-backend/src/service/OidcRouter.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index ba2f2ccef9..1db444db59 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -107,8 +107,10 @@ export class OidcRouter { // todo(blam): maybe this URL could be overridable by config if // the plugin is mounted somewhere else? - const consentUrl = new URL('/oidc/consent', this.appUrl); - consentUrl.searchParams.append('consent_id', result.consentRequestId); + const consentUrl = new URL( + `/auth/consent/${result.consentRequestId}`, + this.appUrl, + ); return res.redirect(consentUrl.toString()); } catch (error) { From ebe65724e4f7c404c781320170d18d6cf7f2bfed Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 4 Jul 2025 08:39:16 +0200 Subject: [PATCH 104/233] chore: added some tests for oidcservice Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- .../auth-backend/src/service/OidcRouter.ts | 13 +- .../src/service/OidcService.test.ts | 649 ++++++++++++++++++ 2 files changed, 654 insertions(+), 8 deletions(-) create mode 100644 plugins/auth-backend/src/service/OidcService.test.ts diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index 1db444db59..c7bb42e1f5 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -257,15 +257,12 @@ export class OidcRouter { redirectUrl: redirectUrl.toString(), }); } catch (error) { - this.logger.error( - `Failed to reject consent: ${ - isError(error) ? error.message : 'Unknown error' - }`, - error, - ); + const description = isError(error) ? error.message : 'Unknown error'; + this.logger.error(`Failed to reject consent: ${description}`, error); + return res.status(400).json({ error: 'invalid_request', - error_description: isError(error) ? error.message : 'Unknown error', + error_description: description, }); } }); @@ -335,7 +332,7 @@ export class OidcRouter { return res.status(500).json({ error: 'server_error', - error_description: isError(error) ? error.message : 'Unknown error', + error_description: description, }); } }); diff --git a/plugins/auth-backend/src/service/OidcService.test.ts b/plugins/auth-backend/src/service/OidcService.test.ts new file mode 100644 index 0000000000..fa4c0a12ac --- /dev/null +++ b/plugins/auth-backend/src/service/OidcService.test.ts @@ -0,0 +1,649 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + mockServices, + TestDatabaseId, + TestDatabases, +} from '@backstage/backend-test-utils'; +import { OidcService } from './OidcService'; +import { + BackstageCredentials, + BackstageServicePrincipal, + BackstageUserPrincipal, + resolvePackagePath, +} from '@backstage/backend-plugin-api'; +import { AuthDatabase } from '../database/AuthDatabase'; +import { OidcDatabase } from '../database/OidcDatabase'; +import { UserInfoDatabase } from '../database/UserInfoDatabase'; +import { InputError, AuthenticationError } from '@backstage/errors'; +import crypto from 'crypto'; +import { AnyJWK, TokenIssuer } from '../identity/types'; + +describe('OidcService', () => { + const databases = TestDatabases.create(); + + async function createOidcService(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + + await knex.migrate.latest({ + directory: resolvePackagePath( + '@backstage/plugin-auth-backend', + 'migrations', + ), + }); + + const oidcDatabase = await OidcDatabase.create({ + database: AuthDatabase.create({ + getClient: async () => knex, + }), + }); + + const mockAuth = mockServices.auth.mock(); + const mockTokenIssuer = { + issueToken: jest.fn(), + listPublicKeys: jest.fn(), + } as jest.Mocked; + + const mockUserInfo = { + addUserInfo: jest.fn(), + getUserInfo: jest.fn(), + } as unknown as jest.Mocked; + + return { + service: OidcService.create({ + auth: mockAuth, + tokenIssuer: mockTokenIssuer, + baseUrl: 'http://mock-base-url', + userInfo: mockUserInfo, + oidc: oidcDatabase, + }), + mocks: { + auth: mockAuth, + tokenIssuer: mockTokenIssuer, + userInfo: mockUserInfo, + }, + }; + } + + describe.each(databases.eachSupportedId())('%p', databaseId => { + describe('getConfiguration', () => { + it('should return OIDC configuration', async () => { + const { service } = await createOidcService(databaseId); + + const config = service.getConfiguration(); + + expect(config).toEqual({ + issuer: 'http://mock-base-url', + token_endpoint: 'http://mock-base-url/v1/token', + userinfo_endpoint: 'http://mock-base-url/v1/userinfo', + jwks_uri: 'http://mock-base-url/.well-known/jwks.json', + response_types_supported: ['code', 'id_token'], + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: [ + 'RS256', + 'RS384', + 'RS512', + 'ES256', + 'ES384', + 'ES512', + 'PS256', + 'PS384', + 'PS512', + 'EdDSA', + ], + scopes_supported: ['openid'], + token_endpoint_auth_methods_supported: [ + 'client_secret_basic', + 'client_secret_post', + ], + claims_supported: ['sub', 'ent'], + grant_types_supported: ['authorization_code'], + authorization_endpoint: 'http://mock-base-url/v1/authorize', + registration_endpoint: 'http://mock-base-url/v1/register', + code_challenge_methods_supported: ['S256', 'plain'], + }); + }); + }); + + describe('listPublicKeys', () => { + it('should return public keys from token issuer', async () => { + const { service, mocks } = await createOidcService(databaseId); + const mockKeys = [{ kid: 'key-1', use: 'sig' }] as AnyJWK[]; + mocks.tokenIssuer.listPublicKeys.mockResolvedValue({ keys: mockKeys }); + + const { keys } = await service.listPublicKeys(); + + expect(keys).toEqual(mockKeys); + expect(mocks.tokenIssuer.listPublicKeys).toHaveBeenCalledTimes(1); + }); + }); + + describe('getUserInfo', () => { + it('should return user info for valid token', async () => { + const { service, mocks } = await createOidcService(databaseId); + const mockCredentials: BackstageCredentials = { + principal: { + type: 'user', + userEntityRef: 'user:default/test', + }, + $$type: '@backstage/BackstageCredentials', + }; + const mockUserInfo = { sub: 'user:default/test', name: 'Test User' }; + + mocks.auth.authenticate.mockResolvedValue(mockCredentials); + mocks.auth.isPrincipal.mockReturnValue(true); + mocks.userInfo.getUserInfo.mockResolvedValue({ + claims: mockUserInfo, + }); + + const mockToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvdGVzdCJ9.signature'; + + const userInfo = await service.getUserInfo({ token: mockToken }); + + expect(userInfo).toEqual({ + claims: mockUserInfo, + }); + + expect(mocks.auth.authenticate).toHaveBeenCalledWith(mockToken, { + allowLimitedAccess: true, + }); + + expect(mocks.userInfo.getUserInfo).toHaveBeenCalledWith( + 'user:default/test', + ); + }); + + it('should throw error for non-user principal', async () => { + const { service, mocks } = await createOidcService(databaseId); + const mockCredentials: BackstageCredentials = + { + principal: { + type: 'service', + subject: 'test-service', + }, + $$type: '@backstage/BackstageCredentials', + }; + + mocks.auth.authenticate.mockResolvedValue(mockCredentials); + mocks.auth.isPrincipal.mockReturnValue(false); + + const mockToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvdGVzdCJ9.signature'; + + await expect(service.getUserInfo({ token: mockToken })).rejects.toThrow( + 'Userinfo endpoint must be called with a token that represents a user principal', + ); + }); + }); + + describe('registerClient', () => { + it('should create a new client with generated credentials', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + expect(client).toEqual( + expect.objectContaining({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }), + ); + expect(client.clientId).toBeDefined(); + expect(client.clientSecret).toBeDefined(); + }); + + it('should create a client with default values', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + }); + + expect(client).toEqual( + expect.objectContaining({ + clientName: 'Test Client', + redirectUris: [], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }), + ); + }); + }); + + describe('createConsentRequest', () => { + it('should create a consent request for valid client', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const consent = await service.createConsentRequest({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + expect(consent).toEqual({ + consentRequestId: expect.any(String), + clientName: 'Test Client', + scope: 'openid', + redirectUri: 'https://example.com/callback', + }); + }); + + it('should throw error for invalid client', async () => { + const { service } = await createOidcService(databaseId); + + await expect( + service.createConsentRequest({ + clientId: 'invalid-client', + redirectUri: 'https://example.com/callback', + responseType: 'code', + }), + ).rejects.toThrow('Invalid client_id'); + }); + + it('should throw error for invalid redirect URI', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + await expect( + service.createConsentRequest({ + clientId: client.clientId, + redirectUri: 'https://invalid.com/callback', + responseType: 'code', + }), + ).rejects.toThrow('Invalid redirect_uri'); + }); + + it('should throw error for unsupported response type', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + await expect( + service.createConsentRequest({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'token', + }), + ).rejects.toThrow('Only authorization code flow is supported'); + }); + + it('should handle PKCE parameters', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const consent = await service.createConsentRequest({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + codeChallenge: 'test-challenge', + codeChallengeMethod: 'S256', + }); + + expect(consent.consentRequestId).toBeDefined(); + }); + + it('should throw error for invalid PKCE method', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + await expect( + service.createConsentRequest({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + codeChallenge: 'test-challenge', + codeChallengeMethod: 'invalid', + }), + ).rejects.toThrow('Invalid code_challenge_method'); + }); + }); + + describe('approveConsentRequest', () => { + it('should approve a valid consent request', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const consent = await service.createConsentRequest({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + state: 'test-state', + }); + + const result = await service.approveConsentRequest({ + consentRequestId: consent.consentRequestId, + userEntityRef: 'user:default/test', + }); + + expect(result.redirectUrl).toMatch( + /^https:\/\/example\.com\/callback\?code=.+&state=test-state$/, + ); + }); + + it('should throw error for invalid consent request', async () => { + const { service } = await createOidcService(databaseId); + + await expect( + service.approveConsentRequest({ + consentRequestId: 'invalid-consent', + userEntityRef: 'user:default/test', + }), + ).rejects.toThrow('Invalid consent request'); + }); + }); + + describe('getConsentRequest', () => { + it('should return consent request details', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const consent = await service.createConsentRequest({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + const details = await service.getConsentRequest({ + consentRequestId: consent.consentRequestId, + }); + + expect(details).toEqual( + expect.objectContaining({ + id: consent.consentRequestId, + clientId: client.clientId, + clientName: 'Test Client', + redirectUri: 'https://example.com/callback', + scope: 'openid', + state: 'test-state', + responseType: 'code', + }), + ); + }); + }); + + describe('deleteConsentRequest', () => { + it('should delete a consent request', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const consent = await service.createConsentRequest({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.deleteConsentRequest({ + consentRequestId: consent.consentRequestId, + }); + + await expect( + service.getConsentRequest({ + consentRequestId: consent.consentRequestId, + }), + ).rejects.toThrow('Invalid consent request'); + }); + + it('should handle deleting non-existent consent request', async () => { + const { service } = await createOidcService(databaseId); + + await expect( + service.deleteConsentRequest({ + consentRequestId: 'non-existent', + }), + ).resolves.not.toThrow(); + }); + }); + + describe('authorize', () => { + it('should create direct authorization', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const result = await service.authorize({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + userEntityRef: 'user:default/test', + state: 'test-state', + }); + + expect(result.redirectUrl).toMatch( + /^https:\/\/example\.com\/callback\?code=.+&state=test-state$/, + ); + }); + + it('should throw error for invalid client', async () => { + const { service } = await createOidcService(databaseId); + + await expect( + service.authorize({ + clientId: 'invalid-client', + redirectUri: 'https://example.com/callback', + responseType: 'code', + userEntityRef: 'user:default/test', + }), + ).rejects.toThrow('Invalid client_id'); + }); + }); + + describe('exchangeCodeForToken', () => { + it('should exchange valid code for tokens', async () => { + const { service, mocks } = await createOidcService(databaseId); + const mockToken = 'mock-jwt-token'; + mocks.tokenIssuer.issueToken.mockResolvedValue({ token: mockToken }); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authResult = await service.authorize({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + userEntityRef: 'user:default/test', + scope: 'openid', + }); + + const code = new URL(authResult.redirectUrl).searchParams.get('code')!; + + const tokenResult = await service.exchangeCodeForToken({ + code, + clientId: client.clientId, + clientSecret: client.clientSecret, + redirectUri: 'https://example.com/callback', + grantType: 'authorization_code', + }); + + expect(tokenResult).toEqual({ + accessToken: mockToken, + tokenType: 'Bearer', + expiresIn: 3600, + idToken: mockToken, + scope: 'openid', + }); + }); + + it('should throw error for invalid grant type', async () => { + const { service } = await createOidcService(databaseId); + + await expect( + service.exchangeCodeForToken({ + code: 'test-code', + clientId: 'test-client', + clientSecret: 'test-secret', + redirectUri: 'https://example.com/callback', + grantType: 'client_credentials', + }), + ).rejects.toThrow('Unsupported grant type'); + }); + + it('should throw error for invalid client', async () => { + const { service } = await createOidcService(databaseId); + + await expect( + service.exchangeCodeForToken({ + code: 'test-code', + clientId: 'invalid-client', + clientSecret: 'test-secret', + redirectUri: 'https://example.com/callback', + grantType: 'authorization_code', + }), + ).rejects.toThrow('Invalid client'); + }); + + it('should throw error for invalid client secret', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + await expect( + service.exchangeCodeForToken({ + code: 'test-code', + clientId: client.clientId, + clientSecret: 'invalid-secret', + redirectUri: 'https://example.com/callback', + grantType: 'authorization_code', + }), + ).rejects.toThrow('Invalid client credentials'); + }); + + it('should handle PKCE verification', async () => { + const { service, mocks } = await createOidcService(databaseId); + const mockToken = 'mock-jwt-token'; + mocks.tokenIssuer.issueToken.mockResolvedValue({ token: mockToken }); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const codeVerifier = 'test-code-verifier'; + const codeChallenge = crypto + .createHash('sha256') + .update(codeVerifier) + .digest('base64url'); + + const authResult = await service.authorize({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + userEntityRef: 'user:default/test', + codeChallenge, + codeChallengeMethod: 'S256', + }); + + const code = new URL(authResult.redirectUrl).searchParams.get('code')!; + + const tokenResult = await service.exchangeCodeForToken({ + code, + clientId: client.clientId, + clientSecret: client.clientSecret, + redirectUri: 'https://example.com/callback', + grantType: 'authorization_code', + codeVerifier, + }); + + expect(tokenResult.accessToken).toBe(mockToken); + }); + + it('should throw error for invalid PKCE verifier', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const codeChallenge = 'test-challenge'; + const authResult = await service.authorize({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + userEntityRef: 'user:default/test', + codeChallenge, + codeChallengeMethod: 'S256', + }); + + const code = new URL(authResult.redirectUrl).searchParams.get('code')!; + + await expect( + service.exchangeCodeForToken({ + code, + clientId: client.clientId, + clientSecret: client.clientSecret, + redirectUri: 'https://example.com/callback', + grantType: 'authorization_code', + codeVerifier: 'invalid-verifier', + }), + ).rejects.toThrow('Invalid code verifier'); + }); + }); + }); +}); From 0d320ca888ffcfa20e6ab50273b130d7d199cf89 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 4 Jul 2025 09:21:22 +0200 Subject: [PATCH 105/233] chore: added some tests for oidcrouter and refactor Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- .../src/service/OidcRouter.test.ts | 1000 ++++++++++++++--- 1 file changed, 867 insertions(+), 133 deletions(-) diff --git a/plugins/auth-backend/src/service/OidcRouter.test.ts b/plugins/auth-backend/src/service/OidcRouter.test.ts index c0f1543487..150065ede5 100644 --- a/plugins/auth-backend/src/service/OidcRouter.test.ts +++ b/plugins/auth-backend/src/service/OidcRouter.test.ts @@ -17,156 +17,890 @@ import { coreServices, createBackendPlugin, + resolvePackagePath, } from '@backstage/backend-plugin-api'; -import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; -import Router from 'express-promise-router'; +import { + mockServices, + startTestBackend, + TestDatabases, + TestDatabaseId, +} from '@backstage/backend-test-utils'; import request from 'supertest'; +import crypto from 'crypto'; import { OidcRouter } from './OidcRouter'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; import { OidcDatabase } from '../database/OidcDatabase'; +import { AuthDatabase } from '../database/AuthDatabase'; +import { OidcService } from '../service/OidcService'; +import { TokenIssuer } from '../identity/types'; describe('OidcRouter', () => { - describe('/v1/userinfo', () => { - it('should return user info for full tokens', async () => { - const auth = mockServices.auth.mock(); - const mockUserInfo = { - getUserInfo: jest.fn().mockResolvedValue({ - claims: { - sub: 'k/ns:n', - ent: ['k/ns:a', 'k/ns:b'], - }, - }), - } as unknown as UserInfoDatabase; + const databases = TestDatabases.create(); - const mockOidc = { - createClient: jest.fn().mockResolvedValue({ - clientId: 'test', - clientSecret: 'test', - }), - } as unknown as OidcDatabase; + async function createRouter(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); - const { server } = await startTestBackend({ - features: [ - createBackendPlugin({ - pluginId: 'auth', - register(reg) { - reg.registerInit({ - deps: { httpRouter: coreServices.httpRouter }, - async init({ httpRouter }) { - const router = Router(); - - router.use( - OidcRouter.create({ - auth, - tokenIssuer: {} as any, - baseUrl: 'http://localhost:7000', - appUrl: 'http://localhost:3000', - logger: mockServices.logger.mock(), - userInfo: mockUserInfo, - oidc: mockOidc, - }).getRouter(), - ); - httpRouter.use(router); - httpRouter.addAuthPolicy({ - path: '/', - allow: 'unauthenticated', - }); - }, - }); - }, - }), - ], - }); - - auth.authenticate.mockResolvedValueOnce({} as any); - auth.isPrincipal.mockReturnValueOnce(true); - - await request(server) - .get('/api/auth/v1/userinfo') - .set( - 'Authorization', - `Bearer h.${btoa( - JSON.stringify({ sub: 'k/ns:n', ent: ['k/ns:a', 'k/ns:b'] }), - )}.s`, - ) - .expect(200, { - claims: { - sub: 'k/ns:n', - ent: ['k/ns:a', 'k/ns:b'], - }, - }); - - expect(mockUserInfo.getUserInfo).toHaveBeenCalledWith('k/ns:n'); + await knex.migrate.latest({ + directory: resolvePackagePath( + '@backstage/plugin-auth-backend', + 'migrations', + ), }); - it('should return user info for limited tokens', async () => { - const auth = mockServices.auth.mock(); - const mockUserInfo = { - getUserInfo: jest.fn().mockResolvedValue({ - claims: { - sub: 'k/ns:n', - ent: ['k/ns:a', 'k/ns:b'], - }, - }), - } as unknown as UserInfoDatabase; - - const mockOidc = { - createClient: jest.fn().mockResolvedValue({ - clientId: 'test', - clientSecret: 'test', - }), - } as unknown as OidcDatabase; - - const { server } = await startTestBackend({ - features: [ - createBackendPlugin({ - pluginId: 'auth', - register(reg) { - reg.registerInit({ - deps: { httpRouter: coreServices.httpRouter }, - async init({ httpRouter }) { - const router = Router(); - - router.use( - OidcRouter.create({ - auth, - tokenIssuer: {} as any, - baseUrl: 'http://localhost:7000', - appUrl: 'http://localhost:3000', - logger: mockServices.logger.mock(), - userInfo: mockUserInfo, - oidc: mockOidc, - }).getRouter(), - ); - httpRouter.use(router); - httpRouter.addAuthPolicy({ - path: '/', - allow: 'unauthenticated', - }); - }, - }); - }, - }), - ], - }); - - auth.authenticate.mockResolvedValueOnce({} as any); - auth.isPrincipal.mockReturnValueOnce(true); - - await request(server) - .get('/api/auth/v1/userinfo') - .set( - 'Authorization', - `Bearer h.${btoa(JSON.stringify({ sub: 'k/ns:n' }))}.s`, - ) - .expect(200, { + const authDatabase = AuthDatabase.create({ + getClient: async () => knex, + }); + + const oidcDatabase = await OidcDatabase.create({ + database: authDatabase, + }); + + const userInfoDatabase = await UserInfoDatabase.create({ + database: authDatabase, + }); + + const mockTokenIssuer = { + issueToken: jest.fn(), + listPublicKeys: jest.fn(), + } as unknown as jest.Mocked; + + const mockAuth = mockServices.auth.mock(); + + const oidcService = OidcService.create({ + auth: mockAuth, + tokenIssuer: mockTokenIssuer, + baseUrl: 'http://localhost:7000', + userInfo: userInfoDatabase, + oidc: oidcDatabase, + }); + + const oidcRouter = OidcRouter.create({ + auth: mockAuth, + tokenIssuer: mockTokenIssuer, + baseUrl: 'http://localhost:7000', + appUrl: 'http://localhost:3000', + logger: mockServices.logger.mock(), + userInfo: userInfoDatabase, + oidc: oidcDatabase, + }); + + return { + router: oidcRouter, + mocks: { + auth: mockAuth, + oidc: oidcDatabase, + userInfo: userInfoDatabase, + service: oidcService, + tokenIssuer: mockTokenIssuer, + }, + }; + } + + describe.each(databases.eachSupportedId())('%p', databaseId => { + describe('/v1/userinfo', () => { + it('should return user info for full tokens', async () => { + const { + mocks: { auth, userInfo }, + router, + } = await createRouter(databaseId); + + await userInfo.addUserInfo({ claims: { sub: 'k/ns:n', ent: ['k/ns:a', 'k/ns:b'], + exp: Math.floor(Date.now() / 1000) + 3600, }, }); - expect(mockUserInfo.getUserInfo).toHaveBeenCalledWith('k/ns:n'); + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + auth.authenticate.mockResolvedValueOnce({} as any); + auth.isPrincipal.mockReturnValueOnce(true); + + const response = await request(server) + .get('/api/auth/v1/userinfo') + .set( + 'Authorization', + `Bearer h.${btoa( + JSON.stringify({ sub: 'k/ns:n', ent: ['k/ns:a', 'k/ns:b'] }), + )}.s`, + ) + .expect(200); + + expect(response.body).toEqual({ + claims: { + sub: 'k/ns:n', + ent: ['k/ns:a', 'k/ns:b'], + exp: expect.any(Number), + }, + }); + }); + + it('should return user info for limited tokens', async () => { + const { + mocks: { auth, userInfo }, + router, + } = await createRouter(databaseId); + + await userInfo.addUserInfo({ + claims: { + sub: 'k/ns:n', + ent: ['k/ns:a', 'k/ns:b'], + exp: Math.floor(Date.now() / 1000) + 3600, + }, + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + auth.authenticate.mockResolvedValueOnce({} as any); + auth.isPrincipal.mockReturnValueOnce(true); + + const response = await request(server) + .get('/api/auth/v1/userinfo') + .set( + 'Authorization', + `Bearer h.${btoa(JSON.stringify({ sub: 'k/ns:n' }))}.s`, + ) + .expect(200); + + expect(response.body).toEqual({ + claims: { + sub: 'k/ns:n', + ent: ['k/ns:a', 'k/ns:b'], + exp: expect.any(Number), + }, + }); + }); + }); + + describe('consent flow', () => { + it('should register a client', async () => { + const { router } = await createRouter(databaseId); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const response = await request(server) + .post('/api/auth/v1/register') + .send({ + client_name: 'Test Client', + redirect_uris: ['https://example.com/callback'], + response_types: ['code'], + grant_types: ['authorization_code'], + scope: 'openid', + }) + .expect(201); + + expect(response.body).toEqual({ + client_id: expect.any(String), + client_secret: expect.any(String), + redirect_uris: ['https://example.com/callback'], + }); + }); + + it('should create a consent request via authorization endpoint', async () => { + const { + mocks: { service }, + router, + } = await createRouter(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const response = await request(server) + .get('/api/auth/v1/authorize') + .query({ + client_id: client.clientId, + redirect_uri: 'https://example.com/callback', + response_type: 'code', + scope: 'openid', + state: 'test-state', + }) + .expect(302); + + expect(response.header.location).toMatch( + /^http:\/\/localhost:3000\/auth\/consent\/[a-f0-9-]+$/, + ); + }); + + it('should get consent request details', async () => { + const { + mocks: { service }, + router, + } = await createRouter(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const consentRequest = await service.createConsentRequest({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const response = await request(server) + .get(`/api/auth/v1/consent/${consentRequest.consentRequestId}`) + .expect(200); + + expect(response.body).toEqual({ + id: consentRequest.consentRequestId, + clientName: 'Test Client', + scope: 'openid', + redirectUri: 'https://example.com/callback', + }); + }); + + it('should approve consent request', async () => { + const { + mocks: { auth, service }, + router, + } = await createRouter(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const consentRequest = await service.createConsentRequest({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + auth.authenticate.mockResolvedValueOnce({ + principal: { + type: 'user', + userEntityRef: 'user:default/test-user', + }, + $$type: '@backstage/BackstageCredentials', + }); + + auth.isPrincipal.mockReturnValueOnce(true); + + const response = await request(server) + .post( + `/api/auth/v1/consent/${consentRequest.consentRequestId}/approve`, + ) + .set('Authorization', 'Bearer test-token') + .expect(200); + + expect(response.body).toEqual({ + redirectUrl: expect.stringMatching( + /^https:\/\/example\.com\/callback\?code=[\w-]+&state=test-state$/, + ), + }); + }); + + it('should reject consent request', async () => { + const { + mocks: { service }, + router, + } = await createRouter(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const consentRequest = await service.createConsentRequest({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const response = await request(server) + .post( + `/api/auth/v1/consent/${consentRequest.consentRequestId}/reject`, + ) + .expect(200); + + expect(response.body).toEqual({ + redirectUrl: expect.stringMatching( + /^https:\/\/example\.com\/callback\?error=access_denied&error_description=User\+denied\+the\+request&state=test-state$/, + ), + }); + }); + }); + + describe('token exchange', () => { + it('should exchange authorization code for tokens', async () => { + const { + mocks: { auth, service, tokenIssuer }, + router, + } = await createRouter(databaseId); + + auth.authenticate.mockResolvedValueOnce({ + principal: { + type: 'user', + userEntityRef: 'user:default/test-user', + }, + $$type: '@backstage/BackstageCredentials', + }); + auth.isPrincipal.mockReturnValueOnce(true); + + tokenIssuer.issueToken.mockResolvedValue({ + token: 'mock-access-token', + }); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const consentRequest = await service.createConsentRequest({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const approvalResponse = await request(server) + .post( + `/api/auth/v1/consent/${consentRequest.consentRequestId}/approve`, + ) + .set('Authorization', 'Bearer test-token') + .expect(200); + + const redirectUrl = new URL(approvalResponse.body.redirectUrl); + const authorizationCode = redirectUrl.searchParams.get('code'); + + expect(authorizationCode).toBeDefined(); + + const tokenResponse = await request(server) + .post('/api/auth/v1/token') + .send({ + grant_type: 'authorization_code', + code: authorizationCode, + client_id: client.clientId, + client_secret: client.clientSecret, + redirect_uri: 'https://example.com/callback', + }) + .expect(200); + + expect(tokenResponse.body).toEqual({ + access_token: 'mock-access-token', + token_type: 'Bearer', + expires_in: 3600, + id_token: 'mock-access-token', + scope: 'openid', + }); + + expect(tokenIssuer.issueToken).toHaveBeenCalledWith({ + claims: { + sub: 'user:default/test-user', + }, + }); + }); + + it('should exchange authorization code for tokens with PKCE', async () => { + const { + mocks: { auth, service, tokenIssuer }, + router, + } = await createRouter(databaseId); + + tokenIssuer.issueToken.mockResolvedValue({ + token: 'mock-access-token-pkce', + }); + + auth.authenticate.mockResolvedValueOnce({ + principal: { + type: 'user', + userEntityRef: 'user:default/test-user-pkce', + }, + $$type: '@backstage/BackstageCredentials', + }); + auth.isPrincipal.mockReturnValueOnce(true); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const codeVerifier = + 'test-code-verifier-123456789012345678901234567890123456789012345'; + const codeChallenge = codeVerifier; + + const consentRequest = await service.createConsentRequest({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + codeChallenge, + codeChallengeMethod: 'plain', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const approvalResponse = await request(server) + .post( + `/api/auth/v1/consent/${consentRequest.consentRequestId}/approve`, + ) + .set('Authorization', 'Bearer test-token') + .expect(200); + + const redirectUrl = new URL(approvalResponse.body.redirectUrl); + const authorizationCode = redirectUrl.searchParams.get('code'); + + expect(authorizationCode).toBeDefined(); + + const tokenResponse = await request(server) + .post('/api/auth/v1/token') + .send({ + grant_type: 'authorization_code', + code: authorizationCode, + client_id: client.clientId, + client_secret: client.clientSecret, + redirect_uri: 'https://example.com/callback', + code_verifier: codeVerifier, + }) + .expect(200); + + expect(tokenResponse.body).toEqual({ + access_token: 'mock-access-token-pkce', + token_type: 'Bearer', + expires_in: 3600, + id_token: 'mock-access-token-pkce', + scope: 'openid', + }); + + expect(tokenIssuer.issueToken).toHaveBeenCalledWith({ + claims: { + sub: 'user:default/test-user-pkce', + }, + }); + }); + + it('should reject token exchange with invalid client credentials', async () => { + const { + mocks: { auth, service }, + router, + } = await createRouter(databaseId); + + auth.authenticate.mockResolvedValueOnce({ + principal: { + type: 'user', + userEntityRef: 'user:default/test-user', + }, + $$type: '@backstage/BackstageCredentials', + }); + auth.isPrincipal.mockReturnValueOnce(true); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const consentRequest = await service.createConsentRequest({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const approvalResponse = await request(server) + .post( + `/api/auth/v1/consent/${consentRequest.consentRequestId}/approve`, + ) + .set('Authorization', 'Bearer test-token') + .expect(200); + + const redirectUrl = new URL(approvalResponse.body.redirectUrl); + const authorizationCode = redirectUrl.searchParams.get('code'); + + const tokenResponse = await request(server) + .post('/api/auth/v1/token') + .send({ + grant_type: 'authorization_code', + code: authorizationCode, + client_id: client.clientId, + client_secret: 'invalid-secret', + redirect_uri: 'https://example.com/callback', + }) + .expect(401); + + expect(tokenResponse.body).toEqual({ + error: 'invalid_client', + error_description: 'Invalid client credentials', + }); + }); + + it('should reject token exchange with invalid authorization code', async () => { + const { + mocks: { service }, + router, + } = await createRouter(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const tokenResponse = await request(server) + .post('/api/auth/v1/token') + .send({ + grant_type: 'authorization_code', + code: 'invalid-code', + client_id: client.clientId, + client_secret: client.clientSecret, + redirect_uri: 'https://example.com/callback', + }) + .expect(401); + + expect(tokenResponse.body).toEqual({ + error: 'invalid_client', + error_description: 'Invalid authorization code', + }); + }); + + it('should exchange authorization code for tokens with PKCE S256', async () => { + const { + mocks: { auth, service, tokenIssuer }, + router, + } = await createRouter(databaseId); + + tokenIssuer.issueToken.mockResolvedValue({ + token: 'mock-access-token-s256', + }); + + auth.authenticate.mockResolvedValueOnce({ + principal: { + type: 'user', + userEntityRef: 'user:default/test-user-s256', + }, + $$type: '@backstage/BackstageCredentials', + }); + auth.isPrincipal.mockReturnValueOnce(true); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + scope: 'openid', + }); + + const codeVerifier = + 'test-code-verifier-s256-123456789012345678901234567890123456789'; + const codeChallenge = crypto + .createHash('sha256') + .update(codeVerifier) + .digest('base64url'); + + const consentRequest = await service.createConsentRequest({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + codeChallenge, + codeChallengeMethod: 'S256', + }); + + const { server } = await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'auth', + register(reg) { + reg.registerInit({ + deps: { httpRouter: coreServices.httpRouter }, + async init({ httpRouter }) { + httpRouter.use(router.getRouter()); + httpRouter.addAuthPolicy({ + path: '/', + allow: 'unauthenticated', + }); + }, + }); + }, + }), + ], + }); + + const approvalResponse = await request(server) + .post( + `/api/auth/v1/consent/${consentRequest.consentRequestId}/approve`, + ) + .set('Authorization', 'Bearer test-token') + .expect(200); + + const redirectUrl = new URL(approvalResponse.body.redirectUrl); + const authorizationCode = redirectUrl.searchParams.get('code'); + + expect(authorizationCode).toBeDefined(); + + const tokenResponse = await request(server) + .post('/api/auth/v1/token') + .send({ + grant_type: 'authorization_code', + code: authorizationCode, + client_id: client.clientId, + client_secret: client.clientSecret, + redirect_uri: 'https://example.com/callback', + code_verifier: codeVerifier, + }) + .expect(200); + + expect(tokenResponse.body).toEqual({ + access_token: 'mock-access-token-s256', + token_type: 'Bearer', + expires_in: 3600, + id_token: 'mock-access-token-s256', + scope: 'openid', + }); + + expect(tokenIssuer.issueToken).toHaveBeenCalledWith({ + claims: { + sub: 'user:default/test-user-s256', + }, + }); + }); }); }); }); From bf372ab53f70b156a9e5cc7c2d4c30e68df53648 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 7 Jul 2025 12:21:57 +0200 Subject: [PATCH 106/233] chore: cleanup and simplify Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- ...20250701120000_oidc_client_registration.js | 63 +------- .../src/database/OidcDatabase.test.ts | 142 ------------------ .../auth-backend/src/database/OidcDatabase.ts | 66 -------- .../src/service/OidcRouter.test.ts | 80 ++++------ .../auth-backend/src/service/OidcRouter.ts | 110 +++++++------- .../src/service/OidcService.test.ts | 80 +++++----- .../auth-backend/src/service/OidcService.ts | 88 ++++------- plugins/auth-backend/src/service/router.ts | 4 + 8 files changed, 166 insertions(+), 467 deletions(-) diff --git a/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js b/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js index bf8ee521f0..e175c922c1 100644 --- a/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js +++ b/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js @@ -43,11 +43,6 @@ exports.up = async function up(knex) { .notNullable() .comment('The name of the client, should be human readable'); - table - .timestamp('expires_at', { useTz: false, precision: 0 }) - .nullable() - .comment('Client registration expiration timestamp'); - table .text('response_types') .notNullable() @@ -110,7 +105,7 @@ exports.up = async function up(knex) { .comment('Authorization session status'); table - .timestamp('expires_at', { useTz: false, precision: 0 }) + .timestamp('expires_at', { useTz: true, precision: 0 }) .notNullable() .comment('Session expiration timestamp'); @@ -119,32 +114,6 @@ exports.up = async function up(knex) { table.index(['status', 'expires_at']); }); - await knex.schema.createTable('oidc_consent_requests', table => { - table.comment('User consent requests for OAuth authorization'); - - table - .string('id') - .primary() - .notNullable() - .comment('Unique consent request identifier'); - - table - .string('session_id') - .notNullable() - .comment('Authorization session identifier'); - - table - .timestamp('expires_at', { useTz: false, precision: 0 }) - .notNullable() - .comment('Consent request expiration timestamp'); - - table - .foreign('session_id') - .references('id') - .inTable('oauth_authorization_sessions') - .onDelete('CASCADE'); - }); - await knex.schema.createTable('oidc_authorization_codes', table => { table.comment('OAuth authorization codes for code exchange flow'); @@ -160,7 +129,7 @@ exports.up = async function up(knex) { .comment('Authorization session identifier'); table - .timestamp('expires_at', { useTz: false, precision: 0 }) + .timestamp('expires_at', { useTz: true, precision: 0 }) .notNullable() .comment('Authorization code expiration timestamp'); @@ -175,41 +144,13 @@ exports.up = async function up(knex) { .inTable('oauth_authorization_sessions') .onDelete('CASCADE'); }); - - await knex.schema.createTable('oidc_access_tokens', table => { - table.comment('OAuth access tokens for API access'); - - table - .string('token_id') - .primary() - .notNullable() - .comment('Unique access token identifier'); - - table - .string('session_id') - .notNullable() - .comment('Authorization session identifier'); - - table - .timestamp('expires_at', { useTz: false, precision: 0 }) - .notNullable() - .comment('Access token expiration timestamp'); - - table - .foreign('session_id') - .references('id') - .inTable('oauth_authorization_sessions') - .onDelete('CASCADE'); - }); }; /** * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { - await knex.schema.dropTable('oidc_access_tokens'); await knex.schema.dropTable('oidc_authorization_codes'); - await knex.schema.dropTable('oidc_consent_requests'); await knex.schema.dropTable('oauth_authorization_sessions'); await knex.schema.dropTable('oidc_clients'); }; diff --git a/plugins/auth-backend/src/database/OidcDatabase.test.ts b/plugins/auth-backend/src/database/OidcDatabase.test.ts index f9c2b83511..82064361a5 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.test.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.test.ts @@ -172,111 +172,6 @@ describe('Oidc Database', () => { }); }); - describe('Consent Requests', () => { - it('should create and return a consent request', async () => { - const { oidc } = await createOidcDatabase(databaseId); - - const client = await oidc.createClient({ - clientId: 'test-client', - clientName: 'Test Client', - clientSecret: 'test-secret', - redirectUris: ['https://example.com/callback'], - responseTypes: ['code'], - grantTypes: ['authorization_code'], - }); - - const session = await oidc.createAuthorizationSession({ - id: 'test-session', - clientId: client.clientId, - redirectUri: 'https://example.com/callback', - responseType: 'code', - expiresAt: '2025-01-01T00:00:00Z', - }); - - const consentRequest = await oidc.createConsentRequest({ - id: 'test-consent', - sessionId: session.id, - expiresAt: '2025-01-01T00:00:00Z', - }); - - await expect( - oidc.getConsentRequest({ id: 'test-consent' }), - ).resolves.toEqual(consentRequest); - }); - - it('should return consent request with session data', async () => { - const { oidc } = await createOidcDatabase(databaseId); - - const client = await oidc.createClient({ - clientId: 'test-client', - clientName: 'Test Client', - clientSecret: 'test-secret', - redirectUris: ['https://example.com/callback'], - responseTypes: ['code'], - grantTypes: ['authorization_code'], - }); - - const session = await oidc.createAuthorizationSession({ - id: 'test-session', - clientId: client.clientId, - redirectUri: 'https://example.com/callback', - responseType: 'code', - scope: 'openid', - state: 'test-state', - expiresAt: '2025-01-01T00:00:00Z', - }); - - const consentRequest = await oidc.createConsentRequest({ - id: 'test-consent', - sessionId: session.id, - expiresAt: '2025-01-01T00:00:00Z', - }); - - const consentFromDb = await oidc.getConsentRequest({ - id: 'test-consent', - }); - const sessionFromDb = await oidc.getAuthorizationSession({ - id: consentFromDb!.sessionId, - }); - - expect(consentFromDb).toEqual(consentRequest); - expect(sessionFromDb).toEqual(session); - }); - - it('should delete consent request', async () => { - const { oidc } = await createOidcDatabase(databaseId); - - const client = await oidc.createClient({ - clientId: 'test-client', - clientName: 'Test Client', - clientSecret: 'test-secret', - redirectUris: ['https://example.com/callback'], - responseTypes: ['code'], - grantTypes: ['authorization_code'], - }); - - const session = await oidc.createAuthorizationSession({ - id: 'test-session', - clientId: client.clientId, - redirectUri: 'https://example.com/callback', - responseType: 'code', - expiresAt: '2025-01-01T00:00:00Z', - }); - - await oidc.createConsentRequest({ - id: 'test-consent', - sessionId: session.id, - expiresAt: '2025-01-01T00:00:00Z', - }); - - await oidc.deleteConsentRequest({ id: 'test-consent' }); - - await expect( - oidc.getConsentRequest({ id: 'test-consent' }), - ).resolves.toBeNull(); - }); - }); - describe('Authorization Codes', () => { it('should create and return an authorization code', async () => { const { oidc } = await createOidcDatabase(databaseId); @@ -392,42 +287,5 @@ describe('Oidc Database', () => { }); }); }); - - describe('Access Tokens', () => { - it('should create and return an access token', async () => { - const { oidc } = await createOidcDatabase(databaseId); - - const client = await oidc.createClient({ - clientId: 'test-client', - clientName: 'Test Client', - clientSecret: 'test-secret', - redirectUris: ['https://example.com/callback'], - responseTypes: ['code'], - grantTypes: ['authorization_code'], - }); - - const session = await oidc.createAuthorizationSession({ - id: 'test-session', - clientId: client.clientId, - redirectUri: 'https://example.com/callback', - responseType: 'code', - expiresAt: '2025-01-01T00:00:00Z', - }); - - const accessToken = await oidc.createAccessToken({ - tokenId: 'test-token', - sessionId: session.id, - expiresAt: '2025-01-01T00:00:00Z', - }); - - expect(accessToken).toEqual( - expect.objectContaining({ - tokenId: 'test-token', - sessionId: session.id, - expiresAt: '2025-01-01T00:00:00Z', - }), - ); - }); - }); }); }); diff --git a/plugins/auth-backend/src/database/OidcDatabase.ts b/plugins/auth-backend/src/database/OidcDatabase.ts index 0c75290f12..17dd4f2c06 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.ts @@ -20,7 +20,6 @@ type OidcClientRow = { client_id: string; client_secret: string; client_name: string; - expires_at: string | null; response_types: string; grant_types: string; redirect_uris: string; @@ -43,12 +42,6 @@ type OAuthAuthorizationSessionRow = { expires_at: string; }; -type OidcConsentRequestRow = { - id: string; - session_id: string; - expires_at: string; -}; - type OidcAuthorizationCodeRow = { code: string; session_id: string; @@ -56,12 +49,6 @@ type OidcAuthorizationCodeRow = { used: boolean; }; -type OidcAccessTokenRow = { - token_id: string; - session_id: string; - expires_at: string; -}; - export type Client = { clientId: string; clientName: string; @@ -70,7 +57,6 @@ export type Client = { responseTypes: string[]; grantTypes: string[]; scope?: string; - expiresAt?: string; metadata?: Record; }; @@ -125,7 +111,6 @@ export class OidcDatabase { client_id: client.clientId, client_secret: client.clientSecret, client_name: client.clientName, - expires_at: client.expiresAt, response_types: JSON.stringify(client.responseTypes), grant_types: JSON.stringify(client.grantTypes), redirect_uris: JSON.stringify(client.redirectUris), @@ -192,30 +177,6 @@ export class OidcDatabase { return this.rowToAuthorizationSession(updated) as AuthorizationSession; } - async createConsentRequest(consentRequest: ConsentRequest) { - await this.db('oidc_consent_requests').insert({ - id: consentRequest.id, - session_id: consentRequest.sessionId, - expires_at: consentRequest.expiresAt, - }); - - return consentRequest; - } - - async getConsentRequest({ id }: { id: string }) { - const consentRequest = await this.db( - 'oidc_consent_requests', - ) - .where('id', id) - .first(); - - if (!consentRequest) { - return null; - } - - return this.rowToConsentRequest(consentRequest) as ConsentRequest; - } - async getAuthorizationSession({ id }: { id: string }) { const session = await this.db( 'oauth_authorization_sessions', @@ -230,12 +191,6 @@ export class OidcDatabase { return this.rowToAuthorizationSession(session) as AuthorizationSession; } - async deleteConsentRequest({ id }: { id: string }) { - await this.db('oidc_consent_requests') - .where('id', id) - .delete(); - } - async createAuthorizationCode( authorizationCode: Omit, ) { @@ -284,16 +239,6 @@ export class OidcDatabase { return this.rowToAuthorizationCode(updated) as AuthorizationCode; } - async createAccessToken(accessToken: AccessToken) { - await this.db('oidc_access_tokens').insert({ - token_id: accessToken.tokenId, - session_id: accessToken.sessionId, - expires_at: accessToken.expiresAt, - }); - - return accessToken; - } - private rowToClient(row: Partial): Partial { return { clientId: row.client_id, @@ -307,7 +252,6 @@ export class OidcDatabase { : undefined, grantTypes: row.grant_types ? JSON.parse(row.grant_types) : undefined, scope: row.scope ?? undefined, - expiresAt: row.expires_at ?? undefined, metadata: row.metadata ? JSON.parse(row.metadata) : undefined, }; } @@ -350,16 +294,6 @@ export class OidcDatabase { }; } - private rowToConsentRequest( - row: Partial, - ): Partial { - return { - id: row.id, - sessionId: row.session_id, - expiresAt: row.expires_at, - }; - } - private authorizationCodeToRow( authorizationCode: Partial, ): Partial { diff --git a/plugins/auth-backend/src/service/OidcRouter.test.ts b/plugins/auth-backend/src/service/OidcRouter.test.ts index 150065ede5..579c96470f 100644 --- a/plugins/auth-backend/src/service/OidcRouter.test.ts +++ b/plugins/auth-backend/src/service/OidcRouter.test.ts @@ -24,6 +24,7 @@ import { startTestBackend, TestDatabases, TestDatabaseId, + mockCredentials, } from '@backstage/backend-test-utils'; import request from 'supertest'; import crypto from 'crypto'; @@ -35,6 +36,8 @@ import { OidcService } from '../service/OidcService'; import { TokenIssuer } from '../identity/types'; describe('OidcRouter', () => { + const MOCK_USER_TOKEN = 'mock-user-token'; + const MOCK_USER_ENTITY_REF = 'user:default/test-user'; const databases = TestDatabases.create(); async function createRouter(databaseId: TestDatabaseId) { @@ -82,6 +85,9 @@ describe('OidcRouter', () => { logger: mockServices.logger.mock(), userInfo: userInfoDatabase, oidc: oidcDatabase, + httpAuth: mockServices.httpAuth({ + defaultCredentials: mockCredentials.user(), + }), }); return { @@ -209,7 +215,7 @@ describe('OidcRouter', () => { }); }); - describe('consent flow', () => { + describe('auth flow', () => { it('should register a client', async () => { const { router } = await createRouter(databaseId); @@ -251,7 +257,7 @@ describe('OidcRouter', () => { }); }); - it('should create a consent request via authorization endpoint', async () => { + it('should create an authorization session via authorization endpoint', async () => { const { mocks: { service }, router, @@ -297,11 +303,11 @@ describe('OidcRouter', () => { .expect(302); expect(response.header.location).toMatch( - /^http:\/\/localhost:3000\/auth\/consent\/[a-f0-9-]+$/, + /^http:\/\/localhost:3000\/auth\/sessions\/[a-f0-9-]+$/, ); }); - it('should get consent request details', async () => { + it('should get auth session details', async () => { const { mocks: { service }, router, @@ -315,7 +321,7 @@ describe('OidcRouter', () => { scope: 'openid', }); - const consentRequest = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -344,11 +350,11 @@ describe('OidcRouter', () => { }); const response = await request(server) - .get(`/api/auth/v1/consent/${consentRequest.consentRequestId}`) + .get(`/api/auth/v1/sessions/${authSession.id}`) .expect(200); expect(response.body).toEqual({ - id: consentRequest.consentRequestId, + id: authSession.id, clientName: 'Test Client', scope: 'openid', redirectUri: 'https://example.com/callback', @@ -369,7 +375,7 @@ describe('OidcRouter', () => { scope: 'openid', }); - const consentRequest = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -397,21 +403,11 @@ describe('OidcRouter', () => { ], }); - auth.authenticate.mockResolvedValueOnce({ - principal: { - type: 'user', - userEntityRef: 'user:default/test-user', - }, - $$type: '@backstage/BackstageCredentials', - }); - auth.isPrincipal.mockReturnValueOnce(true); const response = await request(server) - .post( - `/api/auth/v1/consent/${consentRequest.consentRequestId}/approve`, - ) - .set('Authorization', 'Bearer test-token') + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) .expect(200); expect(response.body).toEqual({ @@ -421,7 +417,7 @@ describe('OidcRouter', () => { }); }); - it('should reject consent request', async () => { + it('should reject auth session', async () => { const { mocks: { service }, router, @@ -435,7 +431,7 @@ describe('OidcRouter', () => { scope: 'openid', }); - const consentRequest = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -464,9 +460,7 @@ describe('OidcRouter', () => { }); const response = await request(server) - .post( - `/api/auth/v1/consent/${consentRequest.consentRequestId}/reject`, - ) + .post(`/api/auth/v1/sessions/${authSession.id}/reject`) .expect(200); expect(response.body).toEqual({ @@ -505,7 +499,7 @@ describe('OidcRouter', () => { scope: 'openid', }); - const consentRequest = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -534,10 +528,8 @@ describe('OidcRouter', () => { }); const approvalResponse = await request(server) - .post( - `/api/auth/v1/consent/${consentRequest.consentRequestId}/approve`, - ) - .set('Authorization', 'Bearer test-token') + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) .expect(200); const redirectUrl = new URL(approvalResponse.body.redirectUrl); @@ -566,7 +558,7 @@ describe('OidcRouter', () => { expect(tokenIssuer.issueToken).toHaveBeenCalledWith({ claims: { - sub: 'user:default/test-user', + sub: MOCK_USER_ENTITY_REF, }, }); }); @@ -602,7 +594,7 @@ describe('OidcRouter', () => { 'test-code-verifier-123456789012345678901234567890123456789012345'; const codeChallenge = codeVerifier; - const consentRequest = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -633,10 +625,8 @@ describe('OidcRouter', () => { }); const approvalResponse = await request(server) - .post( - `/api/auth/v1/consent/${consentRequest.consentRequestId}/approve`, - ) - .set('Authorization', 'Bearer test-token') + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) .expect(200); const redirectUrl = new URL(approvalResponse.body.redirectUrl); @@ -666,7 +656,7 @@ describe('OidcRouter', () => { expect(tokenIssuer.issueToken).toHaveBeenCalledWith({ claims: { - sub: 'user:default/test-user-pkce', + sub: MOCK_USER_ENTITY_REF, }, }); }); @@ -694,7 +684,7 @@ describe('OidcRouter', () => { scope: 'openid', }); - const consentRequest = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -722,10 +712,8 @@ describe('OidcRouter', () => { }); const approvalResponse = await request(server) - .post( - `/api/auth/v1/consent/${consentRequest.consentRequestId}/approve`, - ) - .set('Authorization', 'Bearer test-token') + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) .expect(200); const redirectUrl = new URL(approvalResponse.body.redirectUrl); @@ -833,7 +821,7 @@ describe('OidcRouter', () => { .update(codeVerifier) .digest('base64url'); - const consentRequest = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -864,10 +852,8 @@ describe('OidcRouter', () => { }); const approvalResponse = await request(server) - .post( - `/api/auth/v1/consent/${consentRequest.consentRequestId}/approve`, - ) - .set('Authorization', 'Bearer test-token') + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) .expect(200); const redirectUrl = new URL(approvalResponse.body.redirectUrl); diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index c7bb42e1f5..c07ffd5adb 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -16,7 +16,11 @@ import Router from 'express-promise-router'; import { OidcService } from './OidcService'; import { AuthenticationError, isError } from '@backstage/errors'; -import { AuthService, LoggerService } from '@backstage/backend-plugin-api'; +import { + AuthService, + HttpAuthService, + LoggerService, +} from '@backstage/backend-plugin-api'; import { TokenIssuer } from '../identity/types'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; import { OidcDatabase } from '../database/OidcDatabase'; @@ -28,6 +32,7 @@ export class OidcRouter { private readonly logger: LoggerService, private readonly auth: AuthService, private readonly appUrl: string, + private readonly httpAuth: HttpAuthService, ) {} static create(options: { @@ -38,12 +43,14 @@ export class OidcRouter { logger: LoggerService; userInfo: UserInfoDatabase; oidc: OidcDatabase; + httpAuth: HttpAuthService; }) { return new OidcRouter( OidcService.create(options), options.logger, options.auth, options.appUrl, + options.httpAuth, ); } @@ -70,7 +77,7 @@ export class OidcRouter { // Authorization endpoint // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest // Handles the initial authorization request from the client, validates parameters, - // and redirects to the consent page for user approval + // and redirects to the Authorization Session page for user approval router.get('/v1/authorize', async (req, res) => { // todo(blam): maybe add zod types for validating input const { @@ -94,7 +101,7 @@ export class OidcRouter { } try { - const result = await this.oidc.createConsentRequest({ + const result = await this.oidc.createAuthorizationSession({ clientId: clientId as string, redirectUri: redirectUri as string, responseType: responseType as string, @@ -107,12 +114,13 @@ export class OidcRouter { // todo(blam): maybe this URL could be overridable by config if // the plugin is mounted somewhere else? - const consentUrl = new URL( - `/auth/consent/${result.consentRequestId}`, + // support slashes in baseUrl? + const authSessionRedirectUrl = new URL( + `/auth/sessions/${result.id}`, this.appUrl, ); - return res.redirect(consentUrl.toString()); + return res.redirect(authSessionRedirectUrl.toString()); } catch (error) { const errorParams = new URLSearchParams(); errorParams.append( @@ -133,77 +141,69 @@ export class OidcRouter { } }); - // Consent request details endpoint - // Returns consent request details for the frontend consent page - router.get('/v1/consent/:consentId', async (req, res) => { - const { consentId } = req.params; + // Authorization Session request details endpoint + // Returns Authorization Session request details for the frontned + router.get('/v1/sessions/:sessionId', async (req, res) => { + const { sessionId } = req.params; - if (!consentId) { + if (!sessionId) { return res.status(400).json({ error: 'invalid_request', - error_description: 'Missing consent ID', + error_description: 'Missing Authorization Session ID', }); } try { - const consentRequest = await this.oidc.getConsentRequest({ - consentRequestId: consentId, + const session = await this.oidc.getAuthorizationSession({ + sessionId, }); return res.json({ - id: consentRequest.id, - clientName: consentRequest.clientName, - scope: consentRequest.scope, - redirectUri: consentRequest.redirectUri, + id: session.id, + clientName: session.clientName, + scope: session.scope, + redirectUri: session.redirectUri, }); } catch (error) { this.logger.error( - `Failed to get consent request: ${ + `Failed to get authorization session: ${ isError(error) ? error.message : 'Unknown error' }`, error, ); return res.status(404).json({ error: 'not_found', - error_description: 'Consent request not found or expired', + error_description: 'Authorization session not found or expired', }); } }); - // Consent approval endpoint - // Handles user approval of consent requests and generates authorization codes - router.post('/v1/consent/:consentId/approve', async (req, res) => { - const { consentId } = req.params; + // Authorization Session approval endpoint + // Handles user approval of Authorization Session requests and generates authorization codes + router.post('/v1/sessions/:sessionId/approve', async (req, res) => { + const { sessionId } = req.params; - if (!consentId) { + if (!sessionId) { return res.status(400).json({ error: 'invalid_request', - error_description: 'Missing consent ID', + error_description: 'Missing authorization session ID', }); } try { - const authHeader = req.headers.authorization; - if (!authHeader?.startsWith('Bearer ')) { - return res.status(401).json({ - error: 'unauthorized', - error_description: 'Bearer token required', - }); - } + const httpCredentials = await this.httpAuth.credentials(req); - const token = authHeader.substring(7); - const credentials = await this.auth.authenticate(token); - if (!this.auth.isPrincipal(credentials, 'user')) { + if (!this.auth.isPrincipal(httpCredentials, 'user')) { return res.status(401).json({ error: 'unauthorized', error_description: 'Authentication required', }); } - const userEntityRef = credentials.principal.userEntityRef; + const userEntityRef = httpCredentials.principal.userEntityRef; - const result = await this.oidc.approveConsentRequest({ - consentRequestId: consentId, + const result = await this.oidc.approveAuthorizationSession({ + sessionId, userEntityRef, }); @@ -211,8 +211,9 @@ export class OidcRouter { redirectUrl: result.redirectUrl, }); } catch (error) { + console.log(error); this.logger.error( - `Failed to approve consent: ${ + `Failed to approve authorization session: ${ isError(error) ? error.message : 'Unknown error' }`, error, @@ -224,33 +225,33 @@ export class OidcRouter { } }); - // Consent rejection endpoint - // Handles user rejection of consent requests and redirects with error - router.post('/v1/consent/:consentId/reject', async (req, res) => { - const { consentId } = req.params; + // Authorization Session rejection endpoint + // Handles user rejection of Authorization Session requests and redirects with error + router.post('/v1/sessions/:sessionId/reject', async (req, res) => { + const { sessionId } = req.params; - if (!consentId) { + if (!sessionId) { return res.status(400).json({ error: 'invalid_request', - error_description: 'Missing consent ID', + error_description: 'Missing authorization session ID', }); } try { - const consentRequest = await this.oidc.getConsentRequest({ - consentRequestId: consentId, + const session = await this.oidc.getAuthorizationSession({ + sessionId, }); - await this.oidc.deleteConsentRequest({ consentRequestId: consentId }); + await this.oidc.rejectAuthorizationSession({ sessionId }); const errorParams = new URLSearchParams(); errorParams.append('error', 'access_denied'); errorParams.append('error_description', 'User denied the request'); - if (consentRequest.state) { - errorParams.append('state', consentRequest.state); + if (session.state) { + errorParams.append('state', session.state); } - const redirectUrl = new URL(consentRequest.redirectUri); + const redirectUrl = new URL(session.redirectUri); redirectUrl.search = errorParams.toString(); return res.json({ @@ -258,7 +259,10 @@ export class OidcRouter { }); } catch (error) { const description = isError(error) ? error.message : 'Unknown error'; - this.logger.error(`Failed to reject consent: ${description}`, error); + this.logger.error( + `Failed to reject authorization session: ${description}`, + error, + ); return res.status(400).json({ error: 'invalid_request', diff --git a/plugins/auth-backend/src/service/OidcService.test.ts b/plugins/auth-backend/src/service/OidcService.test.ts index fa4c0a12ac..5067cde458 100644 --- a/plugins/auth-backend/src/service/OidcService.test.ts +++ b/plugins/auth-backend/src/service/OidcService.test.ts @@ -233,8 +233,8 @@ describe('OidcService', () => { }); }); - describe('createConsentRequest', () => { - it('should create a consent request for valid client', async () => { + describe('createAuthorizationSession', () => { + it('should create a authorization session for valid client', async () => { const { service } = await createOidcService(databaseId); const client = await service.registerClient({ @@ -242,7 +242,7 @@ describe('OidcService', () => { redirectUris: ['https://example.com/callback'], }); - const consent = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -250,8 +250,8 @@ describe('OidcService', () => { state: 'test-state', }); - expect(consent).toEqual({ - consentRequestId: expect.any(String), + expect(authSession).toEqual({ + id: expect.any(String), clientName: 'Test Client', scope: 'openid', redirectUri: 'https://example.com/callback', @@ -262,7 +262,7 @@ describe('OidcService', () => { const { service } = await createOidcService(databaseId); await expect( - service.createConsentRequest({ + service.createAuthorizationSession({ clientId: 'invalid-client', redirectUri: 'https://example.com/callback', responseType: 'code', @@ -279,7 +279,7 @@ describe('OidcService', () => { }); await expect( - service.createConsentRequest({ + service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://invalid.com/callback', responseType: 'code', @@ -296,7 +296,7 @@ describe('OidcService', () => { }); await expect( - service.createConsentRequest({ + service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'token', @@ -312,7 +312,7 @@ describe('OidcService', () => { redirectUris: ['https://example.com/callback'], }); - const consent = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -320,7 +320,7 @@ describe('OidcService', () => { codeChallengeMethod: 'S256', }); - expect(consent.consentRequestId).toBeDefined(); + expect(authSession.id).toBeDefined(); }); it('should throw error for invalid PKCE method', async () => { @@ -332,7 +332,7 @@ describe('OidcService', () => { }); await expect( - service.createConsentRequest({ + service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -343,8 +343,8 @@ describe('OidcService', () => { }); }); - describe('approveConsentRequest', () => { - it('should approve a valid consent request', async () => { + describe('approveAuthorizationSession', () => { + it('should approve a valid authorization session', async () => { const { service } = await createOidcService(databaseId); const client = await service.registerClient({ @@ -352,15 +352,15 @@ describe('OidcService', () => { redirectUris: ['https://example.com/callback'], }); - const consent = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', state: 'test-state', }); - const result = await service.approveConsentRequest({ - consentRequestId: consent.consentRequestId, + const result = await service.approveAuthorizationSession({ + sessionId: authSession.id, userEntityRef: 'user:default/test', }); @@ -369,20 +369,20 @@ describe('OidcService', () => { ); }); - it('should throw error for invalid consent request', async () => { + it('should throw error for invalid authorization session', async () => { const { service } = await createOidcService(databaseId); await expect( - service.approveConsentRequest({ - consentRequestId: 'invalid-consent', + service.approveAuthorizationSession({ + sessionId: 'invalid-session', userEntityRef: 'user:default/test', }), - ).rejects.toThrow('Invalid consent request'); + ).rejects.toThrow('Invalid authorization session'); }); }); - describe('getConsentRequest', () => { - it('should return consent request details', async () => { + describe('getAuthorizationSession', () => { + it('should return authorization session details', async () => { const { service } = await createOidcService(databaseId); const client = await service.registerClient({ @@ -390,7 +390,7 @@ describe('OidcService', () => { redirectUris: ['https://example.com/callback'], }); - const consent = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -398,13 +398,13 @@ describe('OidcService', () => { state: 'test-state', }); - const details = await service.getConsentRequest({ - consentRequestId: consent.consentRequestId, + const details = await service.getAuthorizationSession({ + sessionId: authSession.id, }); expect(details).toEqual( expect.objectContaining({ - id: consent.consentRequestId, + id: authSession.id, clientId: client.clientId, clientName: 'Test Client', redirectUri: 'https://example.com/callback', @@ -416,8 +416,8 @@ describe('OidcService', () => { }); }); - describe('deleteConsentRequest', () => { - it('should delete a consent request', async () => { + describe('rejectAuthorizationSession', () => { + it('should delete a authorization session', async () => { const { service } = await createOidcService(databaseId); const client = await service.registerClient({ @@ -425,31 +425,35 @@ describe('OidcService', () => { redirectUris: ['https://example.com/callback'], }); - const consent = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', }); - await service.deleteConsentRequest({ - consentRequestId: consent.consentRequestId, + await service.rejectAuthorizationSession({ + sessionId: authSession.id, }); await expect( - service.getConsentRequest({ - consentRequestId: consent.consentRequestId, + service.getAuthorizationSession({ + sessionId: authSession.id, }), - ).rejects.toThrow('Invalid consent request'); + ).resolves.toEqual( + expect.objectContaining({ + status: 'rejected', + }), + ); }); - it('should handle deleting non-existent consent request', async () => { + it('should throw error for invalid authorization session', async () => { const { service } = await createOidcService(databaseId); await expect( - service.deleteConsentRequest({ - consentRequestId: 'non-existent', + service.rejectAuthorizationSession({ + sessionId: 'invalid-session', }), - ).resolves.not.toThrow(); + ).rejects.toThrow('Invalid authorization session'); }); }); diff --git a/plugins/auth-backend/src/service/OidcService.ts b/plugins/auth-backend/src/service/OidcService.ts index 4214f53a8c..e89dfeac6f 100644 --- a/plugins/auth-backend/src/service/OidcService.ts +++ b/plugins/auth-backend/src/service/OidcService.ts @@ -126,7 +126,7 @@ export class OidcService { }); } - public async createConsentRequest(opts: { + public async createAuthorizationSession(opts: { clientId: string; redirectUri: string; responseType: string; @@ -185,43 +185,24 @@ export class OidcService { expiresAt: sessionExpiresAt, }); - const consentRequestId = crypto.randomUUID(); - const consentExpiresAt = DateTime.now().plus({ minutes: 30 }).toISO(); - - await this.oidc.createConsentRequest({ - id: consentRequestId, - sessionId, - expiresAt: consentExpiresAt, - }); - return { - consentRequestId, + id: sessionId, clientName: client.clientName, scope, redirectUri, }; } - public async approveConsentRequest(opts: { - consentRequestId: string; + public async approveAuthorizationSession(opts: { + sessionId: string; userEntityRef: string; }) { - const { consentRequestId, userEntityRef } = opts; - - const consentRequest = await this.oidc.getConsentRequest({ - id: consentRequestId, - }); - if (!consentRequest) { - throw new InputError('Invalid consent request'); - } - - if (DateTime.fromISO(consentRequest.expiresAt) < DateTime.now()) { - throw new InputError('Consent request expired'); - } + const { sessionId, userEntityRef } = opts; const session = await this.oidc.getAuthorizationSession({ - id: consentRequest.sessionId, + id: sessionId, }); + if (!session) { throw new InputError('Invalid authorization session'); } @@ -245,9 +226,8 @@ export class OidcService { expiresAt: codeExpiresAt, }); - await this.oidc.deleteConsentRequest({ id: consentRequestId }); - const redirectUrl = new URL(session.redirectUri); + redirectUrl.searchParams.append('code', authorizationCode); if (session.state) { redirectUrl.searchParams.append('state', session.state); @@ -258,33 +238,26 @@ export class OidcService { }; } - public async getConsentRequest(opts: { consentRequestId: string }) { - const consentRequest = await this.oidc.getConsentRequest({ - id: opts.consentRequestId, - }); - if (!consentRequest) { - throw new InputError('Invalid consent request'); - } - - if (DateTime.fromISO(consentRequest.expiresAt) < DateTime.now()) { - throw new InputError('Consent request expired'); - } - + public async getAuthorizationSession(opts: { sessionId: string }) { const session = await this.oidc.getAuthorizationSession({ - id: consentRequest.sessionId, + id: opts.sessionId, }); if (!session) { throw new InputError('Invalid authorization session'); } + if (DateTime.fromISO(session.expiresAt) < DateTime.now()) { + throw new InputError('Authorization session expired'); + } + const client = await this.oidc.getClient({ clientId: session.clientId }); if (!client) { throw new InputError('Invalid client_id'); } return { - id: consentRequest.id, + id: session.id, clientId: session.clientId, clientName: client.clientName, redirectUri: session.redirectUri, @@ -294,24 +267,28 @@ export class OidcService { codeChallenge: session.codeChallenge, codeChallengeMethod: session.codeChallengeMethod, nonce: session.nonce, - expiresAt: consentRequest.expiresAt, + expiresAt: session.expiresAt, + status: session.status, }; } - public async deleteConsentRequest(opts: { consentRequestId: string }) { - const consentRequest = await this.oidc.getConsentRequest({ - id: opts.consentRequestId, + public async rejectAuthorizationSession(opts: { sessionId: string }) { + const session = await this.oidc.getAuthorizationSession({ + id: opts.sessionId, }); - if (!consentRequest) { - return; + + if (!session) { + throw new InputError('Invalid authorization session'); + } + + if (DateTime.fromISO(session.expiresAt) < DateTime.now()) { + throw new InputError('Authorization session expired'); } await this.oidc.updateAuthorizationSession({ - id: consentRequest.sessionId, + id: session.id, status: 'rejected', }); - - await this.oidc.deleteConsentRequest({ id: opts.consentRequestId }); } public async authorize(opts: { @@ -487,15 +464,6 @@ export class OidcService { used: true, }); - const accessTokenId = crypto.randomUUID(); - const expiresAt = DateTime.now().plus({ hours: 1 }).toISO(); - - await this.oidc.createAccessToken({ - tokenId: accessTokenId, - sessionId: session.id, - expiresAt, - }); - const { token } = await this.tokenIssuer.issueToken({ claims: { sub: session.userEntityRef, diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 28f218c749..1f5fea7cb5 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -21,6 +21,7 @@ import { AuthService, DatabaseService, DiscoveryService, + HttpAuthService, LoggerService, RootConfigService, } from '@backstage/backend-plugin-api'; @@ -52,6 +53,7 @@ interface RouterOptions { providerFactories?: ProviderFactories; catalog: CatalogService; ownershipResolver?: AuthOwnershipResolver; + httpAuth: HttpAuthService; } export async function createRouter( @@ -64,6 +66,7 @@ export async function createRouter( database: db, tokenFactoryAlgorithm, providerFactories = {}, + httpAuth, } = options; const router = Router(); @@ -158,6 +161,7 @@ export async function createRouter( userInfo, oidc, logger, + httpAuth, }); router.use(oidcRouter.getRouter()); From 1122bb29acd56322a5c7bd51c2175185a3e8e628 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 7 Jul 2025 14:02:02 +0200 Subject: [PATCH 107/233] feat: add sqlreports and fixing up Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- plugins/auth-backend/report.sql.md | 53 +++++++++++++++++++ plugins/auth-backend/src/authPlugin.ts | 3 ++ .../src/service/OidcRouter.test.ts | 43 +++++++++------ .../auth-backend/src/service/OidcRouter.ts | 1 - .../src/service/OidcService.test.ts | 1 - 5 files changed, 82 insertions(+), 19 deletions(-) diff --git a/plugins/auth-backend/report.sql.md b/plugins/auth-backend/report.sql.md index b135414af6..7622a5750e 100644 --- a/plugins/auth-backend/report.sql.md +++ b/plugins/auth-backend/report.sql.md @@ -5,6 +5,59 @@ > [!WARNING] > Failed to migrate down from '20220321100910_timestamptz_again.js' +## Table `oauth_authorization_sessions` + +| Column | Type | Nullable | Max Length | Default | +| ----------------------- | -------------------------- | -------- | ---------- | ----------------- | +| `client_id` | `character varying` | false | 255 | - | +| `code_challenge` | `character varying` | true | 255 | - | +| `code_challenge_method` | `character varying` | true | 255 | - | +| `expires_at` | `timestamp with time zone` | false | - | - | +| `id` | `character varying` | false | 255 | - | +| `nonce` | `character varying` | true | 255 | - | +| `redirect_uri` | `text` | false | - | - | +| `response_type` | `character varying` | false | 255 | - | +| `scope` | `text` | true | - | - | +| `state` | `character varying` | true | 255 | - | +| `status` | `text` | true | - | `'pending'::text` | +| `user_entity_ref` | `character varying` | true | 255 | - | + +### Indices + +- `oauth_authorization_sessions_client_id_user_entity_ref_index` (`client_id`, `user_entity_ref`) +- `oauth_authorization_sessions_pkey` (`id`) unique primary +- `oauth_authorization_sessions_status_expires_at_index` (`status`, `expires_at`) + +## Table `oidc_authorization_codes` + +| Column | Type | Nullable | Max Length | Default | +| ------------ | -------------------------- | -------- | ---------- | ------- | +| `code` | `character varying` | false | 255 | - | +| `expires_at` | `timestamp with time zone` | false | - | - | +| `session_id` | `character varying` | false | 255 | - | +| `used` | `boolean` | true | - | `false` | + +### Indices + +- `oidc_authorization_codes_pkey` (`code`) unique primary + +## Table `oidc_clients` + +| Column | Type | Nullable | Max Length | Default | +| ---------------- | ------------------- | -------- | ---------- | ------- | +| `client_id` | `character varying` | false | 255 | - | +| `client_name` | `character varying` | false | 255 | - | +| `client_secret` | `character varying` | false | 255 | - | +| `grant_types` | `text` | false | - | - | +| `metadata` | `text` | true | - | - | +| `redirect_uris` | `text` | false | - | - | +| `response_types` | `text` | false | - | - | +| `scope` | `text` | true | - | - | + +### Indices + +- `oidc_clients_pkey` (`client_id`) unique primary + ## Table `sessions` | Column | Type | Nullable | Max Length | Default | diff --git a/plugins/auth-backend/src/authPlugin.ts b/plugins/auth-backend/src/authPlugin.ts index f3877d48cd..025d72fcb7 100644 --- a/plugins/auth-backend/src/authPlugin.ts +++ b/plugins/auth-backend/src/authPlugin.ts @@ -66,6 +66,7 @@ export const authPlugin = createBackendPlugin({ database: coreServices.database, discovery: coreServices.discovery, auth: coreServices.auth, + httpAuth: coreServices.httpAuth, catalog: catalogServiceRef, }, async init({ @@ -75,6 +76,7 @@ export const authPlugin = createBackendPlugin({ database, discovery, auth, + httpAuth, catalog, }) { const router = await createRouter({ @@ -86,6 +88,7 @@ export const authPlugin = createBackendPlugin({ catalog, providerFactories: Object.fromEntries(providers), ownershipResolver, + httpAuth, }); httpRouter.addAuthPolicy({ path: '/', diff --git a/plugins/auth-backend/src/service/OidcRouter.test.ts b/plugins/auth-backend/src/service/OidcRouter.test.ts index 579c96470f..fe5182251f 100644 --- a/plugins/auth-backend/src/service/OidcRouter.test.ts +++ b/plugins/auth-backend/src/service/OidcRouter.test.ts @@ -24,7 +24,6 @@ import { startTestBackend, TestDatabases, TestDatabaseId, - mockCredentials, } from '@backstage/backend-test-utils'; import request from 'supertest'; import crypto from 'crypto'; @@ -68,6 +67,7 @@ describe('OidcRouter', () => { } as unknown as jest.Mocked; const mockAuth = mockServices.auth.mock(); + const mockHttpAuth = mockServices.httpAuth.mock(); const oidcService = OidcService.create({ auth: mockAuth, @@ -85,14 +85,13 @@ describe('OidcRouter', () => { logger: mockServices.logger.mock(), userInfo: userInfoDatabase, oidc: oidcDatabase, - httpAuth: mockServices.httpAuth({ - defaultCredentials: mockCredentials.user(), - }), + httpAuth: mockHttpAuth, }); return { router: oidcRouter, mocks: { + httpAuth: mockHttpAuth, auth: mockAuth, oidc: oidcDatabase, userInfo: userInfoDatabase, @@ -138,7 +137,6 @@ describe('OidcRouter', () => { ], }); - auth.authenticate.mockResolvedValueOnce({} as any); auth.isPrincipal.mockReturnValueOnce(true); const response = await request(server) @@ -194,7 +192,6 @@ describe('OidcRouter', () => { ], }); - auth.authenticate.mockResolvedValueOnce({} as any); auth.isPrincipal.mockReturnValueOnce(true); const response = await request(server) @@ -361,9 +358,9 @@ describe('OidcRouter', () => { }); }); - it('should approve consent request', async () => { + it('should approve authorization session', async () => { const { - mocks: { auth, service }, + mocks: { auth, service, httpAuth }, router, } = await createRouter(databaseId); @@ -403,6 +400,14 @@ describe('OidcRouter', () => { ], }); + httpAuth.credentials.mockResolvedValueOnce({ + principal: { + type: 'user', + userEntityRef: 'user:default/test-user', + }, + $$type: '@backstage/BackstageCredentials', + }); + auth.isPrincipal.mockReturnValueOnce(true); const response = await request(server) @@ -474,17 +479,18 @@ describe('OidcRouter', () => { describe('token exchange', () => { it('should exchange authorization code for tokens', async () => { const { - mocks: { auth, service, tokenIssuer }, + mocks: { auth, service, tokenIssuer, httpAuth }, router, } = await createRouter(databaseId); - auth.authenticate.mockResolvedValueOnce({ + httpAuth.credentials.mockResolvedValueOnce({ principal: { type: 'user', userEntityRef: 'user:default/test-user', }, $$type: '@backstage/BackstageCredentials', }); + auth.isPrincipal.mockReturnValueOnce(true); tokenIssuer.issueToken.mockResolvedValue({ @@ -565,7 +571,7 @@ describe('OidcRouter', () => { it('should exchange authorization code for tokens with PKCE', async () => { const { - mocks: { auth, service, tokenIssuer }, + mocks: { auth, service, tokenIssuer, httpAuth }, router, } = await createRouter(databaseId); @@ -573,13 +579,14 @@ describe('OidcRouter', () => { token: 'mock-access-token-pkce', }); - auth.authenticate.mockResolvedValueOnce({ + httpAuth.credentials.mockResolvedValueOnce({ principal: { type: 'user', userEntityRef: 'user:default/test-user-pkce', }, $$type: '@backstage/BackstageCredentials', }); + auth.isPrincipal.mockReturnValueOnce(true); const client = await service.registerClient({ @@ -656,24 +663,25 @@ describe('OidcRouter', () => { expect(tokenIssuer.issueToken).toHaveBeenCalledWith({ claims: { - sub: MOCK_USER_ENTITY_REF, + sub: 'user:default/test-user-pkce', }, }); }); it('should reject token exchange with invalid client credentials', async () => { const { - mocks: { auth, service }, + mocks: { auth, service, httpAuth }, router, } = await createRouter(databaseId); - auth.authenticate.mockResolvedValueOnce({ + httpAuth.credentials.mockResolvedValueOnce({ principal: { type: 'user', userEntityRef: 'user:default/test-user', }, $$type: '@backstage/BackstageCredentials', }); + auth.isPrincipal.mockReturnValueOnce(true); const client = await service.registerClient({ @@ -789,7 +797,7 @@ describe('OidcRouter', () => { it('should exchange authorization code for tokens with PKCE S256', async () => { const { - mocks: { auth, service, tokenIssuer }, + mocks: { auth, service, tokenIssuer, httpAuth }, router, } = await createRouter(databaseId); @@ -797,13 +805,14 @@ describe('OidcRouter', () => { token: 'mock-access-token-s256', }); - auth.authenticate.mockResolvedValueOnce({ + httpAuth.credentials.mockResolvedValueOnce({ principal: { type: 'user', userEntityRef: 'user:default/test-user-s256', }, $$type: '@backstage/BackstageCredentials', }); + auth.isPrincipal.mockReturnValueOnce(true); const client = await service.registerClient({ diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index c07ffd5adb..a0acc57cec 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -211,7 +211,6 @@ export class OidcRouter { redirectUrl: result.redirectUrl, }); } catch (error) { - console.log(error); this.logger.error( `Failed to approve authorization session: ${ isError(error) ? error.message : 'Unknown error' diff --git a/plugins/auth-backend/src/service/OidcService.test.ts b/plugins/auth-backend/src/service/OidcService.test.ts index 5067cde458..4820559b01 100644 --- a/plugins/auth-backend/src/service/OidcService.test.ts +++ b/plugins/auth-backend/src/service/OidcService.test.ts @@ -28,7 +28,6 @@ import { import { AuthDatabase } from '../database/AuthDatabase'; import { OidcDatabase } from '../database/OidcDatabase'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; -import { InputError, AuthenticationError } from '@backstage/errors'; import crypto from 'crypto'; import { AnyJWK, TokenIssuer } from '../identity/types'; From 75e0cdbc0b9069d7fa7483312fe49338ec36f251 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Thu, 10 Jul 2025 07:52:54 +0200 Subject: [PATCH 108/233] chore: when session has been accepted or approved it should return not found from apio Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- .../auth-backend/src/service/OidcRouter.ts | 14 +- .../src/service/OidcService.test.ts | 165 +++++++++++++++++- .../auth-backend/src/service/OidcService.ts | 27 ++- 3 files changed, 186 insertions(+), 20 deletions(-) diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index a0acc57cec..60c0e42f92 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -165,15 +165,14 @@ export class OidcRouter { redirectUri: session.redirectUri, }); } catch (error) { + const description = isError(error) ? error.message : 'Unknown error'; this.logger.error( - `Failed to get authorization session: ${ - isError(error) ? error.message : 'Unknown error' - }`, + `Failed to get authorization session: ${description}`, error, ); return res.status(404).json({ error: 'not_found', - error_description: 'Authorization session not found or expired', + error_description: description, }); } }); @@ -211,15 +210,14 @@ export class OidcRouter { redirectUrl: result.redirectUrl, }); } catch (error) { + const description = isError(error) ? error.message : 'Unknown error'; this.logger.error( - `Failed to approve authorization session: ${ - isError(error) ? error.message : 'Unknown error' - }`, + `Failed to approve authorization session: ${description}`, error, ); return res.status(400).json({ error: 'invalid_request', - error_description: isError(error) ? error.message : 'Unknown error', + error_description: description, }); } }); diff --git a/plugins/auth-backend/src/service/OidcService.test.ts b/plugins/auth-backend/src/service/OidcService.test.ts index 4820559b01..017d3ab481 100644 --- a/plugins/auth-backend/src/service/OidcService.test.ts +++ b/plugins/auth-backend/src/service/OidcService.test.ts @@ -378,6 +378,59 @@ describe('OidcService', () => { }), ).rejects.toThrow('Invalid authorization session'); }); + + it('should throw error when trying to approve an already approved session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + await expect( + service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); + + it('should throw error when trying to approve an already rejected session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.rejectAuthorizationSession({ + sessionId: authSession.id, + }); + + await expect( + service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); }); describe('getAuthorizationSession', () => { @@ -413,10 +466,34 @@ describe('OidcService', () => { }), ); }); - }); - describe('rejectAuthorizationSession', () => { - it('should delete a authorization session', async () => { + it('should throw error when trying to get an already approved session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + await expect( + service.getAuthorizationSession({ + sessionId: authSession.id, + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); + + it('should throw error when trying to get an already rejected session', async () => { const { service } = await createOidcService(databaseId); const client = await service.registerClient({ @@ -438,11 +515,34 @@ describe('OidcService', () => { service.getAuthorizationSession({ sessionId: authSession.id, }), - ).resolves.toEqual( - expect.objectContaining({ - status: 'rejected', + ).rejects.toThrow('Authorization session not found or expired'); + }); + }); + + describe('rejectAuthorizationSession', () => { + it('should reject a authorization session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.rejectAuthorizationSession({ + sessionId: authSession.id, + }); + + await expect( + service.getAuthorizationSession({ + sessionId: authSession.id, }), - ); + ).rejects.toThrow('Authorization session not found or expired'); }); it('should throw error for invalid authorization session', async () => { @@ -454,6 +554,57 @@ describe('OidcService', () => { }), ).rejects.toThrow('Invalid authorization session'); }); + + it('should throw error when trying to reject an already approved session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + + await expect( + service.rejectAuthorizationSession({ + sessionId: authSession.id, + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); + + it('should throw error when trying to reject an already rejected session', async () => { + const { service } = await createOidcService(databaseId); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://example.com/callback'], + }); + + const authSession = await service.createAuthorizationSession({ + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + }); + + await service.rejectAuthorizationSession({ + sessionId: authSession.id, + }); + + await expect( + service.rejectAuthorizationSession({ + sessionId: authSession.id, + }), + ).rejects.toThrow('Authorization session not found or expired'); + }); }); describe('authorize', () => { diff --git a/plugins/auth-backend/src/service/OidcService.ts b/plugins/auth-backend/src/service/OidcService.ts index e89dfeac6f..7809d2fd00 100644 --- a/plugins/auth-backend/src/service/OidcService.ts +++ b/plugins/auth-backend/src/service/OidcService.ts @@ -16,7 +16,11 @@ import { AuthService } from '@backstage/backend-plugin-api'; import { TokenIssuer } from '../identity/types'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; -import { InputError, AuthenticationError } from '@backstage/errors'; +import { + InputError, + AuthenticationError, + NotFoundError, +} from '@backstage/errors'; import { decodeJwt } from 'jose'; import crypto from 'crypto'; import { OidcDatabase } from '../database/OidcDatabase'; @@ -204,13 +208,17 @@ export class OidcService { }); if (!session) { - throw new InputError('Invalid authorization session'); + throw new NotFoundError('Invalid authorization session'); } if (DateTime.fromISO(session.expiresAt) < DateTime.now()) { throw new InputError('Authorization session expired'); } + if (session.status !== 'pending') { + throw new NotFoundError('Authorization session not found or expired'); + } + await this.oidc.updateAuthorizationSession({ id: session.id, userEntityRef, @@ -244,13 +252,17 @@ export class OidcService { }); if (!session) { - throw new InputError('Invalid authorization session'); + throw new NotFoundError('Invalid authorization session'); } if (DateTime.fromISO(session.expiresAt) < DateTime.now()) { throw new InputError('Authorization session expired'); } + if (session.status !== 'pending') { + throw new NotFoundError('Authorization session not found or expired'); + } + const client = await this.oidc.getClient({ clientId: session.clientId }); if (!client) { throw new InputError('Invalid client_id'); @@ -278,13 +290,17 @@ export class OidcService { }); if (!session) { - throw new InputError('Invalid authorization session'); + throw new NotFoundError('Invalid authorization session'); } if (DateTime.fromISO(session.expiresAt) < DateTime.now()) { throw new InputError('Authorization session expired'); } + if (session.status !== 'pending') { + throw new NotFoundError('Authorization session not found or expired'); + } + await this.oidc.updateAuthorizationSession({ id: session.id, status: 'rejected', @@ -424,8 +440,9 @@ export class OidcService { const session = await this.oidc.getAuthorizationSession({ id: authCode.sessionId, }); + if (!session) { - throw new AuthenticationError('Invalid authorization session'); + throw new NotFoundError('Invalid authorization session'); } if (session.clientId !== clientId) { throw new AuthenticationError('Client ID mismatch'); From 1d47bf37f59dc8927ccb3f103217e4d05f2ce035 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 21 Jul 2025 16:21:28 +0200 Subject: [PATCH 109/233] chore: add changesets Signed-off-by: benjdlambert --- .changeset/eleven-doors-down.md | 5 +++++ .changeset/eleven-doors-own.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/eleven-doors-down.md create mode 100644 .changeset/eleven-doors-own.md diff --git a/.changeset/eleven-doors-down.md b/.changeset/eleven-doors-down.md new file mode 100644 index 0000000000..47cb99dd03 --- /dev/null +++ b/.changeset/eleven-doors-down.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-mcp-actions-backend': patch +--- + +Proxy `/.well-known/oauth-authorization-server` to `/.well-known/openid-configuration` on `auth-backend` diff --git a/.changeset/eleven-doors-own.md b/.changeset/eleven-doors-own.md new file mode 100644 index 0000000000..1308aab3eb --- /dev/null +++ b/.changeset/eleven-doors-own.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Implementing Dynamic Client Registration with the OIDC server From fa4cd1d47f7f91f2c1ce41900d705dc7cba81c69 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 8 Sep 2025 10:21:58 +0200 Subject: [PATCH 110/233] frontend-defaults: new display for app startup errors Signed-off-by: Patrik Oldsberg --- packages/frontend-defaults/report.api.md | 12 ++ packages/frontend-defaults/src/createApp.tsx | 6 + packages/frontend-defaults/src/index.ts | 1 + .../src/maybeCreateErrorPage.tsx | 116 ++++++++++++++++++ 4 files changed, 135 insertions(+) create mode 100644 packages/frontend-defaults/src/maybeCreateErrorPage.tsx diff --git a/packages/frontend-defaults/report.api.md b/packages/frontend-defaults/report.api.md index 6fd2536251..75392d2dc1 100644 --- a/packages/frontend-defaults/report.api.md +++ b/packages/frontend-defaults/report.api.md @@ -3,6 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AppError } from '@backstage/frontend-app-api'; +import { AppErrorTypes } from '@backstage/frontend-app-api'; import { Config } from '@backstage/config'; import { ConfigApi } from '@backstage/frontend-plugin-api'; import { CreateAppRouteBinder } from '@backstage/frontend-app-api'; @@ -45,6 +47,16 @@ export function discoverAvailableFeatures(config: Config): { features: (FrontendFeature | FrontendFeatureLoader)[]; }; +// @public +export function maybeCreateErrorPage( + app: { + errors?: AppError[]; + }, + options?: { + warningCodes?: Array; + }, +): JSX_2.Element | undefined; + // @public (undocumented) export function resolveAsyncFeatures(options: { config: Config; diff --git a/packages/frontend-defaults/src/createApp.tsx b/packages/frontend-defaults/src/createApp.tsx index 2c8a80791f..4ff67f6d5a 100644 --- a/packages/frontend-defaults/src/createApp.tsx +++ b/packages/frontend-defaults/src/createApp.tsx @@ -36,6 +36,7 @@ import { import appPlugin from '@backstage/plugin-app'; import { discoverAvailableFeatures } from './discovery'; import { resolveAsyncFeatures } from './resolution'; +import { maybeCreateErrorPage } from './maybeCreateErrorPage'; /** * Options for {@link createApp}. @@ -136,6 +137,11 @@ export function createApp(options?: CreateAppOptions): { advanced: options?.advanced, }); + const errorPage = maybeCreateErrorPage(app); + if (errorPage) { + return { default: () => errorPage }; + } + const rootEl = app.tree.root.instance!.getData( coreExtensionData.reactElement, ); diff --git a/packages/frontend-defaults/src/index.ts b/packages/frontend-defaults/src/index.ts index 68d4c72568..37ec62e794 100644 --- a/packages/frontend-defaults/src/index.ts +++ b/packages/frontend-defaults/src/index.ts @@ -24,3 +24,4 @@ export { createApp, type CreateAppOptions } from './createApp'; export { createPublicSignInApp } from './createPublicSignInApp'; export { discoverAvailableFeatures } from './discovery'; export { resolveAsyncFeatures } from './resolution'; +export { maybeCreateErrorPage } from './maybeCreateErrorPage'; diff --git a/packages/frontend-defaults/src/maybeCreateErrorPage.tsx b/packages/frontend-defaults/src/maybeCreateErrorPage.tsx new file mode 100644 index 0000000000..c4bce8cf73 --- /dev/null +++ b/packages/frontend-defaults/src/maybeCreateErrorPage.tsx @@ -0,0 +1,116 @@ +/* + * 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 { FrontendPluginInfo } from '@backstage/frontend-plugin-api'; +import { JSX, useEffect, useState } from 'react'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { AppError, AppErrorTypes } from '@backstage/frontend-app-api'; + +const DEFAULT_WARNING_CODES: Array = [ + 'EXTENSION_IGNORED', + 'INVALID_EXTENSION_CONFIG_KEY', + 'EXTENSION_INPUT_DATA_IGNORED', + 'EXTENSION_OUTPUT_IGNORED', +]; + +function AppErrorItem(props: { error: AppError }): JSX.Element { + const { context } = props.error; + + const extensionId = + 'extensionId' in context ? context.extensionId : context.node.spec.id; + const node = 'node' in context ? context.node : undefined; + const plugin = 'plugin' in context ? context.plugin : node?.spec.plugin; + const pluginId = plugin?.id ?? 'N/A'; + + const [info, setInfo] = useState(undefined); + useEffect(() => { + plugin?.info().then(setInfo, error => { + // eslint-disable-next-line no-console + console.error(`Failed to load info for plugin ${plugin.id}: ${error}`); + }); + }, [plugin]); + + return ( +
+ {props.error.code}: {props.error.message} +
+        
extensionId: {extensionId}
+ {pluginId &&
pluginId: {pluginId}
} + {info && ( +
+ package: {info.packageName}@{info.version} +
+ )} +
+
+ ); +} + +function AppErrorPage(props: { errors: AppError[] }): JSX.Element { + return ( +
+

App startup failed

+ {props.errors.map((error, index) => ( + + ))} +
+ ); +} + +/** + * If there are any unrecoverable errors in the app, this will return an error page in the form of a JSX element. + * + * If there are any recoverable errors, they will always be logged as warnings in the console. + * @public + */ +export function maybeCreateErrorPage( + app: { errors?: AppError[] }, + options?: { + warningCodes?: Array; + }, +): JSX.Element | undefined { + if (!app.errors) { + return undefined; + } + + const errors = new Array(); + const warnings = new Array(); + + const warningCodes = new Set(options?.warningCodes ?? DEFAULT_WARNING_CODES); + for (const error of app.errors) { + if (warningCodes.has(error.code)) { + warnings.push(error); + } else { + errors.push(error); + } + } + + if (warnings.length > 0) { + // eslint-disable-next-line no-console + console.warn('App startup encountered warnings:'); + for (const warning of warnings) { + // eslint-disable-next-line no-console + console.warn(`${warning.code}: ${warning.message}`); + } + } + + if (errors.length === 0) { + return undefined; + } + + return ; +} From e81f461ed88eac0c8328bb49977615b589cb8b4c Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 8 Sep 2025 10:35:41 +0200 Subject: [PATCH 111/233] chore: fix support for returning Signed-off-by: benjdlambert --- .../auth-backend/src/database/OidcDatabase.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/plugins/auth-backend/src/database/OidcDatabase.ts b/plugins/auth-backend/src/database/OidcDatabase.ts index 17dd4f2c06..f4f99ef6a6 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.ts @@ -167,6 +167,32 @@ export class OidcDatabase { Object.entries(row).filter(([_, value]) => value !== undefined), ); + // MySQL and SQLite3 don't support RETURNING + if ( + this.db.client.config.client.includes('sqlite3') || + this.db.client.config.client.includes('mysql') + ) { + return await this.db.transaction(async trx => { + await trx('oauth_authorization_sessions') + .where('id', session.id) + .update(updatedFields); + + const updated = await trx( + 'oauth_authorization_sessions', + ) + .where('id', session.id) + .first(); + + if (!updated) { + throw new Error( + `Failed to retrieve updated authorization session with id ${session.id}`, + ); + } + + return this.rowToAuthorizationSession(updated) as AuthorizationSession; + }); + } + const [updated] = await this.db( 'oauth_authorization_sessions', ) @@ -229,6 +255,32 @@ export class OidcDatabase { Object.entries(row).filter(([_, value]) => value !== undefined), ); + // MySQL and SQLite3 don't support RETURNING + if ( + this.db.client.config.client.includes('sqlite3') || + this.db.client.config.client.includes('mysql') + ) { + return await this.db.transaction(async trx => { + await trx('oidc_authorization_codes') + .where('code', authorizationCode.code) + .update(updatedFields); + + const updated = await trx( + 'oidc_authorization_codes', + ) + .where('code', authorizationCode.code) + .first(); + + if (!updated) { + throw new Error( + `Failed to retrieve updated authorization code with code ${authorizationCode.code}`, + ); + } + + return this.rowToAuthorizationCode(updated) as AuthorizationCode; + }); + } + const [updated] = await this.db( 'oidc_authorization_codes', ) From 838429ac898c9bd67e28a873770b187e8be69ace Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 8 Sep 2025 10:52:05 +0200 Subject: [PATCH 112/233] chore: fix some more typescript errors Signed-off-by: benjdlambert --- .../src/database/TestDatabases.ts | 2 +- .../src/database/OidcDatabase.test.ts | 20 +++++++-------- .../auth-backend/src/database/OidcDatabase.ts | 25 +++++++++++++------ .../src/service/OidcService.test.ts | 2 ++ .../auth-backend/src/service/OidcService.ts | 16 ++++++------ 5 files changed, 38 insertions(+), 27 deletions(-) diff --git a/packages/backend-test-utils/src/database/TestDatabases.ts b/packages/backend-test-utils/src/database/TestDatabases.ts index cd20e2997b..00fae4130a 100644 --- a/packages/backend-test-utils/src/database/TestDatabases.ts +++ b/packages/backend-test-utils/src/database/TestDatabases.ts @@ -104,7 +104,7 @@ export class TestDatabases { if (supportedIds.length > 0) { afterAll(async () => { await databases.shutdown(); - }); + }, 30_000); } return databases; diff --git a/plugins/auth-backend/src/database/OidcDatabase.test.ts b/plugins/auth-backend/src/database/OidcDatabase.test.ts index 82064361a5..9965069acf 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.test.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.test.ts @@ -117,7 +117,7 @@ describe('Oidc Database', () => { codeChallenge: 'test-challenge', codeChallengeMethod: 'S256', nonce: 'test-nonce', - expiresAt: '2025-01-01T00:00:00Z', + expiresAt: new Date('2025-01-01T00:00:00Z'), }); expect(session).toEqual( @@ -132,7 +132,7 @@ describe('Oidc Database', () => { codeChallenge: 'test-challenge', codeChallengeMethod: 'S256', nonce: 'test-nonce', - expiresAt: '2025-01-01T00:00:00Z', + expiresAt: new Date('2025-01-01T00:00:00Z'), status: 'pending', }), ); @@ -155,7 +155,7 @@ describe('Oidc Database', () => { clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', - expiresAt: '2025-01-01T00:00:00Z', + expiresAt: new Date('2025-01-01T00:00:00Z'), }); await expect( @@ -190,20 +190,20 @@ describe('Oidc Database', () => { clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', - expiresAt: '2025-01-01T00:00:00Z', + expiresAt: new Date('2025-01-01T00:00:00Z'), }); const authCode = await oidc.createAuthorizationCode({ code: 'test-code', sessionId: session.id, - expiresAt: '2025-01-01T00:00:00Z', + expiresAt: new Date('2025-01-01T00:00:00Z'), }); expect(authCode).toEqual( expect.objectContaining({ code: 'test-code', sessionId: session.id, - expiresAt: '2025-01-01T00:00:00Z', + expiresAt: new Date('2025-01-01T00:00:00Z'), }), ); }); @@ -230,13 +230,13 @@ describe('Oidc Database', () => { codeChallenge: 'test-challenge', codeChallengeMethod: 'S256', nonce: 'test-nonce', - expiresAt: '2025-01-01T00:00:00Z', + expiresAt: new Date('2025-01-01T00:00:00Z'), }); const authCode = await oidc.createAuthorizationCode({ code: 'test-code', sessionId: session.id, - expiresAt: '2025-01-01T00:00:00Z', + expiresAt: new Date('2025-01-01T00:00:00Z'), }); const authCodeFromDb = await oidc.getAuthorizationCode({ @@ -267,13 +267,13 @@ describe('Oidc Database', () => { clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', - expiresAt: '2025-01-01T00:00:00Z', + expiresAt: new Date('2025-01-01T00:00:00Z'), }); const authCode = await oidc.createAuthorizationCode({ code: 'test-code', sessionId: session.id, - expiresAt: '2025-01-01T00:00:00Z', + expiresAt: new Date('2025-01-01T00:00:00Z'), }); const updatedAuthCode = await oidc.updateAuthorizationCode({ diff --git a/plugins/auth-backend/src/database/OidcDatabase.ts b/plugins/auth-backend/src/database/OidcDatabase.ts index f4f99ef6a6..ec9a879803 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.ts @@ -16,6 +16,15 @@ import { Knex } from 'knex'; import { AuthDatabase } from './AuthDatabase'; +function toDate(value?: Date | string | number): Date | undefined { + if (!value) { + return undefined; + } + + return typeof value === 'string' || typeof value === 'number' + ? new Date(value) + : value; +} type OidcClientRow = { client_id: string; client_secret: string; @@ -39,13 +48,13 @@ type OAuthAuthorizationSessionRow = { code_challenge_method: string | null; nonce: string | null; status: 'pending' | 'approved' | 'rejected' | 'expired'; - expires_at: string; + expires_at: Date | string; }; type OidcAuthorizationCodeRow = { code: string; session_id: string; - expires_at: string; + expires_at: Date | string; used: boolean; }; @@ -72,26 +81,26 @@ export type AuthorizationSession = { codeChallengeMethod?: string; nonce?: string; status: 'pending' | 'approved' | 'rejected' | 'expired'; - expiresAt: string; + expiresAt: Date; }; export type ConsentRequest = { id: string; sessionId: string; - expiresAt: string; + expiresAt: Date; }; export type AuthorizationCode = { code: string; sessionId: string; - expiresAt: string; + expiresAt: Date; used: boolean; }; export type AccessToken = { tokenId: string; sessionId: string; - expiresAt: string; + expiresAt: Date; }; /** @@ -342,7 +351,7 @@ export class OidcDatabase { codeChallengeMethod: row.code_challenge_method ?? undefined, nonce: row.nonce ?? undefined, status: row.status, - expiresAt: row.expires_at, + expiresAt: toDate(row.expires_at), }; } @@ -363,7 +372,7 @@ export class OidcDatabase { return { code: row.code, sessionId: row.session_id, - expiresAt: row.expires_at, + expiresAt: toDate(row.expires_at), used: Boolean(row.used), }; } diff --git a/plugins/auth-backend/src/service/OidcService.test.ts b/plugins/auth-backend/src/service/OidcService.test.ts index 017d3ab481..11a37e20bf 100644 --- a/plugins/auth-backend/src/service/OidcService.test.ts +++ b/plugins/auth-backend/src/service/OidcService.test.ts @@ -31,6 +31,8 @@ import { UserInfoDatabase } from '../database/UserInfoDatabase'; import crypto from 'crypto'; import { AnyJWK, TokenIssuer } from '../identity/types'; +jest.setTimeout(60_000); + describe('OidcService', () => { const databases = TestDatabases.create(); diff --git a/plugins/auth-backend/src/service/OidcService.ts b/plugins/auth-backend/src/service/OidcService.ts index 7809d2fd00..4b47d963c9 100644 --- a/plugins/auth-backend/src/service/OidcService.ts +++ b/plugins/auth-backend/src/service/OidcService.ts @@ -174,7 +174,7 @@ export class OidcService { } const sessionId = crypto.randomUUID(); - const sessionExpiresAt = DateTime.now().plus({ hours: 1 }).toISO(); + const sessionExpiresAt = DateTime.now().plus({ hours: 1 }).toJSDate(); await this.oidc.createAuthorizationSession({ id: sessionId, @@ -211,7 +211,7 @@ export class OidcService { throw new NotFoundError('Invalid authorization session'); } - if (DateTime.fromISO(session.expiresAt) < DateTime.now()) { + if (DateTime.fromJSDate(session.expiresAt) < DateTime.now()) { throw new InputError('Authorization session expired'); } @@ -226,7 +226,7 @@ export class OidcService { }); const authorizationCode = crypto.randomBytes(32).toString('base64url'); - const codeExpiresAt = DateTime.now().plus({ minutes: 10 }).toISO(); + const codeExpiresAt = DateTime.now().plus({ minutes: 10 }).toJSDate(); await this.oidc.createAuthorizationCode({ code: authorizationCode, @@ -255,7 +255,7 @@ export class OidcService { throw new NotFoundError('Invalid authorization session'); } - if (DateTime.fromISO(session.expiresAt) < DateTime.now()) { + if (DateTime.fromJSDate(session.expiresAt) < DateTime.now()) { throw new InputError('Authorization session expired'); } @@ -293,7 +293,7 @@ export class OidcService { throw new NotFoundError('Invalid authorization session'); } - if (DateTime.fromISO(session.expiresAt) < DateTime.now()) { + if (DateTime.fromJSDate(session.expiresAt) < DateTime.now()) { throw new InputError('Authorization session expired'); } @@ -353,7 +353,7 @@ export class OidcService { } const sessionId = crypto.randomUUID(); - const sessionExpiresAt = DateTime.now().plus({ hours: 1 }).toISO(); + const sessionExpiresAt = DateTime.now().plus({ hours: 1 }).toJSDate(); await this.oidc.createAuthorizationSession({ id: sessionId, @@ -375,7 +375,7 @@ export class OidcService { }); const authorizationCode = crypto.randomBytes(32).toString('base64url'); - const codeExpiresAt = DateTime.now().plus({ minutes: 10 }).toISO(); + const codeExpiresAt = DateTime.now().plus({ minutes: 10 }).toJSDate(); await this.oidc.createAuthorizationCode({ code: authorizationCode, @@ -429,7 +429,7 @@ export class OidcService { throw new AuthenticationError('Invalid authorization code'); } - if (DateTime.fromISO(authCode.expiresAt) < DateTime.now()) { + if (DateTime.fromJSDate(authCode.expiresAt) < DateTime.now()) { throw new AuthenticationError('Authorization code expired'); } From d23bab525d9380ca43b2e322d546bec5299ed054 Mon Sep 17 00:00:00 2001 From: gyan Date: Mon, 8 Sep 2025 15:29:54 +0530 Subject: [PATCH 113/233] remove extra theme string from the tool-tip Signed-off-by: gyan --- plugins/user-settings/src/translation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/user-settings/src/translation.ts b/plugins/user-settings/src/translation.ts index 0161508d14..7445a415f2 100644 --- a/plugins/user-settings/src/translation.ts +++ b/plugins/user-settings/src/translation.ts @@ -28,7 +28,7 @@ export const userSettingsTranslationRef = createTranslationRef({ themeToggle: { title: 'Theme', description: 'Change the theme mode', - select: 'Select theme {{theme}}', + select: 'Select {{theme}}', selectAuto: 'Select Auto Theme', names: { light: 'Light', From 4071458967111ddd87cbf6de5f7923e69b8476fc Mon Sep 17 00:00:00 2001 From: Aditya Kumar <136452216+AdityaK60@users.noreply.github.com> Date: Mon, 8 Sep 2025 15:34:48 +0530 Subject: [PATCH 114/233] Update 02-testing.md Signed-off-by: Aditya Kumar <136452216+AdityaK60@users.noreply.github.com> --- docs/frontend-system/building-plugins/02-testing.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/frontend-system/building-plugins/02-testing.md b/docs/frontend-system/building-plugins/02-testing.md index 9e5bb1be44..ac90185b40 100644 --- a/docs/frontend-system/building-plugins/02-testing.md +++ b/docs/frontend-system/building-plugins/02-testing.md @@ -78,7 +78,7 @@ This pattern also works for many other context providers. An important example i ## Testing extensions -To facilitate testing of frontend extensions, the `@backstage/frontend-test-utils` package provides a tester class that starts up an entire frontend harness, complete with a number of default features. You can then provide overrides for extensions whose behavior you need to adjust for the test run. +To facilitate testing of frontend extensions, the `@backstage/frontend-test-utils` package provides a tester class which starts up an entire frontend harness, complete with a number of default features. You can then provide overrides for extensions whose behavior you need to adjust for the test run. A number of features (frontend extensions and overrides) are also accepted by the tester. Here are some examples of how these facilities can be useful: @@ -108,7 +108,7 @@ Note that the `.reactElement()` method will look for the `coreExtensionData.reac ### Multiple extensions -In some cases, you might need to test multiple extensions together, particularly when testing inputs. In this case, you can add more extensions to the tester instance using the `.add(...)` method. It also accepts an optional options object as the second argument, which you can use to provide configuration for the extension instance. +In some cases you might need to test multiple extensions together, in particular when testing inputs. In this case, you can add more extensions to the tester instance using the `.add(...)` method. It also accepts an optional options object as the second argument, which you can use to provide configuration for the extension instance. ```tsx import { screen } from '@testing-library/react'; @@ -134,7 +134,7 @@ describe('Index page', async () => { }); ``` -When testing multiple extensions, you may sometimes want to access the output of other extensions than the main test subject. You can use the `.query(ext)` method to query a different extension that has been added to the tester, by passing the extension used with the `createExtensionTester(...).add(ext)` +When testing multiple extensions you may sometimes want to access the output of other extensions than the main test subject. You can use the `.query(ext)` method to query a different extension that has been added to the tester, by passing the extension used with the `createExtensionTester(...).add(ext)` ### Setting configuration From b713b543564445dd18908b5ffa791da55d2a5686 Mon Sep 17 00:00:00 2001 From: gyan Date: Mon, 8 Sep 2025 15:45:56 +0530 Subject: [PATCH 115/233] add changeset configuration Signed-off-by: gyan --- .changeset/red-shrimps-fall.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/red-shrimps-fall.md diff --git a/.changeset/red-shrimps-fall.md b/.changeset/red-shrimps-fall.md new file mode 100644 index 0000000000..1ee3aca7ed --- /dev/null +++ b/.changeset/red-shrimps-fall.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': patch +--- + +Tool-tip text correction for the Theme selection in settings page From 4eda590b6a5e0309fcc23a279fc2931471f5b5c0 Mon Sep 17 00:00:00 2001 From: Shijun Wang Date: Tue, 29 Jul 2025 15:53:19 +0300 Subject: [PATCH 116/233] add logic to construct namespace from provided namespace and plugin id Signed-off-by: Shijun Wang --- .changeset/big-cameras-turn.md | 5 + docs/backend-system/core-services/cache.md | 33 ++++ .../entrypoints/cache/CacheManager.test.ts | 143 ++++++++++++++++++ .../src/entrypoints/cache/CacheManager.ts | 23 ++- 4 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 .changeset/big-cameras-turn.md diff --git a/.changeset/big-cameras-turn.md b/.changeset/big-cameras-turn.md new file mode 100644 index 0000000000..6226b17806 --- /dev/null +++ b/.changeset/big-cameras-turn.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Fixed cache namespace and key prefix separator configuration to properly use configured values instead of hardcoded plugin ID. The cache manager now correctly combines the configured namespace with plugin IDs using the configured separator for Redis and Valkey. Memcache and memory store continue to use plugin ID as namespace. diff --git a/docs/backend-system/core-services/cache.md b/docs/backend-system/core-services/cache.md index 0e26e8e1f5..ad4d04b50a 100644 --- a/docs/backend-system/core-services/cache.md +++ b/docs/backend-system/core-services/cache.md @@ -7,6 +7,39 @@ description: Documentation for the Cache service This service lets your plugin interact with a cache. It is bound to your plugin too, so that you will only set and get values in your plugin's private namespace. +## Configuration + +The cache service can be configured using the `backend.cache` section in your `app-config.yaml`: + +```yaml +backend: + cache: + store: redis # or 'valkey', 'memcache', 'memory' + connection: redis://localhost:6379 + + # Store-specific configuration (Redis/Valkey only) + redis: + client: + # Optional: Global namespace prefix for all cache keys + namespace: 'my-app' + # Optional: Separator used between namespace and plugin ID (default: ':') + keyPrefixSeparator: ':' + # Other Redis-specific options... + clearBatchSize: 1000 + useUnlink: false +``` + +### Namespace Configuration + +For Redis and Valkey stores, you can configure a global namespace that will be prefixed to all cache keys: + +- **Without namespace**: Cache keys use only the plugin ID (e.g., `catalog:some-key`) +- **With namespace**: Cache keys use the format `namespace:pluginId:key` (e.g., `my-app:catalog:some-key`) + +The `keyPrefixSeparator` controls what character is used between the namespace and plugin ID (defaults to `:`). + +**Note**: Memory and Memcache stores do not support namespace configuration and will always use the plugin ID directly. + ## Using the service The following example shows how to get a cache client in your `example` backend plugin and setting and getting values from the cache. diff --git a/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts index 4e9475b66e..a308caa222 100644 --- a/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.test.ts @@ -333,4 +333,147 @@ describe('CacheManager store options', () => { keyPrefixSeparator: '!', }); }); + + it('correctly applies namespace configuration to redis and valkey stores', () => { + const testCases = [ + { store: 'redis', namespace: 'test1', separator: ':' }, + { store: 'valkey', namespace: 'test2', separator: '!' }, + ]; + + testCases.forEach(({ store, namespace, separator }) => { + const manager = CacheManager.fromConfig( + mockServices.rootConfig({ + data: { + backend: { + cache: { + store, + connection: 'redis://localhost:6379', + [store]: { + client: { + namespace, + keyPrefixSeparator: separator, + }, + }, + }, + }, + }, + }), + ); + + manager.forPlugin('testPlugin'); + + if (store === 'redis') { + // eslint-disable-next-line jest/no-conditional-expect + expect(KeyvRedis).toHaveBeenCalledWith('redis://localhost:6379', { + namespace, + keyPrefixSeparator: separator, + }); + } else if (store === 'valkey') { + // eslint-disable-next-line jest/no-conditional-expect + expect(KeyvValkey).toHaveBeenCalledWith('redis://localhost:6379', { + namespace, + keyPrefixSeparator: separator, + }); + } + }); + }); + + it('falls back to pluginId when no namespace is configured', () => { + const manager = CacheManager.fromConfig( + mockServices.rootConfig({ + data: { + backend: { + cache: { + store: 'redis', + connection: 'redis://localhost:6379', + }, + }, + }, + }), + ); + + manager.forPlugin('testPlugin'); + + expect(KeyvRedis).toHaveBeenCalledWith('redis://localhost:6379', { + keyPrefixSeparator: ':', + }); + }); + + describe('Namespace construction', () => { + it('returns pluginId when no store options are provided', () => { + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + undefined, + ); + expect(result).toBe('testPlugin'); + }); + + it('returns pluginId when store options have no namespace', () => { + const storeOptions = { + client: { + keyPrefixSeparator: ':', + }, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('testPlugin'); + }); + + it('combines namespace and pluginId with default separator', () => { + const storeOptions = { + client: { + namespace: 'my-app', + keyPrefixSeparator: ':', + }, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('my-app:testPlugin'); + }); + + it('combines namespace and pluginId with custom separator', () => { + const storeOptions = { + client: { + namespace: 'my-app', + keyPrefixSeparator: '-', + }, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('my-app-testPlugin'); + }); + + it('uses default separator when keyPrefixSeparator is not provided', () => { + const storeOptions = { + client: { + namespace: 'my-app', + }, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('my-app:testPlugin'); + }); + + it('handles empty namespace by falling back to pluginId', () => { + const storeOptions = { + client: { + namespace: '', + keyPrefixSeparator: ':', + }, + }; + const result = (CacheManager as any).constructNamespace( + 'testPlugin', + storeOptions, + ); + expect(result).toBe('testPlugin'); + }); + }); }); diff --git a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts index 1c26083525..29bea42273 100644 --- a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts @@ -213,6 +213,25 @@ export class CacheManager { return redisOptions; } + /** + * Construct the full namespace based on the options and pluginId. + * + * @param pluginId - The plugin ID to namespace + * @param storeOptions - Optional cache store configuration options + * @returns The constructed namespace string combining the configured namespace with pluginId + */ + private static constructNamespace( + pluginId: string, + storeOptions: CacheStoreOptions | undefined, + ): string { + if (storeOptions?.client?.namespace) { + const separator = storeOptions.client.keyPrefixSeparator ?? ':'; + return `${storeOptions.client.namespace}${separator}${pluginId}`; + } + + return pluginId; + } + /** @internal */ constructor( store: string, @@ -286,7 +305,7 @@ export class CacheManager { }); } return new Keyv({ - namespace: pluginId, + namespace: CacheManager.constructNamespace(pluginId, this.storeOptions), ttl: defaultTtl, store: stores[pluginId], emitErrors: false, @@ -326,7 +345,7 @@ export class CacheManager { }); } return new Keyv({ - namespace: pluginId, + namespace: CacheManager.constructNamespace(pluginId, this.storeOptions), ttl: defaultTtl, store: stores[pluginId], emitErrors: false, From 0b9278c6235374ac5df7f2deed24dcb5e677a080 Mon Sep 17 00:00:00 2001 From: Shijun Wang Date: Mon, 8 Sep 2025 13:32:43 +0300 Subject: [PATCH 117/233] fix type errors Signed-off-by: Shijun Wang --- .../src/entrypoints/cache/CacheManager.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts index 29bea42273..06b2b0c57b 100644 --- a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts @@ -222,14 +222,15 @@ export class CacheManager { */ private static constructNamespace( pluginId: string, - storeOptions: CacheStoreOptions | undefined, + storeOptions: RedisCacheStoreOptions | undefined, ): string { - if (storeOptions?.client?.namespace) { - const separator = storeOptions.client.keyPrefixSeparator ?? ':'; - return `${storeOptions.client.namespace}${separator}${pluginId}`; - } + const prefix = storeOptions?.client?.namespace + ? `${storeOptions.client.namespace}${ + storeOptions.client.keyPrefixSeparator ?? ':' + }` + : ''; - return pluginId; + return `${prefix}${pluginId}`; } /** @internal */ From 4907da392ae556fef1732ebce8dc041d9494d2ba Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 8 Sep 2025 10:28:06 +0200 Subject: [PATCH 118/233] frontend-test-utils: updated to use and report errors via error collector Signed-off-by: Patrik Oldsberg --- .../src/wiring/createExtension.test.ts | 8 ++++---- .../src/app/createExtensionTester.tsx | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 00dd981e26..59a81f1b59 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -1078,7 +1078,7 @@ describe('createExtension', () => { .add(multi2Ext) .get(outputRef), ).toThrowErrorMatchingInlineSnapshot( - `"Failed to instantiate extension 'subject', override data provided for input 'multi' must match the length of the original inputs"`, + `"Failed to resolve the extension tree: Failed to instantiate extension 'subject', override data provided for input 'multi' must match the length of the original inputs"`, ); // Mix forward and data override @@ -1101,7 +1101,7 @@ describe('createExtension', () => { .add(multi2Ext) .get(outputRef), ).toThrowErrorMatchingInlineSnapshot( - `"Failed to instantiate extension 'subject', override data for input 'multi' may not mix forwarded inputs with data overrides"`, + `"Failed to resolve the extension tree: Failed to instantiate extension 'subject', override data for input 'multi' may not mix forwarded inputs with data overrides"`, ); // Required input not provided @@ -1124,7 +1124,7 @@ describe('createExtension', () => { .add(multi2Ext) .get(outputRef), ).toThrowErrorMatchingInlineSnapshot( - `"Failed to instantiate extension 'subject', missing required extension data value(s) 'test1'"`, + `"Failed to resolve the extension tree: Failed to instantiate extension 'subject', missing required extension data value(s) 'test1'"`, ); // Wrong value provided @@ -1153,7 +1153,7 @@ describe('createExtension', () => { .add(multi2Ext) .get(outputRef), ).toThrowErrorMatchingInlineSnapshot( - `"Failed to instantiate extension 'subject', extension data 'test2' was provided but not declared"`, + `"Failed to resolve the extension tree: Failed to instantiate extension 'subject', extension data 'test2' was provided but not declared"`, ); }); }); diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index 7b321d4413..50d65fddc7 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -35,6 +35,8 @@ import { resolveAppNodeSpecs } from '../../../frontend-app-api/src/tree/resolveA import { instantiateAppNodeTree } from '../../../frontend-app-api/src/tree/instantiateAppNodeTree'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { readAppExtensionsConfig } from '../../../frontend-app-api/src/tree/readAppExtensionsConfig'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { createErrorCollector } from '../../../frontend-app-api/src/wiring/createErrorCollector'; import { TestApiRegistry } from '@backstage/test-utils'; import { OpaqueExtensionDefinition } from '@internal/frontend'; @@ -191,16 +193,29 @@ export class ExtensionTester { ); } + const collector = createErrorCollector(); + const tree = resolveAppTree( subject.id, resolveAppNodeSpecs({ features: [], builtinExtensions: this.#extensions.map(_ => _.extension), parameters: readAppExtensionsConfig(this.#getConfig()), + collector, }), + collector, ); - instantiateAppNodeTree(tree.root, TestApiRegistry.from()); + instantiateAppNodeTree(tree.root, TestApiRegistry.from(), collector); + + const errors = collector.collectErrors(); + if (errors) { + throw new Error( + `Failed to resolve the extension tree: ${errors + .map(e => e.message) + .join(', ')}`, + ); + } this.#tree = tree; From 6516c3d81554ad736965a0cc1d00717e4a8e145c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 8 Sep 2025 10:32:49 +0200 Subject: [PATCH 119/233] changesets: add changesets for new app error API Signed-off-by: Patrik Oldsberg --- .changeset/loud-forks-teach.md | 5 +++++ .changeset/puny-tires-fold.md | 5 +++++ .changeset/silver-baboons-ring.md | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .changeset/loud-forks-teach.md create mode 100644 .changeset/puny-tires-fold.md create mode 100644 .changeset/silver-baboons-ring.md diff --git a/.changeset/loud-forks-teach.md b/.changeset/loud-forks-teach.md new file mode 100644 index 0000000000..662ff458ca --- /dev/null +++ b/.changeset/loud-forks-teach.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Internal update to use and throw app errors. diff --git a/.changeset/puny-tires-fold.md b/.changeset/puny-tires-fold.md new file mode 100644 index 0000000000..e54187cc85 --- /dev/null +++ b/.changeset/puny-tires-fold.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': minor +--- + +The `createSpecializedApp` no longer throws when encountering many common errors when starting up the app. It will instead return them through the `errors` property so that they can be handled more gracefully in the app. diff --git a/.changeset/silver-baboons-ring.md b/.changeset/silver-baboons-ring.md new file mode 100644 index 0000000000..578914b35c --- /dev/null +++ b/.changeset/silver-baboons-ring.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-defaults': patch +--- + +The default app now leverages the new error reporting functionality from `@backstage/frontend-app-api`. If there are critical errors during startup, an error screen that shows a summary of all errors will now be shown, rather than leaving the screen blank. Other errors will be logged as warnings in the console. From 025fdd20ea1de2fa14c28bf8ee6c8d921222a593 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 8 Sep 2025 13:12:36 +0200 Subject: [PATCH 120/233] chore: clientId and clientSecret are not to be passed to the token endpoint Signed-off-by: benjdlambert --- .../src/service/OidcRouter.test.ts | 97 +------------------ .../auth-backend/src/service/OidcRouter.ts | 6 +- .../src/service/OidcService.test.ts | 41 -------- .../auth-backend/src/service/OidcService.ts | 23 +---- 4 files changed, 3 insertions(+), 164 deletions(-) diff --git a/plugins/auth-backend/src/service/OidcRouter.test.ts b/plugins/auth-backend/src/service/OidcRouter.test.ts index fe5182251f..6362fd9a02 100644 --- a/plugins/auth-backend/src/service/OidcRouter.test.ts +++ b/plugins/auth-backend/src/service/OidcRouter.test.ts @@ -548,8 +548,6 @@ describe('OidcRouter', () => { .send({ grant_type: 'authorization_code', code: authorizationCode, - client_id: client.clientId, - client_secret: client.clientSecret, redirect_uri: 'https://example.com/callback', }) .expect(200); @@ -646,8 +644,6 @@ describe('OidcRouter', () => { .send({ grant_type: 'authorization_code', code: authorizationCode, - client_id: client.clientId, - client_secret: client.clientSecret, redirect_uri: 'https://example.com/callback', code_verifier: codeVerifier, }) @@ -668,95 +664,8 @@ describe('OidcRouter', () => { }); }); - it('should reject token exchange with invalid client credentials', async () => { - const { - mocks: { auth, service, httpAuth }, - router, - } = await createRouter(databaseId); - - httpAuth.credentials.mockResolvedValueOnce({ - principal: { - type: 'user', - userEntityRef: 'user:default/test-user', - }, - $$type: '@backstage/BackstageCredentials', - }); - - auth.isPrincipal.mockReturnValueOnce(true); - - const client = await service.registerClient({ - clientName: 'Test Client', - redirectUris: ['https://example.com/callback'], - responseTypes: ['code'], - grantTypes: ['authorization_code'], - scope: 'openid', - }); - - const authSession = await service.createAuthorizationSession({ - clientId: client.clientId, - redirectUri: 'https://example.com/callback', - responseType: 'code', - scope: 'openid', - }); - - const { server } = await startTestBackend({ - features: [ - createBackendPlugin({ - pluginId: 'auth', - register(reg) { - reg.registerInit({ - deps: { httpRouter: coreServices.httpRouter }, - async init({ httpRouter }) { - httpRouter.use(router.getRouter()); - httpRouter.addAuthPolicy({ - path: '/', - allow: 'unauthenticated', - }); - }, - }); - }, - }), - ], - }); - - const approvalResponse = await request(server) - .post(`/api/auth/v1/sessions/${authSession.id}/approve`) - .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) - .expect(200); - - const redirectUrl = new URL(approvalResponse.body.redirectUrl); - const authorizationCode = redirectUrl.searchParams.get('code'); - - const tokenResponse = await request(server) - .post('/api/auth/v1/token') - .send({ - grant_type: 'authorization_code', - code: authorizationCode, - client_id: client.clientId, - client_secret: 'invalid-secret', - redirect_uri: 'https://example.com/callback', - }) - .expect(401); - - expect(tokenResponse.body).toEqual({ - error: 'invalid_client', - error_description: 'Invalid client credentials', - }); - }); - it('should reject token exchange with invalid authorization code', async () => { - const { - mocks: { service }, - router, - } = await createRouter(databaseId); - - const client = await service.registerClient({ - clientName: 'Test Client', - redirectUris: ['https://example.com/callback'], - responseTypes: ['code'], - grantTypes: ['authorization_code'], - scope: 'openid', - }); + const { router } = await createRouter(databaseId); const { server } = await startTestBackend({ features: [ @@ -783,8 +692,6 @@ describe('OidcRouter', () => { .send({ grant_type: 'authorization_code', code: 'invalid-code', - client_id: client.clientId, - client_secret: client.clientSecret, redirect_uri: 'https://example.com/callback', }) .expect(401); @@ -875,8 +782,6 @@ describe('OidcRouter', () => { .send({ grant_type: 'authorization_code', code: authorizationCode, - client_id: client.clientId, - client_secret: client.clientSecret, redirect_uri: 'https://example.com/callback', code_verifier: codeVerifier, }) diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index 60c0e42f92..c64d41a73e 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -276,13 +276,11 @@ export class OidcRouter { const { grant_type: grantType, code, - client_id: clientId, - client_secret: clientSecret, redirect_uri: redirectUri, code_verifier: codeVerifier, } = req.body; - if (!grantType || !code || !clientId || !clientSecret || !redirectUri) { + if (!grantType || !code || !redirectUri) { this.logger.error( `Failed to exchange code for token: Missing required parameters`, ); @@ -295,8 +293,6 @@ export class OidcRouter { try { const result = await this.oidc.exchangeCodeForToken({ code, - clientId, - clientSecret, redirectUri, codeVerifier, grantType, diff --git a/plugins/auth-backend/src/service/OidcService.test.ts b/plugins/auth-backend/src/service/OidcService.test.ts index 11a37e20bf..b1f03c68ed 100644 --- a/plugins/auth-backend/src/service/OidcService.test.ts +++ b/plugins/auth-backend/src/service/OidcService.test.ts @@ -668,8 +668,6 @@ describe('OidcService', () => { const tokenResult = await service.exchangeCodeForToken({ code, - clientId: client.clientId, - clientSecret: client.clientSecret, redirectUri: 'https://example.com/callback', grantType: 'authorization_code', }); @@ -689,47 +687,12 @@ describe('OidcService', () => { await expect( service.exchangeCodeForToken({ code: 'test-code', - clientId: 'test-client', - clientSecret: 'test-secret', redirectUri: 'https://example.com/callback', grantType: 'client_credentials', }), ).rejects.toThrow('Unsupported grant type'); }); - it('should throw error for invalid client', async () => { - const { service } = await createOidcService(databaseId); - - await expect( - service.exchangeCodeForToken({ - code: 'test-code', - clientId: 'invalid-client', - clientSecret: 'test-secret', - redirectUri: 'https://example.com/callback', - grantType: 'authorization_code', - }), - ).rejects.toThrow('Invalid client'); - }); - - it('should throw error for invalid client secret', async () => { - const { service } = await createOidcService(databaseId); - - const client = await service.registerClient({ - clientName: 'Test Client', - redirectUris: ['https://example.com/callback'], - }); - - await expect( - service.exchangeCodeForToken({ - code: 'test-code', - clientId: client.clientId, - clientSecret: 'invalid-secret', - redirectUri: 'https://example.com/callback', - grantType: 'authorization_code', - }), - ).rejects.toThrow('Invalid client credentials'); - }); - it('should handle PKCE verification', async () => { const { service, mocks } = await createOidcService(databaseId); const mockToken = 'mock-jwt-token'; @@ -759,8 +722,6 @@ describe('OidcService', () => { const tokenResult = await service.exchangeCodeForToken({ code, - clientId: client.clientId, - clientSecret: client.clientSecret, redirectUri: 'https://example.com/callback', grantType: 'authorization_code', codeVerifier, @@ -792,8 +753,6 @@ describe('OidcService', () => { await expect( service.exchangeCodeForToken({ code, - clientId: client.clientId, - clientSecret: client.clientSecret, redirectUri: 'https://example.com/callback', grantType: 'authorization_code', codeVerifier: 'invalid-verifier', diff --git a/plugins/auth-backend/src/service/OidcService.ts b/plugins/auth-backend/src/service/OidcService.ts index 4b47d963c9..2b7eb40bc9 100644 --- a/plugins/auth-backend/src/service/OidcService.ts +++ b/plugins/auth-backend/src/service/OidcService.ts @@ -396,34 +396,16 @@ export class OidcService { public async exchangeCodeForToken(params: { code: string; - clientId: string; - clientSecret: string; redirectUri: string; codeVerifier?: string; grantType: string; }) { - const { - code, - clientId, - clientSecret, - redirectUri, - codeVerifier, - grantType, - } = params; + const { code, redirectUri, codeVerifier, grantType } = params; if (grantType !== 'authorization_code') { throw new InputError('Unsupported grant type'); } - const client = await this.oidc.getClient({ clientId }); - if (!client) { - throw new AuthenticationError('Invalid client'); - } - - if (client.clientSecret !== clientSecret) { - throw new AuthenticationError('Invalid client credentials'); - } - const authCode = await this.oidc.getAuthorizationCode({ code }); if (!authCode) { throw new AuthenticationError('Invalid authorization code'); @@ -444,9 +426,6 @@ export class OidcService { if (!session) { throw new NotFoundError('Invalid authorization session'); } - if (session.clientId !== clientId) { - throw new AuthenticationError('Client ID mismatch'); - } if (session.redirectUri !== redirectUri) { throw new AuthenticationError('Redirect URI mismatch'); From 929c55adbc7095a6431512cbb37a22513b94062c Mon Sep 17 00:00:00 2001 From: Shijun Wang Date: Mon, 8 Sep 2025 15:07:15 +0300 Subject: [PATCH 121/233] wait for storage to become ready Signed-off-by: Shijun Wang --- .changeset/heavy-cats-unite.md | 6 ++++++ plugins/home/report.api.md | 2 +- .../CustomHomepage/CustomHomepageGrid.tsx | 14 +++++++++++--- 3 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 .changeset/heavy-cats-unite.md diff --git a/.changeset/heavy-cats-unite.md b/.changeset/heavy-cats-unite.md new file mode 100644 index 0000000000..4875093b0a --- /dev/null +++ b/.changeset/heavy-cats-unite.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-home': patch +--- + +Fixed race condition in CustomHomepageGrid by waiting for storage to load before rendering custom layout to prevent +rendering of the default content. diff --git a/plugins/home/report.api.md b/plugins/home/report.api.md index dd33b952ee..73c6d035cc 100644 --- a/plugins/home/report.api.md +++ b/plugins/home/report.api.md @@ -84,7 +84,7 @@ export const createCardExtension: typeof createCardExtension_2; // @public export const CustomHomepageGrid: ( props: CustomHomepageGridProps, -) => JSX_2.Element; +) => JSX_2.Element | null; // @public export type CustomHomepageGridProps = { diff --git a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx index 5d85e834e8..b4b674be1b 100644 --- a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx +++ b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx @@ -90,7 +90,7 @@ const useStyles = makeStyles((theme: Theme) => function useHomeStorage( defaultWidgets: GridWidget[], -): [GridWidget[], (value: GridWidget[]) => void] { +): [GridWidget[], (value: GridWidget[]) => void, boolean] { const key = 'home'; const storageApi = useApi(storageApiRef).forBucket('home.customHomepage'); // TODO: Support multiple home pages @@ -110,6 +110,9 @@ function useHomeStorage( storageApi.observe$(key), storageApi.snapshot(key), ); + + const isStorageLoading = homeSnapshot.presence === 'unknown' || !homeSnapshot; + const widgets: GridWidget[] = useMemo(() => { if (homeSnapshot.presence === 'absent') { return defaultWidgets; @@ -122,7 +125,7 @@ function useHomeStorage( } }, [homeSnapshot, defaultWidgets]); - return [widgets, setWidgets]; + return [widgets, setWidgets, isStorageLoading]; } const convertConfigToDefaultWidgets = ( @@ -213,7 +216,7 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => { ? convertConfigToDefaultWidgets(props.config, availableWidgets) : []; }, [props.config, availableWidgets]); - const [widgets, setWidgets] = useHomeStorage(defaultLayout); + const [widgets, setWidgets, isStorageLoading] = useHomeStorage(defaultLayout); const [addWidgetDialogOpen, setAddWidgetDialogOpen] = useState(false); const editModeOn = widgets.find(w => w.layout.isResizable) !== undefined; const [editMode, setEditMode] = useState(editModeOn); @@ -322,6 +325,11 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => { ); }; + // Don't render anything while storage is loading + if (isStorageLoading) { + return null; + } + return ( <> From 225cdf5bdf05b767947fb59f1468ebdcdb68c0e9 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 8 Sep 2025 14:27:05 +0200 Subject: [PATCH 122/233] chore: wrap up things in a feature flag Signed-off-by: benjdlambert --- app-config.yaml | 2 + .../src/service/OidcRouter.test.ts | 1 + .../auth-backend/src/service/OidcRouter.ts | 595 +++++++++--------- plugins/auth-backend/src/service/router.ts | 4 + plugins/mcp-actions-backend/src/plugin.ts | 35 +- 5 files changed, 328 insertions(+), 309 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 60842ee61a..e32609c1a1 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -209,6 +209,8 @@ scaffolder: defaultCommitMessage: 'Initial commit' auth: + experimental: + enableDynamicClientRegistration: true ### Add auth.keyStore.provider to more granularly control how to store JWK data when running # the auth-backend. # diff --git a/plugins/auth-backend/src/service/OidcRouter.test.ts b/plugins/auth-backend/src/service/OidcRouter.test.ts index 6362fd9a02..8714eb7f62 100644 --- a/plugins/auth-backend/src/service/OidcRouter.test.ts +++ b/plugins/auth-backend/src/service/OidcRouter.test.ts @@ -86,6 +86,7 @@ describe('OidcRouter', () => { userInfo: userInfoDatabase, oidc: oidcDatabase, httpAuth: mockHttpAuth, + enableDynamicClientRegistration: true, }); return { diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index c64d41a73e..6c39115528 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -33,6 +33,7 @@ export class OidcRouter { private readonly auth: AuthService, private readonly appUrl: string, private readonly httpAuth: HttpAuthService, + private readonly enableDynamicClientRegistration: boolean, ) {} static create(options: { @@ -44,6 +45,7 @@ export class OidcRouter { userInfo: UserInfoDatabase; oidc: OidcDatabase; httpAuth: HttpAuthService; + enableDynamicClientRegistration: boolean; }) { return new OidcRouter( OidcService.create(options), @@ -51,6 +53,7 @@ export class OidcRouter { options.auth, options.appUrl, options.httpAuth, + options.enableDynamicClientRegistration, ); } @@ -74,266 +77,6 @@ export class OidcRouter { res.json({ keys }); }); - // Authorization endpoint - // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest - // Handles the initial authorization request from the client, validates parameters, - // and redirects to the Authorization Session page for user approval - router.get('/v1/authorize', async (req, res) => { - // todo(blam): maybe add zod types for validating input - const { - client_id: clientId, - redirect_uri: redirectUri, - response_type: responseType, - scope, - state, - nonce, - code_challenge: codeChallenge, - code_challenge_method: codeChallengeMethod, - } = req.query; - - if (!clientId || !redirectUri || !responseType) { - this.logger.error(`Failed to authorize: Missing required parameters`); - return res.status(400).json({ - error: 'invalid_request', - error_description: - 'Missing required parameters: client_id, redirect_uri, response_type', - }); - } - - try { - const result = await this.oidc.createAuthorizationSession({ - clientId: clientId as string, - redirectUri: redirectUri as string, - responseType: responseType as string, - scope: scope as string, - state: state as string, - nonce: nonce as string, - codeChallenge: codeChallenge as string, - codeChallengeMethod: codeChallengeMethod as string, - }); - - // todo(blam): maybe this URL could be overridable by config if - // the plugin is mounted somewhere else? - // support slashes in baseUrl? - const authSessionRedirectUrl = new URL( - `/auth/sessions/${result.id}`, - this.appUrl, - ); - - return res.redirect(authSessionRedirectUrl.toString()); - } catch (error) { - const errorParams = new URLSearchParams(); - errorParams.append( - 'error', - isError(error) ? error.name : 'server_error', - ); - errorParams.append( - 'error_description', - isError(error) ? error.message : 'Unknown error', - ); - if (state) { - errorParams.append('state', state as string); - } - - const redirectUrl = new URL(redirectUri as string); - redirectUrl.search = errorParams.toString(); - return res.redirect(redirectUrl.toString()); - } - }); - - // Authorization Session request details endpoint - // Returns Authorization Session request details for the frontned - router.get('/v1/sessions/:sessionId', async (req, res) => { - const { sessionId } = req.params; - - if (!sessionId) { - return res.status(400).json({ - error: 'invalid_request', - error_description: 'Missing Authorization Session ID', - }); - } - - try { - const session = await this.oidc.getAuthorizationSession({ - sessionId, - }); - - return res.json({ - id: session.id, - clientName: session.clientName, - scope: session.scope, - redirectUri: session.redirectUri, - }); - } catch (error) { - const description = isError(error) ? error.message : 'Unknown error'; - this.logger.error( - `Failed to get authorization session: ${description}`, - error, - ); - return res.status(404).json({ - error: 'not_found', - error_description: description, - }); - } - }); - - // Authorization Session approval endpoint - // Handles user approval of Authorization Session requests and generates authorization codes - router.post('/v1/sessions/:sessionId/approve', async (req, res) => { - const { sessionId } = req.params; - - if (!sessionId) { - return res.status(400).json({ - error: 'invalid_request', - error_description: 'Missing authorization session ID', - }); - } - - try { - const httpCredentials = await this.httpAuth.credentials(req); - - if (!this.auth.isPrincipal(httpCredentials, 'user')) { - return res.status(401).json({ - error: 'unauthorized', - error_description: 'Authentication required', - }); - } - - const userEntityRef = httpCredentials.principal.userEntityRef; - - const result = await this.oidc.approveAuthorizationSession({ - sessionId, - userEntityRef, - }); - - return res.json({ - redirectUrl: result.redirectUrl, - }); - } catch (error) { - const description = isError(error) ? error.message : 'Unknown error'; - this.logger.error( - `Failed to approve authorization session: ${description}`, - error, - ); - return res.status(400).json({ - error: 'invalid_request', - error_description: description, - }); - } - }); - - // Authorization Session rejection endpoint - // Handles user rejection of Authorization Session requests and redirects with error - router.post('/v1/sessions/:sessionId/reject', async (req, res) => { - const { sessionId } = req.params; - - if (!sessionId) { - return res.status(400).json({ - error: 'invalid_request', - error_description: 'Missing authorization session ID', - }); - } - - try { - const session = await this.oidc.getAuthorizationSession({ - sessionId, - }); - - await this.oidc.rejectAuthorizationSession({ sessionId }); - - const errorParams = new URLSearchParams(); - errorParams.append('error', 'access_denied'); - errorParams.append('error_description', 'User denied the request'); - if (session.state) { - errorParams.append('state', session.state); - } - - const redirectUrl = new URL(session.redirectUri); - redirectUrl.search = errorParams.toString(); - - return res.json({ - redirectUrl: redirectUrl.toString(), - }); - } catch (error) { - const description = isError(error) ? error.message : 'Unknown error'; - this.logger.error( - `Failed to reject authorization session: ${description}`, - error, - ); - - return res.status(400).json({ - error: 'invalid_request', - error_description: description, - }); - } - }); - - // Token endpoint - // https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest - // Exchanges authorization codes for access tokens and ID tokens - router.post('/v1/token', async (req, res) => { - // todo(blam): maybe add zod types for validating input - const { - grant_type: grantType, - code, - redirect_uri: redirectUri, - code_verifier: codeVerifier, - } = req.body; - - if (!grantType || !code || !redirectUri) { - this.logger.error( - `Failed to exchange code for token: Missing required parameters`, - ); - return res.status(400).json({ - error: 'invalid_request', - error_description: 'Missing required parameters', - }); - } - - try { - const result = await this.oidc.exchangeCodeForToken({ - code, - redirectUri, - codeVerifier, - grantType, - }); - - return res.json({ - access_token: result.accessToken, - token_type: result.tokenType, - expires_in: result.expiresIn, - id_token: result.idToken, - scope: result.scope, - }); - } catch (error) { - const description = isError(error) ? error.message : 'Unknown error'; - this.logger.error( - `Failed to exchange code for token: ${description}`, - error, - ); - - if (isError(error)) { - if (error.name === 'AuthenticationError') { - return res.status(401).json({ - error: 'invalid_client', - error_description: error.message, - }); - } - if (error.name === 'InputError') { - return res.status(400).json({ - error: 'invalid_request', - error_description: error.message, - }); - } - } - - return res.status(500).json({ - error: 'server_error', - error_description: description, - }); - } - }); - // UserInfo endpoint // https://openid.net/specs/openid-connect-core-1_0.html#UserInfo // Returns claims about the authenticated user using an access token @@ -354,45 +97,307 @@ export class OidcRouter { res.json(userInfo); }); - // Dynamic Client Registration endpoint - // https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration - // Allows clients to register themselves dynamically with the provider - router.post('/v1/register', async (req, res) => { - // todo(blam): maybe add zod types for validating input - const registrationRequest = req.body; + if (this.enableDynamicClientRegistration) { + // Authorization endpoint + // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + // Handles the initial authorization request from the client, validates parameters, + // and redirects to the Authorization Session page for user approval + router.get('/v1/authorize', async (req, res) => { + // todo(blam): maybe add zod types for validating input + const { + client_id: clientId, + redirect_uri: redirectUri, + response_type: responseType, + scope, + state, + nonce, + code_challenge: codeChallenge, + code_challenge_method: codeChallengeMethod, + } = req.query; - if (!registrationRequest.redirect_uris?.length) { - res.status(400).json({ - error: 'invalid_request', - error_description: 'redirect_uris is required', - }); - return; - } + if (!clientId || !redirectUri || !responseType) { + this.logger.error(`Failed to authorize: Missing required parameters`); + return res.status(400).json({ + error: 'invalid_request', + error_description: + 'Missing required parameters: client_id, redirect_uri, response_type', + }); + } - try { - const client = await this.oidc.registerClient({ - clientName: registrationRequest.client_name, - redirectUris: registrationRequest.redirect_uris, - responseTypes: registrationRequest.response_types, - grantTypes: registrationRequest.grant_types, - scope: registrationRequest.scope, - }); + try { + const result = await this.oidc.createAuthorizationSession({ + clientId: clientId as string, + redirectUri: redirectUri as string, + responseType: responseType as string, + scope: scope as string, + state: state as string, + nonce: nonce as string, + codeChallenge: codeChallenge as string, + codeChallengeMethod: codeChallengeMethod as string, + }); - res.status(201).json({ - client_id: client.clientId, - redirect_uris: client.redirectUris, - client_secret: client.clientSecret, - }); - } catch (e) { - const description = isError(e) ? e.message : 'Unknown error'; - this.logger.error(`Failed to register client: ${description}`, e); + // todo(blam): maybe this URL could be overridable by config if + // the plugin is mounted somewhere else? + // support slashes in baseUrl? + const authSessionRedirectUrl = new URL( + `/auth/sessions/${result.id}`, + this.appUrl, + ); - res.status(500).json({ - error: 'server_error', - error_description: `Failed to register client: ${description}`, - }); - } - }); + return res.redirect(authSessionRedirectUrl.toString()); + } catch (error) { + const errorParams = new URLSearchParams(); + errorParams.append( + 'error', + isError(error) ? error.name : 'server_error', + ); + errorParams.append( + 'error_description', + isError(error) ? error.message : 'Unknown error', + ); + if (state) { + errorParams.append('state', state as string); + } + + const redirectUrl = new URL(redirectUri as string); + redirectUrl.search = errorParams.toString(); + return res.redirect(redirectUrl.toString()); + } + }); + + // Authorization Session request details endpoint + // Returns Authorization Session request details for the frontned + router.get('/v1/sessions/:sessionId', async (req, res) => { + const { sessionId } = req.params; + + if (!sessionId) { + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing Authorization Session ID', + }); + } + + try { + const session = await this.oidc.getAuthorizationSession({ + sessionId, + }); + + return res.json({ + id: session.id, + clientName: session.clientName, + scope: session.scope, + redirectUri: session.redirectUri, + }); + } catch (error) { + const description = isError(error) ? error.message : 'Unknown error'; + this.logger.error( + `Failed to get authorization session: ${description}`, + error, + ); + return res.status(404).json({ + error: 'not_found', + error_description: description, + }); + } + }); + + // Authorization Session approval endpoint + // Handles user approval of Authorization Session requests and generates authorization codes + router.post('/v1/sessions/:sessionId/approve', async (req, res) => { + const { sessionId } = req.params; + + if (!sessionId) { + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing authorization session ID', + }); + } + + try { + const httpCredentials = await this.httpAuth.credentials(req); + + if (!this.auth.isPrincipal(httpCredentials, 'user')) { + return res.status(401).json({ + error: 'unauthorized', + error_description: 'Authentication required', + }); + } + + const userEntityRef = httpCredentials.principal.userEntityRef; + + const result = await this.oidc.approveAuthorizationSession({ + sessionId, + userEntityRef, + }); + + return res.json({ + redirectUrl: result.redirectUrl, + }); + } catch (error) { + const description = isError(error) ? error.message : 'Unknown error'; + this.logger.error( + `Failed to approve authorization session: ${description}`, + error, + ); + return res.status(400).json({ + error: 'invalid_request', + error_description: description, + }); + } + }); + + // Authorization Session rejection endpoint + // Handles user rejection of Authorization Session requests and redirects with error + router.post('/v1/sessions/:sessionId/reject', async (req, res) => { + const { sessionId } = req.params; + + if (!sessionId) { + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing authorization session ID', + }); + } + + try { + const session = await this.oidc.getAuthorizationSession({ + sessionId, + }); + + await this.oidc.rejectAuthorizationSession({ sessionId }); + + const errorParams = new URLSearchParams(); + errorParams.append('error', 'access_denied'); + errorParams.append('error_description', 'User denied the request'); + if (session.state) { + errorParams.append('state', session.state); + } + + const redirectUrl = new URL(session.redirectUri); + redirectUrl.search = errorParams.toString(); + + return res.json({ + redirectUrl: redirectUrl.toString(), + }); + } catch (error) { + const description = isError(error) ? error.message : 'Unknown error'; + this.logger.error( + `Failed to reject authorization session: ${description}`, + error, + ); + + return res.status(400).json({ + error: 'invalid_request', + error_description: description, + }); + } + }); + + // Token endpoint + // https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest + // Exchanges authorization codes for access tokens and ID tokens + router.post('/v1/token', async (req, res) => { + // todo(blam): maybe add zod types for validating input + const { + grant_type: grantType, + code, + redirect_uri: redirectUri, + code_verifier: codeVerifier, + } = req.body; + + if (!grantType || !code || !redirectUri) { + this.logger.error( + `Failed to exchange code for token: Missing required parameters`, + ); + return res.status(400).json({ + error: 'invalid_request', + error_description: 'Missing required parameters', + }); + } + + try { + const result = await this.oidc.exchangeCodeForToken({ + code, + redirectUri, + codeVerifier, + grantType, + }); + + return res.json({ + access_token: result.accessToken, + token_type: result.tokenType, + expires_in: result.expiresIn, + id_token: result.idToken, + scope: result.scope, + }); + } catch (error) { + const description = isError(error) ? error.message : 'Unknown error'; + this.logger.error( + `Failed to exchange code for token: ${description}`, + error, + ); + + if (isError(error)) { + if (error.name === 'AuthenticationError') { + return res.status(401).json({ + error: 'invalid_client', + error_description: error.message, + }); + } + if (error.name === 'InputError') { + return res.status(400).json({ + error: 'invalid_request', + error_description: error.message, + }); + } + } + + return res.status(500).json({ + error: 'server_error', + error_description: description, + }); + } + }); + + // Dynamic Client Registration endpoint + // https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration + // Allows clients to register themselves dynamically with the provider + router.post('/v1/register', async (req, res) => { + // todo(blam): maybe add zod types for validating input + const registrationRequest = req.body; + + if (!registrationRequest.redirect_uris?.length) { + res.status(400).json({ + error: 'invalid_request', + error_description: 'redirect_uris is required', + }); + return; + } + + try { + const client = await this.oidc.registerClient({ + clientName: registrationRequest.client_name, + redirectUris: registrationRequest.redirect_uris, + responseTypes: registrationRequest.response_types, + grantTypes: registrationRequest.grant_types, + scope: registrationRequest.scope, + }); + + res.status(201).json({ + client_id: client.clientId, + redirect_uris: client.redirectUris, + client_secret: client.clientSecret, + }); + } catch (e) { + const description = isError(e) ? e.message : 'Unknown error'; + this.logger.error(`Failed to register client: ${description}`, e); + + res.status(500).json({ + error: 'server_error', + error_description: `Failed to register client: ${description}`, + }); + } + }); + } return router; } diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 1f5fea7cb5..d2790ff35c 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -162,6 +162,10 @@ export async function createRouter( oidc, logger, httpAuth, + enableDynamicClientRegistration: + config.getOptionalBoolean( + 'auth.experimental.enableDynamicClientRegistration', + ) ?? false, }); router.use(oidcRouter.getRouter()); diff --git a/plugins/mcp-actions-backend/src/plugin.ts b/plugins/mcp-actions-backend/src/plugin.ts index 2e29847964..bcac77921c 100644 --- a/plugins/mcp-actions-backend/src/plugin.ts +++ b/plugins/mcp-actions-backend/src/plugin.ts @@ -17,7 +17,8 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { json, Router } from 'express'; +import { json } from 'express'; +import Router from 'express-promise-router'; import { McpService } from './services/McpService'; import { createStreamableRouter } from './routers/createStreamableRouter'; import { createSseRouter } from './routers/createSseRouter'; @@ -44,6 +45,7 @@ export const mcpPlugin = createBackendPlugin({ registry: actionsRegistryServiceRef, rootRouter: coreServices.rootHttpRouter, discovery: coreServices.discovery, + config: coreServices.rootConfig, }, async init({ actions, @@ -52,6 +54,7 @@ export const mcpPlugin = createBackendPlugin({ httpAuth, rootRouter, discovery, + config, }) { const mcpService = await McpService.create({ actions, @@ -76,21 +79,25 @@ export const mcpPlugin = createBackendPlugin({ httpRouter.use(router); - // todo(blam): there's probably a better way to proxy this, but it's required - // for mcp auth spec that it lives on the root of the mcp entrypoint server. - const authRouter = Router(); - authRouter.use('/', async (_, res) => { - const authBaseUrl = await discovery.getBaseUrl('auth'); + if ( + config.getOptionalBoolean( + 'auth.experimental.enableDynamicClientRegistration', + ) + ) { + // This should be replaced with throwing a WWW-Authenticate header, but that doesn't seem to be supported by + // many of the MCP client as of yet. So this seems to be the oldest version of the spec thats implemented. + rootRouter.use( + '/.well-known/oauth-authorization-server', + async (_, res) => { + const authBaseUrl = await discovery.getBaseUrl('auth'); + const oidcResponse = await fetch( + `${authBaseUrl}/.well-known/openid-configuration`, + ); - const oidcResponse = await fetch( - `${authBaseUrl}/.well-known/openid-configuration`, + res.json(await oidcResponse.json()); + }, ); - - const oidcResponseJson = await oidcResponse.json(); - - res.json(oidcResponseJson); - }); - rootRouter.use('/.well-known/oauth-authorization-server', authRouter); + } }, }); }, From 75b5880cb790721b5d9af691ff53d0eb593b8f24 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 8 Sep 2025 15:09:30 +0200 Subject: [PATCH 123/233] chore: Fixing changesets ] Signed-off-by: benjdlambert --- .changeset/eleven-doors-down.md | 2 +- .changeset/eleven-doors-own.md | 2 +- packages/backend-test-utils/src/database/TestDatabases.ts | 2 +- plugins/auth-backend/src/database/OidcDatabase.test.ts | 2 ++ plugins/auth-backend/src/service/OidcRouter.test.ts | 2 ++ 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.changeset/eleven-doors-down.md b/.changeset/eleven-doors-down.md index 47cb99dd03..a253828c54 100644 --- a/.changeset/eleven-doors-down.md +++ b/.changeset/eleven-doors-down.md @@ -2,4 +2,4 @@ '@backstage/plugin-mcp-actions-backend': patch --- -Proxy `/.well-known/oauth-authorization-server` to `/.well-known/openid-configuration` on `auth-backend` +Proxy `/.well-known/oauth-authorization-server` to `/.well-known/openid-configuration` on `auth-backend` when `auth.experimental.enableDynamicClientRegistration` is enabled. diff --git a/.changeset/eleven-doors-own.md b/.changeset/eleven-doors-own.md index 1308aab3eb..8c83082b22 100644 --- a/.changeset/eleven-doors-own.md +++ b/.changeset/eleven-doors-own.md @@ -2,4 +2,4 @@ '@backstage/plugin-auth-backend': patch --- -Implementing Dynamic Client Registration with the OIDC server +Implementing Dynamic Client Registration with the OIDC server. You can enable this by setting `auth.experimental.enableDynamicClientRegistration` in `app-config.yaml`. This is highly experimental, but feedback welcome. diff --git a/packages/backend-test-utils/src/database/TestDatabases.ts b/packages/backend-test-utils/src/database/TestDatabases.ts index 00fae4130a..cd20e2997b 100644 --- a/packages/backend-test-utils/src/database/TestDatabases.ts +++ b/packages/backend-test-utils/src/database/TestDatabases.ts @@ -104,7 +104,7 @@ export class TestDatabases { if (supportedIds.length > 0) { afterAll(async () => { await databases.shutdown(); - }, 30_000); + }); } return databases; diff --git a/plugins/auth-backend/src/database/OidcDatabase.test.ts b/plugins/auth-backend/src/database/OidcDatabase.test.ts index 9965069acf..19285aec0a 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.test.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.test.ts @@ -18,6 +18,8 @@ import { AuthDatabase } from './AuthDatabase'; import { OidcDatabase } from './OidcDatabase'; import { resolvePackagePath } from '@backstage/backend-plugin-api'; +jest.setTimeout(60_000); + describe('Oidc Database', () => { const databases = TestDatabases.create(); diff --git a/plugins/auth-backend/src/service/OidcRouter.test.ts b/plugins/auth-backend/src/service/OidcRouter.test.ts index 8714eb7f62..bffe9ee509 100644 --- a/plugins/auth-backend/src/service/OidcRouter.test.ts +++ b/plugins/auth-backend/src/service/OidcRouter.test.ts @@ -34,6 +34,8 @@ import { AuthDatabase } from '../database/AuthDatabase'; import { OidcService } from '../service/OidcService'; import { TokenIssuer } from '../identity/types'; +jest.setTimeout(60_000); + describe('OidcRouter', () => { const MOCK_USER_TOKEN = 'mock-user-token'; const MOCK_USER_ENTITY_REF = 'user:default/test-user'; From a4b9f94d4f4358084f13b47c7931eaec68d13274 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 8 Sep 2025 15:14:05 +0200 Subject: [PATCH 124/233] chore: fix experimental flag Signed-off-by: benjdlambert --- .changeset/eleven-doors-down.md | 2 +- .changeset/eleven-doors-own.md | 2 +- plugins/auth-backend/src/service/router.ts | 2 +- plugins/mcp-actions-backend/src/plugin.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/eleven-doors-down.md b/.changeset/eleven-doors-down.md index a253828c54..0d380c8ddf 100644 --- a/.changeset/eleven-doors-down.md +++ b/.changeset/eleven-doors-down.md @@ -2,4 +2,4 @@ '@backstage/plugin-mcp-actions-backend': patch --- -Proxy `/.well-known/oauth-authorization-server` to `/.well-known/openid-configuration` on `auth-backend` when `auth.experimental.enableDynamicClientRegistration` is enabled. +Proxy `/.well-known/oauth-authorization-server` to `/.well-known/openid-configuration` on `auth-backend` when `auth.experimentalDynamicClientRegistration.enabled` is enabled. diff --git a/.changeset/eleven-doors-own.md b/.changeset/eleven-doors-own.md index 8c83082b22..1da0297e5c 100644 --- a/.changeset/eleven-doors-own.md +++ b/.changeset/eleven-doors-own.md @@ -2,4 +2,4 @@ '@backstage/plugin-auth-backend': patch --- -Implementing Dynamic Client Registration with the OIDC server. You can enable this by setting `auth.experimental.enableDynamicClientRegistration` in `app-config.yaml`. This is highly experimental, but feedback welcome. +Implementing Dynamic Client Registration with the OIDC server. You can enable this by setting `auth.experimentalDynamicClientRegistration.enabled` in `app-config.yaml`. This is highly experimental, but feedback welcome. diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index d2790ff35c..0d5b26078d 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -164,7 +164,7 @@ export async function createRouter( httpAuth, enableDynamicClientRegistration: config.getOptionalBoolean( - 'auth.experimental.enableDynamicClientRegistration', + 'auth.experimentalDynamicClientRegistration.enabled', ) ?? false, }); diff --git a/plugins/mcp-actions-backend/src/plugin.ts b/plugins/mcp-actions-backend/src/plugin.ts index bcac77921c..d04df6cdf5 100644 --- a/plugins/mcp-actions-backend/src/plugin.ts +++ b/plugins/mcp-actions-backend/src/plugin.ts @@ -81,7 +81,7 @@ export const mcpPlugin = createBackendPlugin({ if ( config.getOptionalBoolean( - 'auth.experimental.enableDynamicClientRegistration', + 'auth.experimentalDynamicClientRegistration.enabled', ) ) { // This should be replaced with throwing a WWW-Authenticate header, but that doesn't seem to be supported by From ff15f3032970aa35015ce245f68ba00f03fc3283 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 8 Sep 2025 17:48:33 +0200 Subject: [PATCH 125/233] feat: implementing fixes for wildcard matching for callback URLs Signed-off-by: benjdlambert --- .../src/service/OidcRouter.test.ts | 14 ++++++- .../auth-backend/src/service/OidcRouter.ts | 15 ++++--- .../src/service/OidcService.test.ts | 42 +++++++++++++++++++ .../auth-backend/src/service/OidcService.ts | 22 ++++++++-- plugins/auth-backend/src/service/router.ts | 5 +-- 5 files changed, 84 insertions(+), 14 deletions(-) diff --git a/plugins/auth-backend/src/service/OidcRouter.test.ts b/plugins/auth-backend/src/service/OidcRouter.test.ts index bffe9ee509..81430ef05e 100644 --- a/plugins/auth-backend/src/service/OidcRouter.test.ts +++ b/plugins/auth-backend/src/service/OidcRouter.test.ts @@ -70,6 +70,15 @@ describe('OidcRouter', () => { const mockAuth = mockServices.auth.mock(); const mockHttpAuth = mockServices.httpAuth.mock(); + const mockConfig = mockServices.rootConfig({ + data: { + auth: { + experimentalDynamicClientRegistration: { + enabled: true, + }, + }, + }, + }); const oidcService = OidcService.create({ auth: mockAuth, @@ -77,6 +86,7 @@ describe('OidcRouter', () => { baseUrl: 'http://localhost:7000', userInfo: userInfoDatabase, oidc: oidcDatabase, + config: mockConfig, }); const oidcRouter = OidcRouter.create({ @@ -88,7 +98,7 @@ describe('OidcRouter', () => { userInfo: userInfoDatabase, oidc: oidcDatabase, httpAuth: mockHttpAuth, - enableDynamicClientRegistration: true, + config: mockConfig, }); return { @@ -303,7 +313,7 @@ describe('OidcRouter', () => { .expect(302); expect(response.header.location).toMatch( - /^http:\/\/localhost:3000\/auth\/sessions\/[a-f0-9-]+$/, + /^http:\/\/localhost:3000\/oauth2\/authorize\/[a-f0-9-]+$/, ); }); diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index 6c39115528..7090fadf0b 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -20,6 +20,7 @@ import { AuthService, HttpAuthService, LoggerService, + RootConfigService, } from '@backstage/backend-plugin-api'; import { TokenIssuer } from '../identity/types'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; @@ -33,7 +34,7 @@ export class OidcRouter { private readonly auth: AuthService, private readonly appUrl: string, private readonly httpAuth: HttpAuthService, - private readonly enableDynamicClientRegistration: boolean, + private readonly config: RootConfigService, ) {} static create(options: { @@ -45,7 +46,7 @@ export class OidcRouter { userInfo: UserInfoDatabase; oidc: OidcDatabase; httpAuth: HttpAuthService; - enableDynamicClientRegistration: boolean; + config: RootConfigService; }) { return new OidcRouter( OidcService.create(options), @@ -53,7 +54,7 @@ export class OidcRouter { options.auth, options.appUrl, options.httpAuth, - options.enableDynamicClientRegistration, + options.config, ); } @@ -97,7 +98,11 @@ export class OidcRouter { res.json(userInfo); }); - if (this.enableDynamicClientRegistration) { + if ( + this.config.getOptionalBoolean( + 'auth.experimentalDynamicClientRegistration.enabled', + ) + ) { // Authorization endpoint // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest // Handles the initial authorization request from the client, validates parameters, @@ -140,7 +145,7 @@ export class OidcRouter { // the plugin is mounted somewhere else? // support slashes in baseUrl? const authSessionRedirectUrl = new URL( - `/auth/sessions/${result.id}`, + `/oauth2/authorize/${result.id}`, this.appUrl, ); diff --git a/plugins/auth-backend/src/service/OidcService.test.ts b/plugins/auth-backend/src/service/OidcService.test.ts index b1f03c68ed..7829be3841 100644 --- a/plugins/auth-backend/src/service/OidcService.test.ts +++ b/plugins/auth-backend/src/service/OidcService.test.ts @@ -63,6 +63,8 @@ describe('OidcService', () => { getUserInfo: jest.fn(), } as unknown as jest.Mocked; + const mockConfig = mockServices.rootConfig.mock(); + return { service: OidcService.create({ auth: mockAuth, @@ -70,11 +72,13 @@ describe('OidcService', () => { baseUrl: 'http://mock-base-url', userInfo: mockUserInfo, oidc: oidcDatabase, + config: mockConfig, }), mocks: { auth: mockAuth, tokenIssuer: mockTokenIssuer, userInfo: mockUserInfo, + config: mockConfig, }, }; } @@ -216,6 +220,44 @@ describe('OidcService', () => { expect(client.clientSecret).toBeDefined(); }); + it('should throw an error for invalid redirect URI', async () => { + const { + service, + mocks: { config }, + } = await createOidcService(databaseId); + + config.getOptionalStringArray.mockReturnValue([ + 'https://example.com/*', + ]); + + await expect( + service.registerClient({ + clientName: 'Test Client', + redirectUris: ['https://invalid.com/callback'], + }), + ).rejects.toThrow('Invalid redirect_uri'); + }); + + it('should create a new client with valid redirect URI', async () => { + const { + service, + mocks: { config }, + } = await createOidcService(databaseId); + + config.getOptionalStringArray.mockReturnValue(['cursor://*']); + + const client = await service.registerClient({ + clientName: 'Test Client', + redirectUris: ['cursor://callback'], + }); + + expect(client).toEqual( + expect.objectContaining({ + redirectUris: ['cursor://callback'], + }), + ); + }); + it('should create a client with default values', async () => { const { service } = await createOidcService(databaseId); diff --git a/plugins/auth-backend/src/service/OidcService.ts b/plugins/auth-backend/src/service/OidcService.ts index 2b7eb40bc9..3139ccc73d 100644 --- a/plugins/auth-backend/src/service/OidcService.ts +++ b/plugins/auth-backend/src/service/OidcService.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AuthService } from '@backstage/backend-plugin-api'; +import { AuthService, RootConfigService } from '@backstage/backend-plugin-api'; import { TokenIssuer } from '../identity/types'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; import { @@ -33,6 +33,7 @@ export class OidcService { private readonly baseUrl: string, private readonly userInfo: UserInfoDatabase, private readonly oidc: OidcDatabase, + private readonly config: RootConfigService, ) {} static create(options: { @@ -41,6 +42,7 @@ export class OidcService { baseUrl: string; userInfo: UserInfoDatabase; oidc: OidcDatabase; + config: RootConfigService; }) { return new OidcService( options.auth, @@ -48,6 +50,7 @@ export class OidcService { options.baseUrl, options.userInfo, options.oidc, + options.config, ); } @@ -116,8 +119,21 @@ export class OidcService { const generatedClientId = crypto.randomUUID(); const generatedClientSecret = crypto.randomUUID(); - // todo(blam): add validation for redirectUris here. - // should be a list of urls and / or allowed schemes or something. + const allowedRedirectUriPatterns = this.config.getOptionalStringArray( + 'auth.experimentalDynamicClientRegistration.allowedRedirectUriPatterns', + ); + + if (allowedRedirectUriPatterns) { + for (const redirectUri of opts.redirectUris ?? []) { + if ( + !allowedRedirectUriPatterns.some(pattern => + new RegExp(pattern).test(redirectUri), + ) + ) { + throw new InputError('Invalid redirect_uri'); + } + } + } return await this.oidc.createClient({ clientId: generatedClientId, diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 0d5b26078d..0f0d5b2830 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -162,10 +162,7 @@ export async function createRouter( oidc, logger, httpAuth, - enableDynamicClientRegistration: - config.getOptionalBoolean( - 'auth.experimentalDynamicClientRegistration.enabled', - ) ?? false, + config, }); router.use(oidcRouter.getRouter()); From a2be46e16b8f820fae952e149cb69afe3e20527d Mon Sep 17 00:00:00 2001 From: Shijun Wang Date: Tue, 9 Sep 2025 08:33:25 +0300 Subject: [PATCH 126/233] return progress bar instead of null Signed-off-by: Shijun Wang --- plugins/home/report.api.md | 2 +- .../src/components/CustomHomepage/CustomHomepageGrid.tsx | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/home/report.api.md b/plugins/home/report.api.md index 73c6d035cc..dd33b952ee 100644 --- a/plugins/home/report.api.md +++ b/plugins/home/report.api.md @@ -84,7 +84,7 @@ export const createCardExtension: typeof createCardExtension_2; // @public export const CustomHomepageGrid: ( props: CustomHomepageGridProps, -) => JSX_2.Element | null; +) => JSX_2.Element; // @public export type CustomHomepageGridProps = { diff --git a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx index b4b674be1b..fec7895170 100644 --- a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx +++ b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx @@ -34,7 +34,11 @@ import { } from '@material-ui/core/styles'; import { compact } from 'lodash'; import useObservable from 'react-use/esm/useObservable'; -import { ContentHeader, ErrorBoundary } from '@backstage/core-components'; +import { + ContentHeader, + ErrorBoundary, + Progress, +} from '@backstage/core-components'; import Typography from '@material-ui/core/Typography'; import { WidgetSettingsOverlay } from './WidgetSettingsOverlay'; import { AddWidgetDialog } from './AddWidgetDialog'; @@ -325,9 +329,8 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => { ); }; - // Don't render anything while storage is loading if (isStorageLoading) { - return null; + return ; } return ( From 70a332477f5f707f9be89d5263faf2758a3f7e48 Mon Sep 17 00:00:00 2001 From: gyan Date: Tue, 9 Sep 2025 11:09:48 +0530 Subject: [PATCH 127/233] report-alpha.api.md Signed-off-by: gyan --- plugins/user-settings/report-alpha.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/user-settings/report-alpha.api.md b/plugins/user-settings/report-alpha.api.md index 5a28aacc02..1c8e844d05 100644 --- a/plugins/user-settings/report-alpha.api.md +++ b/plugins/user-settings/report-alpha.api.md @@ -123,7 +123,7 @@ export const userSettingsTranslationRef: TranslationRef< readonly 'languageToggle.select': 'Select language {{language}}'; readonly 'languageToggle.title': 'Language'; readonly 'languageToggle.description': 'Change the language'; - readonly 'themeToggle.select': 'Select theme {{theme}}'; + readonly 'themeToggle.select': 'Select {{theme}}'; readonly 'themeToggle.title': 'Theme'; readonly 'themeToggle.description': 'Change the theme mode'; readonly 'themeToggle.names.auto': 'Auto'; From a79d7cbe36e701d88e024b698ac0b0fe11fcd0f5 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 9 Sep 2025 10:32:05 +0200 Subject: [PATCH 128/233] chore: cleanup a little bit Signed-off-by: benjdlambert --- .../operations/stitcher/markForStitching.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts index 913936202f..484d6e4ed6 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/markForStitching.ts @@ -17,7 +17,9 @@ import { Knex } from 'knex'; import splitToChunks from 'lodash/chunk'; import { v4 as uuid } from 'uuid'; +import { ErrorLike, isError } from '@backstage/errors'; import { StitchingStrategy } from '../../../stitching/types'; +import { setTimeout as sleep } from 'timers/promises'; import { DbFinalEntitiesRow, DbRefreshStateRow } from '../../tables'; const UPDATE_CHUNK_SIZE = 100; // Smaller chunks reduce contention @@ -30,10 +32,13 @@ const POSTGRES_DEADLOCK_SQLSTATE = '40P01'; /** * Checks if the given error is a deadlock error for the database engine in use. */ -function isDeadlockError(knex: Knex | Knex.Transaction, e: unknown): boolean { +function isDeadlockError( + knex: Knex | Knex.Transaction, + e: unknown, +): e is ErrorLike { if (knex.client.config.client.includes('pg')) { // PostgreSQL deadlock detection - return (e as any)?.code === POSTGRES_DEADLOCK_SQLSTATE; + return isError(e) && e.code === POSTGRES_DEADLOCK_SQLSTATE; } // Add more database engine checks here as needed @@ -149,7 +154,7 @@ async function retryOnDeadlock( for (;;) { try { return await fn(); - } catch (e: any) { + } catch (e: unknown) { if (isDeadlockError(knex, e) && attempt < retries) { await sleep(baseMs * Math.pow(2, attempt)); attempt++; @@ -159,7 +164,3 @@ async function retryOnDeadlock( } } } - -function sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); -} From c9f1fb203a0105b0747fd6d047c45aa59f607f88 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 9 Sep 2025 10:46:45 +0200 Subject: [PATCH 129/233] chore: cleanup Signed-off-by: benjdlambert --- app-config.yaml | 7 ++++-- .../auth-backend/src/service/OidcRouter.ts | 22 ++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index e32609c1a1..eacf9a96aa 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -209,8 +209,11 @@ scaffolder: defaultCommitMessage: 'Initial commit' auth: - experimental: - enableDynamicClientRegistration: true + experimentalDynamicClientRegistration: + enabled: true + allowedRedirectUriPatterns: + - cursor://* + ### Add auth.keyStore.provider to more granularly control how to store JWK data when running # the auth-backend. # diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index 7090fadf0b..f20b5f4ede 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -228,7 +228,7 @@ export class OidcRouter { }); } - const userEntityRef = httpCredentials.principal.userEntityRef; + const { userEntityRef } = httpCredentials.principal; const result = await this.oidc.approveAuthorizationSession({ sessionId, @@ -368,9 +368,15 @@ export class OidcRouter { // Allows clients to register themselves dynamically with the provider router.post('/v1/register', async (req, res) => { // todo(blam): maybe add zod types for validating input - const registrationRequest = req.body; + const { + client_name: clientName, + redirect_uris: redirectUris, + response_types: responseTypes, + grant_types: grantTypes, + scope, + } = req.body; - if (!registrationRequest.redirect_uris?.length) { + if (!redirectUris?.length) { res.status(400).json({ error: 'invalid_request', error_description: 'redirect_uris is required', @@ -380,11 +386,11 @@ export class OidcRouter { try { const client = await this.oidc.registerClient({ - clientName: registrationRequest.client_name, - redirectUris: registrationRequest.redirect_uris, - responseTypes: registrationRequest.response_types, - grantTypes: registrationRequest.grant_types, - scope: registrationRequest.scope, + clientName, + redirectUris, + responseTypes, + grantTypes, + scope, }); res.status(201).json({ From 1d15f6b01baaefdd8b0b3c4c047382c7a5f84270 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Thu, 7 Aug 2025 06:20:45 -0500 Subject: [PATCH 130/233] Removed references to Lerna as it's not used Signed-off-by: Andre Wanlin --- docs/contribute/project-structure.md | 4 ---- docs/getting-started/index.md | 4 ++-- docs/references/glossary.md | 2 +- lerna.json | 6 ------ packages/config-loader/src/schema/collect.test.ts | 10 ---------- 5 files changed, 3 insertions(+), 23 deletions(-) delete mode 100644 lerna.json diff --git a/docs/contribute/project-structure.md b/docs/contribute/project-structure.md index 5b86b655b2..f58afedee8 100644 --- a/docs/contribute/project-structure.md +++ b/docs/contribute/project-structure.md @@ -221,7 +221,3 @@ future. - [`catalog-info.yaml`](https://github.com/backstage/backstage/tree/master/catalog-info.yaml) - Description of Backstage in the Backstage Entity format. - -- [`lerna.json`](https://github.com/backstage/backstage/tree/master/lerna.json) - - [Lerna](https://github.com/lerna/lerna) monorepo config. We are using - `yarn workspaces`, so this will only be used for executing scripts. diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index cd86aa3343..9fb29f693e 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -89,8 +89,8 @@ app - **package.json**: Root package.json for the project. _Note: Be sure that you don't add any npm dependencies here as they probably should be installed in the intended workspace rather than in the root._ -- **packages/**: Lerna leaf packages or "workspaces". Everything here is going - to be a separate package, managed by lerna. +- **packages/**: Yarn workspaces, everything here is going + to be a separate package, managed by Yarn. - **packages/app/**: A fully functioning Backstage frontend app that acts as a good starting point for you to get to know Backstage. - **packages/backend/**: We include a backend that helps power features such as diff --git a/docs/references/glossary.md b/docs/references/glossary.md index c955d9a5ae..ad9e2bbee5 100644 --- a/docs/references/glossary.md +++ b/docs/references/glossary.md @@ -208,7 +208,7 @@ One of the [packages](#package) within a [monorepo](#monorepo). A package may or 1. A single repository for a collection of related software projects, such as all projects belonging to an organization. -2. A project layout that consists of multiple [packages](#package) within a single project, where packages are able to have local dependencies on each other. Often enabled through tooling such as [lerna](https://lerna.js.org/) and [yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/) +2. A project layout that consists of multiple [packages](#package) within a single project, where packages are able to have local dependencies on each other. Often enabled through tooling such as [yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/) ## Name diff --git a/lerna.json b/lerna.json deleted file mode 100644 index 4621689664..0000000000 --- a/lerna.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "packages": ["packages/*", "plugins/*"], - "npmClient": "yarn", - "useWorkspaces": true, - "version": "0.0.0" -} diff --git a/packages/config-loader/src/schema/collect.test.ts b/packages/config-loader/src/schema/collect.test.ts index 53db5c6da7..d48c98fbe4 100644 --- a/packages/config-loader/src/schema/collect.test.ts +++ b/packages/config-loader/src/schema/collect.test.ts @@ -41,16 +41,6 @@ describe('collectConfigSchemas', () => { mockDir.clear(); }); - it('should not find any schemas without packages', async () => { - mockDir.setContent({ - 'lerna.json': JSON.stringify({ - packages: ['packages/*'], - }), - }); - - await expect(collectConfigSchemas([], [])).resolves.toEqual([]); - }); - it('should find schema in a local package', async () => { mockDir.setContent({ node_modules: { From ec6cb6bce220e854674001e54aefecd96f7c0962 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 9 Sep 2025 13:17:00 +0200 Subject: [PATCH 131/233] chore: code review comments Signed-off-by: benjdlambert --- ...0250909120000_oidc_client_registration.js} | 0 .../auth-backend/src/database/OidcDatabase.ts | 36 +++-- plugins/auth-backend/src/migrations.test.ts | 140 ++++++++++++++++++ .../src/service/OidcRouter.test.ts | 49 +++--- .../auth-backend/src/service/OidcRouter.ts | 37 +++-- .../src/service/OidcService.test.ts | 67 ++++----- .../auth-backend/src/service/OidcService.ts | 95 +----------- 7 files changed, 247 insertions(+), 177 deletions(-) rename plugins/auth-backend/migrations/{20250701120000_oidc_client_registration.js => 20250909120000_oidc_client_registration.js} (100%) diff --git a/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js b/plugins/auth-backend/migrations/20250909120000_oidc_client_registration.js similarity index 100% rename from plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js rename to plugins/auth-backend/migrations/20250909120000_oidc_client_registration.js diff --git a/plugins/auth-backend/src/database/OidcDatabase.ts b/plugins/auth-backend/src/database/OidcDatabase.ts index ec9a879803..ebb6619400 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.ts @@ -202,14 +202,24 @@ export class OidcDatabase { }); } - const [updated] = await this.db( + const returnedRows = await this.db( 'oauth_authorization_sessions', ) .where('id', session.id) .update(updatedFields) .returning('*'); - return this.rowToAuthorizationSession(updated) as AuthorizationSession; + if (returnedRows.length !== 1) { + throw new Error( + `Failed to retrieve updated authorization session with id ${session.id}`, + ); + } + + const [returnedSession] = returnedRows; + + return this.rowToAuthorizationSession( + returnedSession, + ) as AuthorizationSession; } async getAuthorizationSession({ id }: { id: string }) { @@ -290,17 +300,25 @@ export class OidcDatabase { }); } - const [updated] = await this.db( + const returnedRows = await this.db( 'oidc_authorization_codes', ) .where('code', authorizationCode.code) .update(updatedFields) .returning('*'); - return this.rowToAuthorizationCode(updated) as AuthorizationCode; + if (returnedRows.length !== 1) { + throw new Error( + `Failed to retrieve updated authorization code with code ${authorizationCode.code}`, + ); + } + + const [returnedCode] = returnedRows; + + return this.rowToAuthorizationCode(returnedCode) as AuthorizationCode; } - private rowToClient(row: Partial): Partial { + private rowToClient(row: OidcClientRow): Client { return { clientId: row.client_id, clientName: row.client_name, @@ -332,12 +350,12 @@ export class OidcDatabase { code_challenge_method: session.codeChallengeMethod, nonce: session.nonce, status: session.status, - expires_at: session.expiresAt, + expires_at: toDate(session.expiresAt), }; } private rowToAuthorizationSession( - row: Partial, + row: OAuthAuthorizationSessionRow, ): Partial { return { id: row.id, @@ -361,13 +379,13 @@ export class OidcDatabase { return { code: authorizationCode.code, session_id: authorizationCode.sessionId, - expires_at: authorizationCode.expiresAt, + expires_at: toDate(authorizationCode.expiresAt), used: authorizationCode.used, }; } private rowToAuthorizationCode( - row: Partial, + row: OidcAuthorizationCodeRow, ): Partial { return { code: row.code, diff --git a/plugins/auth-backend/src/migrations.test.ts b/plugins/auth-backend/src/migrations.test.ts index f9575c70fd..7cb6f982ea 100644 --- a/plugins/auth-backend/src/migrations.test.ts +++ b/plugins/auth-backend/src/migrations.test.ts @@ -186,4 +186,144 @@ describe('migrations', () => { await knex.destroy(); }, ); + + it.each(databases.eachSupportedId())( + '20250909120000_oidc_client_registration.js, %p', + async databaseId => { + const knex = await databases.init(databaseId); + + await migrateUntilBefore( + knex, + '20250909120000_oidc_client_registration.js', + ); + await migrateUpOnce(knex); + + await knex + .insert({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + client_name: 'Test Client', + response_types: JSON.stringify(['code']), + grant_types: JSON.stringify(['authorization_code']), + redirect_uris: JSON.stringify(['https://example.com/callback']), + scope: 'openid profile', + metadata: JSON.stringify({ description: 'Test client' }), + }) + .into('oidc_clients'); + + await expect( + knex('oidc_clients').where('client_id', 'test-client-id').first(), + ).resolves.toEqual({ + client_id: 'test-client-id', + client_secret: 'test-client-secret', + client_name: 'Test Client', + response_types: JSON.stringify(['code']), + grant_types: JSON.stringify(['authorization_code']), + redirect_uris: JSON.stringify(['https://example.com/callback']), + scope: 'openid profile', + metadata: JSON.stringify({ description: 'Test client' }), + }); + + await knex + .insert({ + id: 'test-session-id', + client_id: 'test-client-id', + user_entity_ref: 'user:default/test-user', + redirect_uri: 'https://example.com/callback', + scope: 'openid', + state: 'test-state', + response_type: 'code', + code_challenge: 'test-challenge', + code_challenge_method: 'S256', + nonce: 'test-nonce', + status: 'pending', + expires_at: new Date(Date.now() + 3600000), + }) + .into('oauth_authorization_sessions'); + + await expect( + knex('oauth_authorization_sessions') + .where('id', 'test-session-id') + .first(), + ).resolves.toEqual( + expect.objectContaining({ + id: 'test-session-id', + client_id: 'test-client-id', + user_entity_ref: 'user:default/test-user', + redirect_uri: 'https://example.com/callback', + scope: 'openid', + state: 'test-state', + response_type: 'code', + code_challenge: 'test-challenge', + code_challenge_method: 'S256', + nonce: 'test-nonce', + status: 'pending', + }), + ); + + await knex + .insert({ + code: 'test-auth-code', + session_id: 'test-session-id', + expires_at: new Date(Date.now() + 600000), + used: false, + }) + .into('oidc_authorization_codes'); + + await expect( + knex('oidc_authorization_codes') + .where('code', 'test-auth-code') + .first(), + ).resolves.toEqual( + expect.objectContaining({ + code: 'test-auth-code', + session_id: 'test-session-id', + }), + ); + + await expect( + knex + .insert({ + id: 'invalid-session', + client_id: 'non-existent-client', + redirect_uri: 'https://example.com/callback', + response_type: 'code', + expires_at: new Date(), + }) + .into('oauth_authorization_sessions'), + ).rejects.toThrow(); + + await expect( + knex + .insert({ + code: 'invalid-code', + session_id: 'non-existent-session', + expires_at: new Date(), + }) + .into('oidc_authorization_codes'), + ).rejects.toThrow(); + + await knex('oauth_authorization_sessions') + .where('id', 'test-session-id') + .del(); + + await expect( + knex('oidc_authorization_codes').where('session_id', 'test-session-id'), + ).resolves.toHaveLength(0); + + await migrateDownOnce(knex); + + const tables = [ + 'oidc_clients', + 'oauth_authorization_sessions', + 'oidc_authorization_codes', + ]; + + for (const table of tables) { + await expect(knex.schema.hasTable(table)).resolves.toBe(false); + } + + await knex.destroy(); + }, + ); }); diff --git a/plugins/auth-backend/src/service/OidcRouter.test.ts b/plugins/auth-backend/src/service/OidcRouter.test.ts index 81430ef05e..de865a9c43 100644 --- a/plugins/auth-backend/src/service/OidcRouter.test.ts +++ b/plugins/auth-backend/src/service/OidcRouter.test.ts @@ -24,6 +24,7 @@ import { startTestBackend, TestDatabases, TestDatabaseId, + mockCredentials, } from '@backstage/backend-test-utils'; import request from 'supertest'; import crypto from 'crypto'; @@ -413,13 +414,9 @@ describe('OidcRouter', () => { ], }); - httpAuth.credentials.mockResolvedValueOnce({ - principal: { - type: 'user', - userEntityRef: 'user:default/test-user', - }, - $$type: '@backstage/BackstageCredentials', - }); + httpAuth.credentials.mockResolvedValueOnce( + mockCredentials.user('user:default/test-user'), + ); auth.isPrincipal.mockReturnValueOnce(true); @@ -437,7 +434,7 @@ describe('OidcRouter', () => { it('should reject auth session', async () => { const { - mocks: { service }, + mocks: { service, httpAuth, auth }, router, } = await createRouter(databaseId); @@ -457,6 +454,12 @@ describe('OidcRouter', () => { state: 'test-state', }); + httpAuth.credentials.mockResolvedValueOnce( + mockCredentials.user('user:default/test-user'), + ); + + auth.isPrincipal.mockReturnValueOnce(true); + const { server } = await startTestBackend({ features: [ createBackendPlugin({ @@ -496,13 +499,9 @@ describe('OidcRouter', () => { router, } = await createRouter(databaseId); - httpAuth.credentials.mockResolvedValueOnce({ - principal: { - type: 'user', - userEntityRef: 'user:default/test-user', - }, - $$type: '@backstage/BackstageCredentials', - }); + httpAuth.credentials.mockResolvedValueOnce( + mockCredentials.user('user:default/test-user'), + ); auth.isPrincipal.mockReturnValueOnce(true); @@ -590,13 +589,9 @@ describe('OidcRouter', () => { token: 'mock-access-token-pkce', }); - httpAuth.credentials.mockResolvedValueOnce({ - principal: { - type: 'user', - userEntityRef: 'user:default/test-user-pkce', - }, - $$type: '@backstage/BackstageCredentials', - }); + httpAuth.credentials.mockResolvedValueOnce( + mockCredentials.user('user:default/test-user-pkce'), + ); auth.isPrincipal.mockReturnValueOnce(true); @@ -725,13 +720,9 @@ describe('OidcRouter', () => { token: 'mock-access-token-s256', }); - httpAuth.credentials.mockResolvedValueOnce({ - principal: { - type: 'user', - userEntityRef: 'user:default/test-user-s256', - }, - $$type: '@backstage/BackstageCredentials', - }); + httpAuth.credentials.mockResolvedValueOnce( + mockCredentials.user('user:default/test-user-s256'), + ); auth.isPrincipal.mockReturnValueOnce(true); diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index f20b5f4ede..9a3308b4eb 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -134,19 +134,19 @@ export class OidcRouter { clientId: clientId as string, redirectUri: redirectUri as string, responseType: responseType as string, - scope: scope as string, - state: state as string, - nonce: nonce as string, - codeChallenge: codeChallenge as string, - codeChallengeMethod: codeChallengeMethod as string, + scope: scope as string | undefined, + state: state as string | undefined, + nonce: nonce as string | undefined, + codeChallenge: codeChallenge as string | undefined, + codeChallengeMethod: codeChallengeMethod as string | undefined, }); // todo(blam): maybe this URL could be overridable by config if // the plugin is mounted somewhere else? // support slashes in baseUrl? const authSessionRedirectUrl = new URL( - `/oauth2/authorize/${result.id}`, - this.appUrl, + `./oauth2/authorize/${result.id}`, + ensureTrailingSlash(this.appUrl), ); return res.redirect(authSessionRedirectUrl.toString()); @@ -171,7 +171,7 @@ export class OidcRouter { }); // Authorization Session request details endpoint - // Returns Authorization Session request details for the frontned + // Returns Authorization Session request details for the frontend router.get('/v1/sessions/:sessionId', async (req, res) => { const { sessionId } = req.params; @@ -263,12 +263,25 @@ export class OidcRouter { }); } + const httpCredentials = await this.httpAuth.credentials(req); + + if (!this.auth.isPrincipal(httpCredentials, 'user')) { + return res.status(401).json({ + error: 'unauthorized', + error_description: 'Authentication required', + }); + } + + const { userEntityRef } = httpCredentials.principal; try { const session = await this.oidc.getAuthorizationSession({ sessionId, }); - await this.oidc.rejectAuthorizationSession({ sessionId }); + await this.oidc.rejectAuthorizationSession({ + sessionId, + userEntityRef, + }); const errorParams = new URLSearchParams(); errorParams.append('error', 'access_denied'); @@ -413,3 +426,9 @@ export class OidcRouter { return router; } } +function ensureTrailingSlash(appUrl: string): string | URL | undefined { + if (appUrl.endsWith('/')) { + return appUrl; + } + return `${appUrl}/`; +} diff --git a/plugins/auth-backend/src/service/OidcService.test.ts b/plugins/auth-backend/src/service/OidcService.test.ts index 7829be3841..328a753e4c 100644 --- a/plugins/auth-backend/src/service/OidcService.test.ts +++ b/plugins/auth-backend/src/service/OidcService.test.ts @@ -466,6 +466,7 @@ describe('OidcService', () => { await service.rejectAuthorizationSession({ sessionId: authSession.id, + userEntityRef: 'user:default/test', }); await expect( @@ -553,6 +554,7 @@ describe('OidcService', () => { await service.rejectAuthorizationSession({ sessionId: authSession.id, + userEntityRef: 'user:default/test', }); await expect( @@ -580,6 +582,7 @@ describe('OidcService', () => { await service.rejectAuthorizationSession({ sessionId: authSession.id, + userEntityRef: 'user:default/test', }); await expect( @@ -595,6 +598,7 @@ describe('OidcService', () => { await expect( service.rejectAuthorizationSession({ sessionId: 'invalid-session', + userEntityRef: 'user:default/test', }), ).rejects.toThrow('Invalid authorization session'); }); @@ -621,6 +625,7 @@ describe('OidcService', () => { await expect( service.rejectAuthorizationSession({ sessionId: authSession.id, + userEntityRef: 'user:default/test', }), ).rejects.toThrow('Authorization session not found or expired'); }); @@ -641,49 +646,15 @@ describe('OidcService', () => { await service.rejectAuthorizationSession({ sessionId: authSession.id, + userEntityRef: 'user:default/test', }); await expect( service.rejectAuthorizationSession({ sessionId: authSession.id, - }), - ).rejects.toThrow('Authorization session not found or expired'); - }); - }); - - describe('authorize', () => { - it('should create direct authorization', async () => { - const { service } = await createOidcService(databaseId); - - const client = await service.registerClient({ - clientName: 'Test Client', - redirectUris: ['https://example.com/callback'], - }); - - const result = await service.authorize({ - clientId: client.clientId, - redirectUri: 'https://example.com/callback', - responseType: 'code', - userEntityRef: 'user:default/test', - state: 'test-state', - }); - - expect(result.redirectUrl).toMatch( - /^https:\/\/example\.com\/callback\?code=.+&state=test-state$/, - ); - }); - - it('should throw error for invalid client', async () => { - const { service } = await createOidcService(databaseId); - - await expect( - service.authorize({ - clientId: 'invalid-client', - redirectUri: 'https://example.com/callback', - responseType: 'code', userEntityRef: 'user:default/test', }), - ).rejects.toThrow('Invalid client_id'); + ).rejects.toThrow('Authorization session not found or expired'); }); }); @@ -698,14 +669,18 @@ describe('OidcService', () => { redirectUris: ['https://example.com/callback'], }); - const authResult = await service.authorize({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', - userEntityRef: 'user:default/test', scope: 'openid', }); + const authResult = await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + const code = new URL(authResult.redirectUrl).searchParams.get('code')!; const tokenResult = await service.exchangeCodeForToken({ @@ -751,15 +726,19 @@ describe('OidcService', () => { .update(codeVerifier) .digest('base64url'); - const authResult = await service.authorize({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', - userEntityRef: 'user:default/test', codeChallenge, codeChallengeMethod: 'S256', }); + const authResult = await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + const code = new URL(authResult.redirectUrl).searchParams.get('code')!; const tokenResult = await service.exchangeCodeForToken({ @@ -781,15 +760,19 @@ describe('OidcService', () => { }); const codeChallenge = 'test-challenge'; - const authResult = await service.authorize({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', - userEntityRef: 'user:default/test', codeChallenge, codeChallengeMethod: 'S256', }); + const authResult = await service.approveAuthorizationSession({ + sessionId: authSession.id, + userEntityRef: 'user:default/test', + }); + const code = new URL(authResult.redirectUrl).searchParams.get('code')!; await expect( diff --git a/plugins/auth-backend/src/service/OidcService.ts b/plugins/auth-backend/src/service/OidcService.ts index 3139ccc73d..e8a308148a 100644 --- a/plugins/auth-backend/src/service/OidcService.ts +++ b/plugins/auth-backend/src/service/OidcService.ts @@ -300,9 +300,14 @@ export class OidcService { }; } - public async rejectAuthorizationSession(opts: { sessionId: string }) { + public async rejectAuthorizationSession(opts: { + sessionId: string; + userEntityRef: string; + }) { + const { sessionId, userEntityRef } = opts; + const session = await this.oidc.getAuthorizationSession({ - id: opts.sessionId, + id: sessionId, }); if (!session) { @@ -320,94 +325,8 @@ export class OidcService { await this.oidc.updateAuthorizationSession({ id: session.id, status: 'rejected', - }); - } - - public async authorize(opts: { - clientId: string; - redirectUri: string; - responseType: string; - scope?: string; - state?: string; - nonce?: string; - codeChallenge?: string; - codeChallengeMethod?: string; - userEntityRef: string; - }) { - const { - clientId, - redirectUri, - responseType, - scope, - state, - nonce, - codeChallenge, - codeChallengeMethod, userEntityRef, - } = opts; - - if (responseType !== 'code') { - throw new InputError('Only authorization code flow is supported'); - } - - const client = await this.oidc.getClient({ clientId }); - if (!client) { - throw new InputError('Invalid client_id'); - } - - if (!client.redirectUris.includes(redirectUri)) { - throw new InputError('Invalid redirect_uri'); - } - - if (codeChallenge) { - if ( - !codeChallengeMethod || - !['S256', 'plain'].includes(codeChallengeMethod) - ) { - throw new InputError('Invalid code_challenge_method'); - } - } - - const sessionId = crypto.randomUUID(); - const sessionExpiresAt = DateTime.now().plus({ hours: 1 }).toJSDate(); - - await this.oidc.createAuthorizationSession({ - id: sessionId, - clientId, - userEntityRef, - redirectUri, - responseType, - scope, - state, - codeChallenge, - codeChallengeMethod, - nonce, - expiresAt: sessionExpiresAt, }); - - await this.oidc.updateAuthorizationSession({ - id: sessionId, - status: 'approved', - }); - - const authorizationCode = crypto.randomBytes(32).toString('base64url'); - const codeExpiresAt = DateTime.now().plus({ minutes: 10 }).toJSDate(); - - await this.oidc.createAuthorizationCode({ - code: authorizationCode, - sessionId, - expiresAt: codeExpiresAt, - }); - - const redirectUrl = new URL(redirectUri); - redirectUrl.searchParams.append('code', authorizationCode); - if (state) { - redirectUrl.searchParams.append('state', state); - } - - return { - redirectUrl: redirectUrl.toString(), - }; } public async exchangeCodeForToken(params: { From 675814c0962df46a37abc30664360bc74c938dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 9 Sep 2025 13:29:58 +0200 Subject: [PATCH 132/233] Update .changeset/warm-emus-itch.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/warm-emus-itch.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/warm-emus-itch.md b/.changeset/warm-emus-itch.md index 3275098425..a7bd51abdb 100644 --- a/.changeset/warm-emus-itch.md +++ b/.changeset/warm-emus-itch.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog': patch --- -Fix incorrect defaultTarget on `createComponentRouteRef`. +Fix incorrect `defaultTarget` on `createComponentRouteRef`. From c2afe12dfd479e95b6cb256c64befcee24a42d0f Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 9 Sep 2025 13:50:38 +0200 Subject: [PATCH 133/233] chore: cleanup a little bit more :tada: Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- ...20250909120000_oidc_client_registration.js | 9 +++++--- plugins/auth-backend/package.json | 1 + plugins/auth-backend/src/migrations.test.ts | 22 ------------------- .../src/service/OidcService.test.ts | 6 ++--- .../auth-backend/src/service/OidcService.ts | 19 ++++++++-------- yarn.lock | 10 +++++++++ 6 files changed, 29 insertions(+), 38 deletions(-) diff --git a/plugins/auth-backend/migrations/20250909120000_oidc_client_registration.js b/plugins/auth-backend/migrations/20250909120000_oidc_client_registration.js index e175c922c1..87391c3467 100644 --- a/plugins/auth-backend/migrations/20250909120000_oidc_client_registration.js +++ b/plugins/auth-backend/migrations/20250909120000_oidc_client_registration.js @@ -44,12 +44,12 @@ exports.up = async function up(knex) { .comment('The name of the client, should be human readable'); table - .text('response_types') + .text('response_types', 'longtext') .notNullable() .comment('JSON array of supported response types'); table - .text('grant_types') + .text('grant_types', 'longtext') .notNullable() .comment('JSON array of supported grant types'); @@ -82,7 +82,10 @@ exports.up = async function up(knex) { .nullable() .comment('Backstage user entity reference'); - table.text('redirect_uri').notNullable().comment('Client redirect URI'); + table + .text('redirect_uri', 'longtext') + .notNullable() + .comment('Client redirect URI'); table.text('scope').nullable().comment('Requested scopes space-separated'); diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 0dddd935e6..9507df7fc3 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -60,6 +60,7 @@ "knex": "^3.0.0", "lodash": "^4.17.21", "luxon": "^3.0.0", + "matcher": "^4.0.0", "minimatch": "^9.0.0", "passport": "^0.7.0", "uuid": "^11.0.0" diff --git a/plugins/auth-backend/src/migrations.test.ts b/plugins/auth-backend/src/migrations.test.ts index 7cb6f982ea..df7063351a 100644 --- a/plugins/auth-backend/src/migrations.test.ts +++ b/plugins/auth-backend/src/migrations.test.ts @@ -281,28 +281,6 @@ describe('migrations', () => { }), ); - await expect( - knex - .insert({ - id: 'invalid-session', - client_id: 'non-existent-client', - redirect_uri: 'https://example.com/callback', - response_type: 'code', - expires_at: new Date(), - }) - .into('oauth_authorization_sessions'), - ).rejects.toThrow(); - - await expect( - knex - .insert({ - code: 'invalid-code', - session_id: 'non-existent-session', - expires_at: new Date(), - }) - .into('oidc_authorization_codes'), - ).rejects.toThrow(); - await knex('oauth_authorization_sessions') .where('id', 'test-session-id') .del(); diff --git a/plugins/auth-backend/src/service/OidcService.test.ts b/plugins/auth-backend/src/service/OidcService.test.ts index 328a753e4c..e4e1673397 100644 --- a/plugins/auth-backend/src/service/OidcService.test.ts +++ b/plugins/auth-backend/src/service/OidcService.test.ts @@ -244,16 +244,16 @@ describe('OidcService', () => { mocks: { config }, } = await createOidcService(databaseId); - config.getOptionalStringArray.mockReturnValue(['cursor://*']); + config.getOptionalStringArray.mockReturnValue(['cursor:*']); const client = await service.registerClient({ clientName: 'Test Client', - redirectUris: ['cursor://callback'], + redirectUris: ['cursor://callback/asd?asd=asd'], }); expect(client).toEqual( expect.objectContaining({ - redirectUris: ['cursor://callback'], + redirectUris: ['cursor://callback/asd?asd=asd'], }), ); }); diff --git a/plugins/auth-backend/src/service/OidcService.ts b/plugins/auth-backend/src/service/OidcService.ts index e8a308148a..b4c6bb122b 100644 --- a/plugins/auth-backend/src/service/OidcService.ts +++ b/plugins/auth-backend/src/service/OidcService.ts @@ -25,6 +25,7 @@ import { decodeJwt } from 'jose'; import crypto from 'crypto'; import { OidcDatabase } from '../database/OidcDatabase'; import { DateTime } from 'luxon'; +import matcher from 'matcher'; export class OidcService { private constructor( @@ -121,17 +122,15 @@ export class OidcService { const allowedRedirectUriPatterns = this.config.getOptionalStringArray( 'auth.experimentalDynamicClientRegistration.allowedRedirectUriPatterns', - ); + ) ?? ['*']; - if (allowedRedirectUriPatterns) { - for (const redirectUri of opts.redirectUris ?? []) { - if ( - !allowedRedirectUriPatterns.some(pattern => - new RegExp(pattern).test(redirectUri), - ) - ) { - throw new InputError('Invalid redirect_uri'); - } + for (const redirectUri of opts.redirectUris ?? []) { + if ( + !allowedRedirectUriPatterns.some(pattern => + matcher.isMatch(redirectUri, pattern), + ) + ) { + throw new InputError('Invalid redirect_uri'); } } diff --git a/yarn.lock b/yarn.lock index 853749b99e..5d78326d94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4196,6 +4196,7 @@ __metadata: knex: "npm:^3.0.0" lodash: "npm:^4.17.21" luxon: "npm:^3.0.0" + matcher: "npm:^4.0.0" minimatch: "npm:^9.0.0" passport: "npm:^0.7.0" supertest: "npm:^7.0.0" @@ -37208,6 +37209,15 @@ __metadata: languageName: node linkType: hard +"matcher@npm:^4.0.0": + version: 4.0.0 + resolution: "matcher@npm:4.0.0" + dependencies: + escape-string-regexp: "npm:^4.0.0" + checksum: 10/d338aff31d8dfd3626873e43777f46b123579734d53bb8d18d64b08a822ba5e8d39f5fe2e23403258e6143aa0cbe20a15662720d825cd0d3af961d5a44230328 + languageName: node + linkType: hard + "material-ui-confirm@npm:^3.0.12": version: 3.0.18 resolution: "material-ui-confirm@npm:3.0.18" From 0ab3ae36541e8beee0a9f52ac8e323268833e14d Mon Sep 17 00:00:00 2001 From: Praphull Purohit <1288395+praphull-purohit@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:10:01 +0200 Subject: [PATCH 134/233] Change from minor to patch Co-authored-by: Paul Schultz Signed-off-by: Praphull Purohit <1288395+praphull-purohit@users.noreply.github.com> --- .changeset/ripe-boxes-stand.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/ripe-boxes-stand.md b/.changeset/ripe-boxes-stand.md index f76f7b2516..9a1e72f052 100644 --- a/.changeset/ripe-boxes-stand.md +++ b/.changeset/ripe-boxes-stand.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-scaffolder-backend-module-github': minor +'@backstage/plugin-scaffolder-backend-module-github': patch --- Add block creations field in github branch protection scaffolder actions From 02110671648a8cd8de00c57c5fd622c5e445a856 Mon Sep 17 00:00:00 2001 From: Praphull Purohit <1288395+praphull-purohit@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:10:28 +0200 Subject: [PATCH 135/233] Remove blockCreations defaulting Apply suggestion from @schultzp2020 Co-authored-by: Paul Schultz Signed-off-by: Praphull Purohit <1288395+praphull-purohit@users.noreply.github.com> --- .../src/actions/githubBranchProtection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts index 6ae0f67135..ee8555661b 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts @@ -77,7 +77,7 @@ export function createGithubBranchProtectionAction(options: { requireLastPushApproval = false, requiredCommitSigning = false, requiredLinearHistory = false, - blockCreations = false, + blockCreations, token: providedToken, } = ctx.input; From 62e3de764c2aa3daf17533c31466be7693b715f2 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 8 Sep 2025 17:25:52 +0200 Subject: [PATCH 136/233] chore: initial plugin fix Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- plugins/auth-backend/package.json | 1 + plugins/auth-node/package.json | 1 + plugins/auth-react/package.json | 1 + plugins/auth/.eslintrc.js | 1 + plugins/auth/README.md | 12 + plugins/auth/catalog-info.yaml | 9 + plugins/auth/dev/index.tsx | 17 + plugins/auth/package.json | 76 ++++ .../components/ConsentPage/ConsentPage.tsx | 348 ++++++++++++++++++ .../auth/src/components/ConsentPage/index.ts | 16 + plugins/auth/src/components/Router.tsx | 29 ++ plugins/auth/src/index.ts | 17 + plugins/auth/src/plugin.test.ts | 22 ++ plugins/auth/src/plugin.tsx | 38 ++ plugins/auth/src/routes.ts | 18 + plugins/auth/src/setupTests.ts | 16 + yarn.lock | 138 ++++++- 17 files changed, 748 insertions(+), 12 deletions(-) create mode 100644 plugins/auth/.eslintrc.js create mode 100644 plugins/auth/README.md create mode 100644 plugins/auth/catalog-info.yaml create mode 100644 plugins/auth/dev/index.tsx create mode 100644 plugins/auth/package.json create mode 100644 plugins/auth/src/components/ConsentPage/ConsentPage.tsx create mode 100644 plugins/auth/src/components/ConsentPage/index.ts create mode 100644 plugins/auth/src/components/Router.tsx create mode 100644 plugins/auth/src/index.ts create mode 100644 plugins/auth/src/plugin.test.ts create mode 100644 plugins/auth/src/plugin.tsx create mode 100644 plugins/auth/src/routes.ts create mode 100644 plugins/auth/src/setupTests.ts diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 9507df7fc3..bf52739ea4 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -6,6 +6,7 @@ "role": "backend-plugin", "pluginId": "auth", "pluginPackages": [ + "@backstage/plugin-auth", "@backstage/plugin-auth-backend", "@backstage/plugin-auth-node", "@backstage/plugin-auth-react" diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index 23cff9720c..a846a868eb 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -5,6 +5,7 @@ "role": "node-library", "pluginId": "auth", "pluginPackages": [ + "@backstage/plugin-auth", "@backstage/plugin-auth-backend", "@backstage/plugin-auth-node", "@backstage/plugin-auth-react" diff --git a/plugins/auth-react/package.json b/plugins/auth-react/package.json index 4335bbc03a..2c6b5d6ff2 100644 --- a/plugins/auth-react/package.json +++ b/plugins/auth-react/package.json @@ -6,6 +6,7 @@ "role": "web-library", "pluginId": "auth", "pluginPackages": [ + "@backstage/plugin-auth", "@backstage/plugin-auth-backend", "@backstage/plugin-auth-node", "@backstage/plugin-auth-react" diff --git a/plugins/auth/.eslintrc.js b/plugins/auth/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth/README.md b/plugins/auth/README.md new file mode 100644 index 0000000000..789ae670b4 --- /dev/null +++ b/plugins/auth/README.md @@ -0,0 +1,12 @@ +# @backstage/plugin-auth + +A Backstage frontend plugin that provides user interface components for authentication flows, specifically for OpenID Connect (OIDC) consent management. + +## Installation + +This plugin is designed to work with the `@backstage/plugin-auth-backend` package that provides OIDC provider functionality. + +```bash +# From your Backstage app directory +yarn --cwd packages/app add @backstage/plugin-auth +``` diff --git a/plugins/auth/catalog-info.yaml b/plugins/auth/catalog-info.yaml new file mode 100644 index 0000000000..66d835a198 --- /dev/null +++ b/plugins/auth/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth + title: '@backstage/plugin-auth' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: auth-maintainers diff --git a/plugins/auth/dev/index.tsx b/plugins/auth/dev/index.tsx new file mode 100644 index 0000000000..04598f0f2e --- /dev/null +++ b/plugins/auth/dev/index.tsx @@ -0,0 +1,17 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// todo diff --git a/plugins/auth/package.json b/plugins/auth/package.json new file mode 100644 index 0000000000..9d3fe7b197 --- /dev/null +++ b/plugins/auth/package.json @@ -0,0 +1,76 @@ +{ + "name": "@backstage/plugin-auth", + "version": "0.1.0", + "license": "Apache-2.0", + "private": true, + "main": "src/index.ts", + "types": "src/index.ts", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "package.json": [ + "package.json" + ] + } + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/auth" + }, + "backstage": { + "role": "frontend-plugin", + "pluginId": "auth", + "pluginPackages": [ + "@backstage/plugin-auth", + "@backstage/plugin-auth-backend", + "@backstage/plugin-auth-node", + "@backstage/plugin-auth-react" + ] + }, + "sideEffects": false, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-compat-api": "workspace:^", + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^17.0.0", + "react-router-dom": "^6.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.0.0", + "msw": "^1.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/auth/src/components/ConsentPage/ConsentPage.tsx b/plugins/auth/src/components/ConsentPage/ConsentPage.tsx new file mode 100644 index 0000000000..3234450a1d --- /dev/null +++ b/plugins/auth/src/components/ConsentPage/ConsentPage.tsx @@ -0,0 +1,348 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useCallback, useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { + Box, + Button, + Card, + CardContent, + CardActions, + Typography, + makeStyles, + Divider, +} from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import CheckCircleIcon from '@material-ui/icons/CheckCircle'; +import CancelIcon from '@material-ui/icons/Cancel'; +import AppsIcon from '@material-ui/icons/Apps'; +import WarningIcon from '@material-ui/icons/Warning'; +import { + Header, + Page, + Content, + Progress, + EmptyState, + ResponseErrorPanel, +} from '@backstage/core-components'; +import { + alertApiRef, + useApi, + fetchApiRef, + discoveryApiRef, +} from '@backstage/core-plugin-api'; +import { isError } from '@backstage/errors'; + +const useStyles = makeStyles(theme => ({ + authCard: { + maxWidth: 600, + margin: '0 auto', + marginTop: theme.spacing(4), + }, + appHeader: { + display: 'flex', + alignItems: 'center', + marginBottom: theme.spacing(2), + }, + appIcon: { + marginRight: theme.spacing(2), + fontSize: 40, + }, + appName: { + fontSize: '1.5rem', + fontWeight: 'bold', + }, + securityWarning: { + margin: theme.spacing(2, 0), + }, + buttonContainer: { + display: 'flex', + justifyContent: 'space-between', + gap: theme.spacing(2), + padding: theme.spacing(2), + }, + callbackUrl: { + fontFamily: 'monospace', + backgroundColor: theme.palette.background.default, + padding: theme.spacing(1), + borderRadius: theme.shape.borderRadius, + wordBreak: 'break-all', + fontSize: '0.875rem', + }, + scopeList: { + backgroundColor: theme.palette.background.default, + borderRadius: theme.shape.borderRadius, + padding: theme.spacing(1), + }, +})); + +interface Session { + id: string; + clientName?: string; + clientId: string; + redirectUri: string; + scopes?: string[]; + responseType?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + expiresAt?: string; +} + +export const ConsentPage = () => { + const classes = useStyles(); + const { sessionId } = useParams<{ sessionId: string }>(); + const alertApi = useApi(alertApiRef); + const fetchApi = useApi(fetchApiRef); + const discoveryApi = useApi(discoveryApiRef); + const [session, setSession] = useState(null); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + const [completed, setCompleted] = useState< + | { + action: 'approve' | 'reject'; + } + | undefined + >(undefined); + + useEffect(() => { + const fetchSession = async () => { + if (!sessionId) return; + + try { + const baseUrl = await discoveryApi.getBaseUrl('auth'); + const response = await fetchApi.fetch( + `${baseUrl}/v1/sessions/${sessionId}`, + ); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + setSession(data); + } catch (err) { + setError(isError(err) ? err.message : 'Failed to load consent request'); + } finally { + setLoading(false); + } + }; + + fetchSession(); + }, [sessionId, discoveryApi, fetchApi]); + + const handleAction = useCallback( + async (action: 'approve' | 'reject') => { + if (!session) return; + + setSubmitting(true); + try { + const baseUrl = await discoveryApi.getBaseUrl('auth'); + const response = await fetchApi.fetch( + `${baseUrl}/v1/sessions/${session.id}/${action}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const result = await response.json(); + + setCompleted({ + action, + }); + + if (result.redirectUrl) { + window.location.href = result.redirectUrl; + } + } catch (err) { + alertApi.post({ + message: isError(err) ? err.message : `Failed to ${action} consent`, + severity: 'error', + }); + } finally { + setSubmitting(false); + } + }, + [session, discoveryApi, fetchApi, alertApi], + ); + + if (!sessionId) { + return ( + +
+ + + + + ); + } + + if (loading) { + return ( + +
+ + + + + + + ); + } + + if (error ?? !session) { + return ( + +
+ + + + + ); + } + + if (completed) { + return ( + +
+ + + + + {completed.action === 'approve' ? ( + + ) : ( + + )} + + {completed.action === 'approve' + ? 'Authorization Approved' + : 'Authorization Denied'} + + + {completed.action === 'approve' + ? 'You have successfully authorized the application to access your Backstage account.' + : 'You have denied the application access to your Backstage account.'} + + + Redirecting to the application... + + + + + + + ); + } + + const appName = session.clientName ?? session.clientId; + + return ( + +
+ + + + + + + {appName} + + wants to access your Backstage account + + + + + + + } + className={classes.securityWarning} + > + + Security Notice: By authorizing this + application, you are granting it access to your Backstage + account. The application will receive an access token that + allows it to act on your behalf. + + + + Callback URL: + + {session.redirectUri} + + + + + + Make sure you trust this application and recognize the callback + URL above. Only authorize applications you trust. + + + + + + + + + + + + ); +}; diff --git a/plugins/auth/src/components/ConsentPage/index.ts b/plugins/auth/src/components/ConsentPage/index.ts new file mode 100644 index 0000000000..9fdd44c96d --- /dev/null +++ b/plugins/auth/src/components/ConsentPage/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { ConsentPage } from './ConsentPage'; diff --git a/plugins/auth/src/components/Router.tsx b/plugins/auth/src/components/Router.tsx new file mode 100644 index 0000000000..fbb5f9aaa9 --- /dev/null +++ b/plugins/auth/src/components/Router.tsx @@ -0,0 +1,29 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Routes, Route } from 'react-router-dom'; +import { ConsentPage } from './ConsentPage'; + +/** + * Router component for the auth plugin + * @public + */ +export const Router = () => { + return ( + + } /> + + ); +}; diff --git a/plugins/auth/src/index.ts b/plugins/auth/src/index.ts new file mode 100644 index 0000000000..d507bac202 --- /dev/null +++ b/plugins/auth/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { authPlugin, AuthRouter } from './plugin'; +export { rootRouteRef } from './routes'; diff --git a/plugins/auth/src/plugin.test.ts b/plugins/auth/src/plugin.test.ts new file mode 100644 index 0000000000..a50b718e5d --- /dev/null +++ b/plugins/auth/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { default as authPlugin } from './plugin'; + +describe('auth', () => { + it('should export plugin', () => { + expect(authPlugin).toBeDefined(); + }); +}); diff --git a/plugins/auth/src/plugin.tsx b/plugins/auth/src/plugin.tsx new file mode 100644 index 0000000000..9b52960f4c --- /dev/null +++ b/plugins/auth/src/plugin.tsx @@ -0,0 +1,38 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { compatWrapper } from '@backstage/core-compat-api'; +import { + createFrontendPlugin, + PageBlueprint, +} from '@backstage/frontend-plugin-api'; +import { rootRouteRef } from './routes'; + +export const AuthPage = PageBlueprint.make({ + params: { + path: '/oauth2', + routeRef: rootRouteRef, + loader: () => + import('./components/Router').then(m => compatWrapper()), + }, +}); + +export default createFrontendPlugin({ + pluginId: 'auth', + extensions: [AuthPage], + routes: { + root: rootRouteRef, + }, +}); diff --git a/plugins/auth/src/routes.ts b/plugins/auth/src/routes.ts new file mode 100644 index 0000000000..09e6a48da4 --- /dev/null +++ b/plugins/auth/src/routes.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createRouteRef } from '@backstage/frontend-plugin-api'; + +export const rootRouteRef = createRouteRef(); diff --git a/plugins/auth/src/setupTests.ts b/plugins/auth/src/setupTests.ts new file mode 100644 index 0000000000..b57590b525 --- /dev/null +++ b/plugins/auth/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; diff --git a/yarn.lock b/yarn.lock index 81c7b8aa13..fd631a0fe0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4264,6 +4264,34 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-auth@workspace:plugins/auth": + version: 0.0.0-use.local + resolution: "@backstage/plugin-auth@workspace:plugins/auth" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-compat-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": "npm:^4.9.13" + "@material-ui/icons": "npm:^4.9.1" + "@material-ui/lab": "npm:4.0.0-alpha.57" + "@testing-library/jest-dom": "npm:^6.0.0" + "@testing-library/react": "npm:^14.0.0" + "@testing-library/user-event": "npm:^14.0.0" + msw: "npm:^1.0.0" + react-use: "npm:^17.2.4" + peerDependencies: + react: ^17.0.0 + react-router-dom: ^6.0.0 + languageName: unknown + linkType: soft + "@backstage/plugin-bitbucket-cloud-common@workspace:^, @backstage/plugin-bitbucket-cloud-common@workspace:plugins/bitbucket-cloud-common": version: 0.0.0-use.local resolution: "@backstage/plugin-bitbucket-cloud-common@workspace:plugins/bitbucket-cloud-common" @@ -10466,6 +10494,27 @@ __metadata: languageName: node linkType: hard +"@material-ui/lab@npm:4.0.0-alpha.57": + version: 4.0.0-alpha.57 + resolution: "@material-ui/lab@npm:4.0.0-alpha.57" + dependencies: + "@babel/runtime": "npm:^7.4.4" + "@material-ui/utils": "npm:^4.11.2" + clsx: "npm:^1.0.4" + prop-types: "npm:^15.7.2" + react-is: "npm:^16.8.0 || ^17.0.0" + peerDependencies: + "@material-ui/core": ^4.9.10 + "@types/react": ^16.8.6 || ^17.0.0 + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/0142df7864fd8307a577a7e98e5c198bc71225a1abfe186abb3f5bb6d15bfcf99cf64204d43ea1a9be6723907135e1773a63dc41605b84c37596a02015f3e3b4 + languageName: node + linkType: hard + "@material-ui/lab@npm:4.0.0-alpha.61, @material-ui/lab@npm:^4.0.0-alpha.57, @material-ui/lab@npm:^4.0.0-alpha.60, @material-ui/lab@npm:^4.0.0-alpha.61": version: 4.0.0-alpha.61 resolution: "@material-ui/lab@npm:4.0.0-alpha.61" @@ -10601,7 +10650,7 @@ __metadata: languageName: node linkType: hard -"@material-ui/utils@npm:^4.11.3": +"@material-ui/utils@npm:^4.11.2, @material-ui/utils@npm:^4.11.3": version: 4.11.3 resolution: "@material-ui/utils@npm:4.11.3" dependencies: @@ -19487,6 +19536,22 @@ __metadata: languageName: node linkType: hard +"@testing-library/dom@npm:^9.0.0": + version: 9.3.4 + resolution: "@testing-library/dom@npm:9.3.4" + dependencies: + "@babel/code-frame": "npm:^7.10.4" + "@babel/runtime": "npm:^7.12.5" + "@types/aria-query": "npm:^5.0.1" + aria-query: "npm:5.1.3" + chalk: "npm:^4.1.0" + dom-accessibility-api: "npm:^0.5.9" + lz-string: "npm:^1.5.0" + pretty-format: "npm:^27.0.2" + checksum: 10/510da752ea76f4a10a0a4e3a77917b0302cf03effe576cd3534cab7e796533ee2b0e9fb6fb11b911a1ebd7c70a0bb6f235bf4f816c9b82b95b8fe0cddfd10975 + languageName: node + linkType: hard + "@testing-library/jest-dom@npm:6.5.0": version: 6.5.0 resolution: "@testing-library/jest-dom@npm:6.5.0" @@ -19539,6 +19604,20 @@ __metadata: languageName: node linkType: hard +"@testing-library/react@npm:^14.0.0": + version: 14.3.1 + resolution: "@testing-library/react@npm:14.3.1" + dependencies: + "@babel/runtime": "npm:^7.12.5" + "@testing-library/dom": "npm:^9.0.0" + "@types/react-dom": "npm:^18.0.0" + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 10/83359dcdf9eaf067839f34604e1a181cbc14fc09f3a07672403700fcc6a900c4b8054ad1114fc24b4b9f89d84e2a09e1b7c9afce2306b1d4b4c9e30eb1cb12de + languageName: node + linkType: hard + "@testing-library/react@npm:^16.0.0": version: 16.3.0 resolution: "@testing-library/react@npm:16.3.0" @@ -23941,6 +24020,15 @@ __metadata: languageName: node linkType: hard +"aria-query@npm:5.1.3": + version: 5.1.3 + resolution: "aria-query@npm:5.1.3" + dependencies: + deep-equal: "npm:^2.0.5" + checksum: 10/e5da608a7c4954bfece2d879342b6c218b6b207e2d9e5af270b5e38ef8418f02d122afdc948b68e32649b849a38377785252059090d66fa8081da95d1609c0d2 + languageName: node + linkType: hard + "aria-query@npm:5.3.0": version: 5.3.0 resolution: "aria-query@npm:5.3.0" @@ -23957,7 +24045,7 @@ __metadata: languageName: node linkType: hard -"array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2": +"array-buffer-byte-length@npm:^1.0.0, array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2": version: 1.0.2 resolution: "array-buffer-byte-length@npm:1.0.2" dependencies: @@ -27717,6 +27805,32 @@ __metadata: languageName: node linkType: hard +"deep-equal@npm:^2.0.5": + version: 2.2.3 + resolution: "deep-equal@npm:2.2.3" + dependencies: + array-buffer-byte-length: "npm:^1.0.0" + call-bind: "npm:^1.0.5" + es-get-iterator: "npm:^1.1.3" + get-intrinsic: "npm:^1.2.2" + is-arguments: "npm:^1.1.1" + is-array-buffer: "npm:^3.0.2" + is-date-object: "npm:^1.0.5" + is-regex: "npm:^1.1.4" + is-shared-array-buffer: "npm:^1.0.2" + isarray: "npm:^2.0.5" + object-is: "npm:^1.1.5" + object-keys: "npm:^1.1.1" + object.assign: "npm:^4.1.4" + regexp.prototype.flags: "npm:^1.5.1" + side-channel: "npm:^1.0.4" + which-boxed-primitive: "npm:^1.0.2" + which-collection: "npm:^1.0.1" + which-typed-array: "npm:^1.1.13" + checksum: 10/1ce49d0b71d0f14d8ef991a742665eccd488dfc9b3cada069d4d7a86291e591c92d2589c832811dea182b4015736b210acaaebce6184be356c1060d176f5a05f + languageName: node + linkType: hard + "deep-equal@npm:~1.0.1": version: 1.0.1 resolution: "deep-equal@npm:1.0.1" @@ -28925,7 +29039,7 @@ __metadata: languageName: node linkType: hard -"es-get-iterator@npm:^1.0.2": +"es-get-iterator@npm:^1.0.2, es-get-iterator@npm:^1.1.3": version: 1.1.3 resolution: "es-get-iterator@npm:1.1.3" dependencies: @@ -31522,7 +31636,7 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": +"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": version: 1.3.0 resolution: "get-intrinsic@npm:1.3.0" dependencies: @@ -33455,7 +33569,7 @@ __metadata: languageName: node linkType: hard -"is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5": +"is-array-buffer@npm:^3.0.2, is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5": version: 3.0.5 resolution: "is-array-buffer@npm:3.0.5" dependencies: @@ -33928,7 +34042,7 @@ __metadata: languageName: node linkType: hard -"is-regex@npm:^1.2.1": +"is-regex@npm:^1.1.4, is-regex@npm:^1.2.1": version: 1.2.1 resolution: "is-regex@npm:1.2.1" dependencies: @@ -33977,7 +34091,7 @@ __metadata: languageName: node linkType: hard -"is-shared-array-buffer@npm:^1.0.4": +"is-shared-array-buffer@npm:^1.0.2, is-shared-array-buffer@npm:^1.0.4": version: 1.0.4 resolution: "is-shared-array-buffer@npm:1.0.4" dependencies: @@ -43716,7 +43830,7 @@ __metadata: languageName: node linkType: hard -"regexp.prototype.flags@npm:^1.5.3": +"regexp.prototype.flags@npm:^1.5.1, regexp.prototype.flags@npm:^1.5.3": version: 1.5.4 resolution: "regexp.prototype.flags@npm:1.5.4" dependencies: @@ -45220,7 +45334,7 @@ __metadata: languageName: node linkType: hard -"side-channel@npm:^1.0.6, side-channel@npm:^1.1.0": +"side-channel@npm:^1.0.4, side-channel@npm:^1.0.6, side-channel@npm:^1.1.0": version: 1.1.0 resolution: "side-channel@npm:1.1.0" dependencies: @@ -49387,7 +49501,7 @@ __metadata: languageName: node linkType: hard -"which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1": +"which-boxed-primitive@npm:^1.0.2, which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1": version: 1.1.1 resolution: "which-boxed-primitive@npm:1.1.1" dependencies: @@ -49421,7 +49535,7 @@ __metadata: languageName: node linkType: hard -"which-collection@npm:^1.0.2": +"which-collection@npm:^1.0.1, which-collection@npm:^1.0.2": version: 1.0.2 resolution: "which-collection@npm:1.0.2" dependencies: @@ -49443,7 +49557,7 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18, which-typed-array@npm:^1.1.2": +"which-typed-array@npm:^1.1.13, which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18, which-typed-array@npm:^1.1.2": version: 1.1.19 resolution: "which-typed-array@npm:1.1.19" dependencies: From a2f3718c8b2ab882ee54a42e1c351c277a1bd424 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 9 Sep 2025 10:55:13 +0200 Subject: [PATCH 137/233] chore: added tdev app Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- plugins/auth/dev/index.tsx | 13 ++++++++++++- plugins/auth/package.json | 16 +++++++++++----- plugins/auth/src/index.ts | 2 +- yarn.lock | 37 +++++++++++-------------------------- 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/plugins/auth/dev/index.tsx b/plugins/auth/dev/index.tsx index 04598f0f2e..b0762416c6 100644 --- a/plugins/auth/dev/index.tsx +++ b/plugins/auth/dev/index.tsx @@ -14,4 +14,15 @@ * limitations under the License. */ -// todo +import { createApp } from '@backstage/frontend-defaults'; +import { createRoot } from 'react-dom/client'; + +import plugin from '../src'; + +const app = createApp({ + features: [plugin], +}); + +const container = document.getElementById('root'); +const root = createRoot(container!); +root.render(app.createRoot()); diff --git a/plugins/auth/package.json b/plugins/auth/package.json index 9d3fe7b197..a623185f42 100644 --- a/plugins/auth/package.json +++ b/plugins/auth/package.json @@ -51,24 +51,30 @@ "@backstage/errors": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/theme": "workspace:^", - "@material-ui/core": "^4.9.13", + "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", - "@material-ui/lab": "4.0.0-alpha.57", + "@material-ui/lab": "4.0.0-alpha.61", "react-use": "^17.2.4" }, "peerDependencies": { - "react": "^17.0.0", - "react-router-dom": "^6.0.0" + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0", + "react-router-dom": "^6.3.0" }, "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", + "@backstage/frontend-defaults": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.0.0", - "msw": "^1.0.0" + "@types/react": "^18.0.0", + "msw": "^1.0.0", + "react": "^18.0.2", + "react-dom": "^18.0.2" }, "files": [ "dist" diff --git a/plugins/auth/src/index.ts b/plugins/auth/src/index.ts index d507bac202..31460b3e9d 100644 --- a/plugins/auth/src/index.ts +++ b/plugins/auth/src/index.ts @@ -13,5 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { authPlugin, AuthRouter } from './plugin'; +export { default } from './plugin'; export { rootRouteRef } from './routes'; diff --git a/yarn.lock b/yarn.lock index fd631a0fe0..4d15a81ad2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4275,20 +4275,26 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/frontend-defaults": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" - "@material-ui/core": "npm:^4.9.13" + "@material-ui/core": "npm:^4.12.2" "@material-ui/icons": "npm:^4.9.1" - "@material-ui/lab": "npm:4.0.0-alpha.57" + "@material-ui/lab": "npm:4.0.0-alpha.61" "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^14.0.0" "@testing-library/user-event": "npm:^14.0.0" + "@types/react": "npm:^18.0.0" msw: "npm:^1.0.0" + react: "npm:^18.0.2" + react-dom: "npm:^18.0.2" react-use: "npm:^17.2.4" peerDependencies: - react: ^17.0.0 - react-router-dom: ^6.0.0 + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + react-router-dom: ^6.3.0 languageName: unknown linkType: soft @@ -10494,27 +10500,6 @@ __metadata: languageName: node linkType: hard -"@material-ui/lab@npm:4.0.0-alpha.57": - version: 4.0.0-alpha.57 - resolution: "@material-ui/lab@npm:4.0.0-alpha.57" - dependencies: - "@babel/runtime": "npm:^7.4.4" - "@material-ui/utils": "npm:^4.11.2" - clsx: "npm:^1.0.4" - prop-types: "npm:^15.7.2" - react-is: "npm:^16.8.0 || ^17.0.0" - peerDependencies: - "@material-ui/core": ^4.9.10 - "@types/react": ^16.8.6 || ^17.0.0 - react: ^16.8.0 || ^17.0.0 - react-dom: ^16.8.0 || ^17.0.0 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 10/0142df7864fd8307a577a7e98e5c198bc71225a1abfe186abb3f5bb6d15bfcf99cf64204d43ea1a9be6723907135e1773a63dc41605b84c37596a02015f3e3b4 - languageName: node - linkType: hard - "@material-ui/lab@npm:4.0.0-alpha.61, @material-ui/lab@npm:^4.0.0-alpha.57, @material-ui/lab@npm:^4.0.0-alpha.60, @material-ui/lab@npm:^4.0.0-alpha.61": version: 4.0.0-alpha.61 resolution: "@material-ui/lab@npm:4.0.0-alpha.61" @@ -10650,7 +10635,7 @@ __metadata: languageName: node linkType: hard -"@material-ui/utils@npm:^4.11.2, @material-ui/utils@npm:^4.11.3": +"@material-ui/utils@npm:^4.11.3": version: 4.11.3 resolution: "@material-ui/utils@npm:4.11.3" dependencies: From c7be7c1c6ddae17c956b9425121065634105c497 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 9 Sep 2025 10:56:41 +0200 Subject: [PATCH 138/233] chore: added docs Signed-off-by: benjdlambert --- plugins/auth/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/auth/README.md b/plugins/auth/README.md index 789ae670b4..440d7d294a 100644 --- a/plugins/auth/README.md +++ b/plugins/auth/README.md @@ -10,3 +10,7 @@ This plugin is designed to work with the `@backstage/plugin-auth-backend` packag # From your Backstage app directory yarn --cwd packages/app add @backstage/plugin-auth ``` + +## Usage + +The plugin provides the route `/oauth2/authorize/:sessionId` for approving of oauth2 sessions for clients. You should see an approval flow for any sessions created through the `auth-backend`. From 54ddfefee9f9232be3630ddea015215eef6f0388 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 9 Sep 2025 11:01:13 +0200 Subject: [PATCH 139/233] chore: fix package metadata and changeset Signed-off-by: benjdlambert --- .changeset/funny-eagles-try.md | 7 +++ .changeset/giant-buttons-flash.md | 5 +++ plugins/auth/package.json | 73 +++++++++++++++---------------- 3 files changed, 48 insertions(+), 37 deletions(-) create mode 100644 .changeset/funny-eagles-try.md create mode 100644 .changeset/giant-buttons-flash.md diff --git a/.changeset/funny-eagles-try.md b/.changeset/funny-eagles-try.md new file mode 100644 index 0000000000..c5550756e0 --- /dev/null +++ b/.changeset/funny-eagles-try.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-auth-react': patch +'@backstage/plugin-auth-node': patch +--- + +Updating plugin metadata diff --git a/.changeset/giant-buttons-flash.md b/.changeset/giant-buttons-flash.md new file mode 100644 index 0000000000..97564d37ac --- /dev/null +++ b/.changeset/giant-buttons-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth': minor +--- + +Initial publish of the `auth` frontend package diff --git a/plugins/auth/package.json b/plugins/auth/package.json index a623185f42..c69ffe656f 100644 --- a/plugins/auth/package.json +++ b/plugins/auth/package.json @@ -1,29 +1,6 @@ { "name": "@backstage/plugin-auth", - "version": "0.1.0", - "license": "Apache-2.0", - "private": true, - "main": "src/index.ts", - "types": "src/index.ts", - "publishConfig": { - "access": "public" - }, - "exports": { - ".": "./src/index.ts", - "./package.json": "./package.json" - }, - "typesVersions": { - "*": { - "package.json": [ - "package.json" - ] - } - }, - "repository": { - "type": "git", - "url": "https://github.com/backstage/backstage", - "directory": "plugins/auth" - }, + "version": "0.0.0", "backstage": { "role": "frontend-plugin", "pluginId": "auth", @@ -34,15 +11,40 @@ "@backstage/plugin-auth-react" ] }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/auth" + }, + "license": "Apache-2.0", "sideEffects": false, + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + }, + "main": "src/index.ts", + "types": "src/index.ts", + "typesVersions": { + "*": { + "package.json": [ + "package.json" + ] + } + }, + "files": [ + "dist" + ], "scripts": { - "start": "backstage-cli package start", "build": "backstage-cli package build", - "lint": "backstage-cli package lint", - "test": "backstage-cli package test", "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", "prepack": "backstage-cli package prepack", - "postpack": "backstage-cli package postpack" + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" }, "dependencies": { "@backstage/core-compat-api": "workspace:^", @@ -56,12 +58,6 @@ "@material-ui/lab": "4.0.0-alpha.61", "react-use": "^17.2.4" }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0", - "react-router-dom": "^6.3.0" - }, "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", @@ -76,7 +72,10 @@ "react": "^18.0.2", "react-dom": "^18.0.2" }, - "files": [ - "dist" - ] + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0", + "react-router-dom": "^6.3.0" + } } From 4fcd282522b141a9d58b446348a73751b86047f2 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 9 Sep 2025 11:32:48 +0200 Subject: [PATCH 140/233] chore: fix linting of peer deps Signed-off-by: benjdlambert --- plugins/auth/package.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/auth/package.json b/plugins/auth/package.json index c69ffe656f..6dadc78423 100644 --- a/plugins/auth/package.json +++ b/plugins/auth/package.json @@ -70,12 +70,18 @@ "@types/react": "^18.0.0", "msw": "^1.0.0", "react": "^18.0.2", - "react-dom": "^18.0.2" + "react-dom": "^18.0.2", + "react-router-dom": "^6.3.0" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0", "react": "^17.0.0 || ^18.0.0", "react-dom": "^17.0.0 || ^18.0.0", "react-router-dom": "^6.3.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } } From 81dd70aebd994af6b2065a3aa665743d65b2fe27 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 9 Sep 2025 14:21:54 +0200 Subject: [PATCH 141/233] chore: fixing dependencies and code improvements Signed-off-by: benjdlambert --- plugins/auth/package.json | 5 +- .../components/ConsentPage/ConsentPage.tsx | 370 +++++++----------- .../ConsentPage/useConsentSession.ts | 144 +++++++ plugins/auth/src/plugin.tsx | 4 +- yarn.lock | 96 +---- 5 files changed, 296 insertions(+), 323 deletions(-) create mode 100644 plugins/auth/src/components/ConsentPage/useConsentSession.ts diff --git a/plugins/auth/package.json b/plugins/auth/package.json index 6dadc78423..54e87cb7b1 100644 --- a/plugins/auth/package.json +++ b/plugins/auth/package.json @@ -47,9 +47,7 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/core-compat-api": "workspace:^", "@backstage/core-components": "workspace:^", - "@backstage/core-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/theme": "workspace:^", @@ -60,12 +58,11 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", "@backstage/frontend-defaults": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/jest-dom": "^6.0.0", - "@testing-library/react": "^14.0.0", + "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.0.0", "@types/react": "^18.0.0", "msw": "^1.0.0", diff --git a/plugins/auth/src/components/ConsentPage/ConsentPage.tsx b/plugins/auth/src/components/ConsentPage/ConsentPage.tsx index 3234450a1d..0f071025a2 100644 --- a/plugins/auth/src/components/ConsentPage/ConsentPage.tsx +++ b/plugins/auth/src/components/ConsentPage/ConsentPage.tsx @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useCallback, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; + import { Box, Button, @@ -38,13 +38,7 @@ import { EmptyState, ResponseErrorPanel, } from '@backstage/core-components'; -import { - alertApiRef, - useApi, - fetchApiRef, - discoveryApiRef, -} from '@backstage/core-plugin-api'; -import { isError } from '@backstage/errors'; +import { useConsentSession } from './useConsentSession'; const useStyles = makeStyles(theme => ({ authCard: { @@ -89,260 +83,164 @@ const useStyles = makeStyles(theme => ({ }, })); -interface Session { - id: string; - clientName?: string; - clientId: string; - redirectUri: string; - scopes?: string[]; - responseType?: string; - state?: string; - nonce?: string; - codeChallenge?: string; - codeChallengeMethod?: string; - expiresAt?: string; -} +const ConsentPageLayout = ({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) => ( + +
+ {children} + +); export const ConsentPage = () => { const classes = useStyles(); const { sessionId } = useParams<{ sessionId: string }>(); - const alertApi = useApi(alertApiRef); - const fetchApi = useApi(fetchApiRef); - const discoveryApi = useApi(discoveryApiRef); - const [session, setSession] = useState(null); - const [loading, setLoading] = useState(true); - const [submitting, setSubmitting] = useState(false); - const [error, setError] = useState(null); - const [completed, setCompleted] = useState< - | { - action: 'approve' | 'reject'; - } - | undefined - >(undefined); - - useEffect(() => { - const fetchSession = async () => { - if (!sessionId) return; - - try { - const baseUrl = await discoveryApi.getBaseUrl('auth'); - const response = await fetchApi.fetch( - `${baseUrl}/v1/sessions/${sessionId}`, - ); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - const data = await response.json(); - setSession(data); - } catch (err) { - setError(isError(err) ? err.message : 'Failed to load consent request'); - } finally { - setLoading(false); - } - }; - - fetchSession(); - }, [sessionId, discoveryApi, fetchApi]); - - const handleAction = useCallback( - async (action: 'approve' | 'reject') => { - if (!session) return; - - setSubmitting(true); - try { - const baseUrl = await discoveryApi.getBaseUrl('auth'); - const response = await fetchApi.fetch( - `${baseUrl}/v1/sessions/${session.id}/${action}`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - const result = await response.json(); - - setCompleted({ - action, - }); - - if (result.redirectUrl) { - window.location.href = result.redirectUrl; - } - } catch (err) { - alertApi.post({ - message: isError(err) ? err.message : `Failed to ${action} consent`, - severity: 'error', - }); - } finally { - setSubmitting(false); - } - }, - [session, discoveryApi, fetchApi, alertApi], - ); + const { state, handleAction } = useConsentSession({ sessionId }); if (!sessionId) { return ( - -
- - - - + + + ); } - if (loading) { + if (state.status === 'loading') { return ( - -
- - - - - - + + + + + ); } - if (error ?? !session) { + if (state.status === 'error') { return ( - -
- - - - + + + ); } - if (completed) { + if (state.status === 'completed') { return ( - -
- - - - - {completed.action === 'approve' ? ( - - ) : ( - - )} - - {completed.action === 'approve' - ? 'Authorization Approved' - : 'Authorization Denied'} - - - {completed.action === 'approve' - ? 'You have successfully authorized the application to access your Backstage account.' - : 'You have denied the application access to your Backstage account.'} - - - Redirecting to the application... - - - - - - - ); - } - - const appName = session.clientName ?? session.clientId; - - return ( - -
- + - - - - {appName} - - wants to access your Backstage account - - - - - - - } - className={classes.securityWarning} - > - - Security Notice: By authorizing this - application, you are granting it access to your Backstage - account. The application will receive an access token that - allows it to act on your behalf. + + {state.action === 'approve' ? ( + + ) : ( + + )} + + {state.action === 'approve' + ? 'Authorization Approved' + : 'Authorization Denied'} + + + {state.action === 'approve' + ? 'You have successfully authorized the application to access your Backstage account.' + : 'You have denied the application access to your Backstage account.'} - - - Callback URL: - - {session.redirectUri} - - - - - Make sure you trust this application and recognize the callback - URL above. Only authorize applications you trust. + Redirecting to the application... - - - - - - - + + ); + } + + const session = state.session; + const isSubmitting = state.status === 'submitting'; + const appName = session.clientName ?? session.clientId; + + return ( + + + + + + + {appName} + + wants to access your Backstage account + + + + + + + } + className={classes.securityWarning} + > + + Security Notice: By authorizing this application, + you are granting it access to your Backstage account. The + application will receive an access token that allows it to act on + your behalf. + + + + Callback URL: + + {session.redirectUri} + + + + + + Make sure you trust this application and recognize the callback + URL above. Only authorize applications you trust. + + + + + + + + + + ); }; diff --git a/plugins/auth/src/components/ConsentPage/useConsentSession.ts b/plugins/auth/src/components/ConsentPage/useConsentSession.ts new file mode 100644 index 0000000000..a19faae6fe --- /dev/null +++ b/plugins/auth/src/components/ConsentPage/useConsentSession.ts @@ -0,0 +1,144 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + useApi, + alertApiRef, + fetchApiRef, + discoveryApiRef, +} from '@backstage/frontend-plugin-api'; +import { useCallback } from 'react'; +import useAsync from 'react-use/esm/useAsync'; +import useAsyncFn from 'react-use/esm/useAsyncFn'; +import { isError } from '@backstage/errors'; + +interface Session { + id: string; + clientName?: string; + clientId: string; + redirectUri: string; + scopes?: string[]; + responseType?: string; + state?: string; + nonce?: string; + codeChallenge?: string; + codeChallengeMethod?: string; + expiresAt?: string; +} + +type ConsentState = + | { status: 'loading' } + | { status: 'error'; error: string } + | { status: 'loaded'; session: Session } + | { status: 'submitting'; session: Session; action: 'approve' | 'reject' } + | { status: 'completed'; action: 'approve' | 'reject' }; + +export const useConsentSession = (opts: { sessionId?: string }) => { + const alertApi = useApi(alertApiRef); + const fetchApi = useApi(fetchApiRef); + const discoveryApi = useApi(discoveryApiRef); + const { sessionId } = opts; + + const sessionState = useAsync(async () => { + if (!sessionId) { + throw new Error('Session ID is missing'); + } + + const baseUrl = await discoveryApi.getBaseUrl('auth'); + const response = await fetchApi.fetch( + `${baseUrl}/v1/sessions/${sessionId}`, + ); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return (await response.json()) as Session; + }, [sessionId]); + + const [actionState, handleActionInternal] = useAsyncFn( + async (action: 'approve' | 'reject', session: Session) => { + const baseUrl = await discoveryApi.getBaseUrl('auth'); + const response = await fetchApi.fetch( + `${baseUrl}/v1/sessions/${session.id}/${action}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const result = await response.json(); + + if (result.redirectUrl) { + window.location.href = result.redirectUrl; + } + + return { action, redirectUrl: result.redirectUrl }; + }, + [discoveryApi, fetchApi], + ); + + const getConsentState = (): ConsentState => { + if (actionState.value) { + return { status: 'completed', action: actionState.value.action }; + } + if (actionState.loading && sessionState.value) { + return { + status: 'submitting', + session: sessionState.value, + action: 'approve', // This will be set properly when called + }; + } + if (sessionState.error) { + return { + status: 'error', + error: isError(sessionState.error) + ? sessionState.error.message + : 'Failed to load consent request', + }; + } + if (sessionState.value) { + return { status: 'loaded', session: sessionState.value }; + } + return { status: 'loading' }; + }; + + const state = getConsentState(); + return { + state, + handleAction: useCallback( + async (action: 'approve' | 'reject') => { + if (state.status !== 'loaded') return; + + try { + await handleActionInternal(action, state.session); + } catch (err) { + alertApi.post({ + message: isError(err) ? err.message : `Failed to ${action} consent`, + severity: 'error', + }); + } + }, + [state, handleActionInternal, alertApi], + ), + }; +}; diff --git a/plugins/auth/src/plugin.tsx b/plugins/auth/src/plugin.tsx index 9b52960f4c..4ed5d57223 100644 --- a/plugins/auth/src/plugin.tsx +++ b/plugins/auth/src/plugin.tsx @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { compatWrapper } from '@backstage/core-compat-api'; import { createFrontendPlugin, PageBlueprint, @@ -24,8 +23,7 @@ export const AuthPage = PageBlueprint.make({ params: { path: '/oauth2', routeRef: rootRouteRef, - loader: () => - import('./components/Router').then(m => compatWrapper()), + loader: () => import('./components/Router').then(m => ), }, }); diff --git a/yarn.lock b/yarn.lock index 4d15a81ad2..9d3c3af1f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4269,10 +4269,7 @@ __metadata: resolution: "@backstage/plugin-auth@workspace:plugins/auth" dependencies: "@backstage/cli": "workspace:^" - "@backstage/core-app-api": "workspace:^" - "@backstage/core-compat-api": "workspace:^" "@backstage/core-components": "workspace:^" - "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/frontend-defaults": "workspace:^" @@ -4283,18 +4280,22 @@ __metadata: "@material-ui/icons": "npm:^4.9.1" "@material-ui/lab": "npm:4.0.0-alpha.61" "@testing-library/jest-dom": "npm:^6.0.0" - "@testing-library/react": "npm:^14.0.0" + "@testing-library/react": "npm:^16.0.0" "@testing-library/user-event": "npm:^14.0.0" "@types/react": "npm:^18.0.0" msw: "npm:^1.0.0" react: "npm:^18.0.2" react-dom: "npm:^18.0.2" + react-router-dom: "npm:^6.3.0" react-use: "npm:^17.2.4" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 react-dom: ^17.0.0 || ^18.0.0 react-router-dom: ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true languageName: unknown linkType: soft @@ -19521,22 +19522,6 @@ __metadata: languageName: node linkType: hard -"@testing-library/dom@npm:^9.0.0": - version: 9.3.4 - resolution: "@testing-library/dom@npm:9.3.4" - dependencies: - "@babel/code-frame": "npm:^7.10.4" - "@babel/runtime": "npm:^7.12.5" - "@types/aria-query": "npm:^5.0.1" - aria-query: "npm:5.1.3" - chalk: "npm:^4.1.0" - dom-accessibility-api: "npm:^0.5.9" - lz-string: "npm:^1.5.0" - pretty-format: "npm:^27.0.2" - checksum: 10/510da752ea76f4a10a0a4e3a77917b0302cf03effe576cd3534cab7e796533ee2b0e9fb6fb11b911a1ebd7c70a0bb6f235bf4f816c9b82b95b8fe0cddfd10975 - languageName: node - linkType: hard - "@testing-library/jest-dom@npm:6.5.0": version: 6.5.0 resolution: "@testing-library/jest-dom@npm:6.5.0" @@ -19589,20 +19574,6 @@ __metadata: languageName: node linkType: hard -"@testing-library/react@npm:^14.0.0": - version: 14.3.1 - resolution: "@testing-library/react@npm:14.3.1" - dependencies: - "@babel/runtime": "npm:^7.12.5" - "@testing-library/dom": "npm:^9.0.0" - "@types/react-dom": "npm:^18.0.0" - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10/83359dcdf9eaf067839f34604e1a181cbc14fc09f3a07672403700fcc6a900c4b8054ad1114fc24b4b9f89d84e2a09e1b7c9afce2306b1d4b4c9e30eb1cb12de - languageName: node - linkType: hard - "@testing-library/react@npm:^16.0.0": version: 16.3.0 resolution: "@testing-library/react@npm:16.3.0" @@ -24005,15 +23976,6 @@ __metadata: languageName: node linkType: hard -"aria-query@npm:5.1.3": - version: 5.1.3 - resolution: "aria-query@npm:5.1.3" - dependencies: - deep-equal: "npm:^2.0.5" - checksum: 10/e5da608a7c4954bfece2d879342b6c218b6b207e2d9e5af270b5e38ef8418f02d122afdc948b68e32649b849a38377785252059090d66fa8081da95d1609c0d2 - languageName: node - linkType: hard - "aria-query@npm:5.3.0": version: 5.3.0 resolution: "aria-query@npm:5.3.0" @@ -24030,7 +23992,7 @@ __metadata: languageName: node linkType: hard -"array-buffer-byte-length@npm:^1.0.0, array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2": +"array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2": version: 1.0.2 resolution: "array-buffer-byte-length@npm:1.0.2" dependencies: @@ -27790,32 +27752,6 @@ __metadata: languageName: node linkType: hard -"deep-equal@npm:^2.0.5": - version: 2.2.3 - resolution: "deep-equal@npm:2.2.3" - dependencies: - array-buffer-byte-length: "npm:^1.0.0" - call-bind: "npm:^1.0.5" - es-get-iterator: "npm:^1.1.3" - get-intrinsic: "npm:^1.2.2" - is-arguments: "npm:^1.1.1" - is-array-buffer: "npm:^3.0.2" - is-date-object: "npm:^1.0.5" - is-regex: "npm:^1.1.4" - is-shared-array-buffer: "npm:^1.0.2" - isarray: "npm:^2.0.5" - object-is: "npm:^1.1.5" - object-keys: "npm:^1.1.1" - object.assign: "npm:^4.1.4" - regexp.prototype.flags: "npm:^1.5.1" - side-channel: "npm:^1.0.4" - which-boxed-primitive: "npm:^1.0.2" - which-collection: "npm:^1.0.1" - which-typed-array: "npm:^1.1.13" - checksum: 10/1ce49d0b71d0f14d8ef991a742665eccd488dfc9b3cada069d4d7a86291e591c92d2589c832811dea182b4015736b210acaaebce6184be356c1060d176f5a05f - languageName: node - linkType: hard - "deep-equal@npm:~1.0.1": version: 1.0.1 resolution: "deep-equal@npm:1.0.1" @@ -29024,7 +28960,7 @@ __metadata: languageName: node linkType: hard -"es-get-iterator@npm:^1.0.2, es-get-iterator@npm:^1.1.3": +"es-get-iterator@npm:^1.0.2": version: 1.1.3 resolution: "es-get-iterator@npm:1.1.3" dependencies: @@ -31621,7 +31557,7 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": +"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": version: 1.3.0 resolution: "get-intrinsic@npm:1.3.0" dependencies: @@ -33554,7 +33490,7 @@ __metadata: languageName: node linkType: hard -"is-array-buffer@npm:^3.0.2, is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5": +"is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5": version: 3.0.5 resolution: "is-array-buffer@npm:3.0.5" dependencies: @@ -34027,7 +33963,7 @@ __metadata: languageName: node linkType: hard -"is-regex@npm:^1.1.4, is-regex@npm:^1.2.1": +"is-regex@npm:^1.2.1": version: 1.2.1 resolution: "is-regex@npm:1.2.1" dependencies: @@ -34076,7 +34012,7 @@ __metadata: languageName: node linkType: hard -"is-shared-array-buffer@npm:^1.0.2, is-shared-array-buffer@npm:^1.0.4": +"is-shared-array-buffer@npm:^1.0.4": version: 1.0.4 resolution: "is-shared-array-buffer@npm:1.0.4" dependencies: @@ -43815,7 +43751,7 @@ __metadata: languageName: node linkType: hard -"regexp.prototype.flags@npm:^1.5.1, regexp.prototype.flags@npm:^1.5.3": +"regexp.prototype.flags@npm:^1.5.3": version: 1.5.4 resolution: "regexp.prototype.flags@npm:1.5.4" dependencies: @@ -45319,7 +45255,7 @@ __metadata: languageName: node linkType: hard -"side-channel@npm:^1.0.4, side-channel@npm:^1.0.6, side-channel@npm:^1.1.0": +"side-channel@npm:^1.0.6, side-channel@npm:^1.1.0": version: 1.1.0 resolution: "side-channel@npm:1.1.0" dependencies: @@ -49486,7 +49422,7 @@ __metadata: languageName: node linkType: hard -"which-boxed-primitive@npm:^1.0.2, which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1": +"which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1": version: 1.1.1 resolution: "which-boxed-primitive@npm:1.1.1" dependencies: @@ -49520,7 +49456,7 @@ __metadata: languageName: node linkType: hard -"which-collection@npm:^1.0.1, which-collection@npm:^1.0.2": +"which-collection@npm:^1.0.2": version: 1.0.2 resolution: "which-collection@npm:1.0.2" dependencies: @@ -49542,7 +49478,7 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.13, which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18, which-typed-array@npm:^1.1.2": +"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18, which-typed-array@npm:^1.1.2": version: 1.1.19 resolution: "which-typed-array@npm:1.1.19" dependencies: From 0f5b2d6489bc0cc16b216b3d79fd811ee6b5fa47 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 9 Sep 2025 16:12:06 +0200 Subject: [PATCH 142/233] chore: updating api-report Signed-off-by: benjdlambert --- plugins/auth/report.api.md | 52 ++++++++++++++++++++++++++++++++++++++ plugins/auth/src/index.ts | 1 - 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 plugins/auth/report.api.md diff --git a/plugins/auth/report.api.md b/plugins/auth/report.api.md new file mode 100644 index 0000000000..4c7d2d4a11 --- /dev/null +++ b/plugins/auth/report.api.md @@ -0,0 +1,52 @@ +## API Report File for "@backstage/plugin-auth" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; +import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; +import { RouteRef } from '@backstage/frontend-plugin-api'; + +// @public (undocumented) +const _default: OverridableFrontendPlugin< + { + root: RouteRef; + }, + {}, + { + 'page:auth': ExtensionDefinition<{ + kind: 'page'; + name: undefined; + config: { + path: string | undefined; + }; + configInput: { + path?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef + | ExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >; + inputs: {}; + params: { + defaultPath?: [Error: `Use the 'path' param instead`]; + path: string; + loader: () => Promise; + routeRef?: RouteRef; + }; + }>; + } +>; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/auth/src/index.ts b/plugins/auth/src/index.ts index 31460b3e9d..717cdd4672 100644 --- a/plugins/auth/src/index.ts +++ b/plugins/auth/src/index.ts @@ -14,4 +14,3 @@ * limitations under the License. */ export { default } from './plugin'; -export { rootRouteRef } from './routes'; From 14b5ccfbbea1aa22016d0f182006b6e562a79077 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 9 Sep 2025 16:28:05 +0200 Subject: [PATCH 143/233] chore: add auth to app next Signed-off-by: benjdlambert --- packages/app-next/package.json | 1 + yarn.lock | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app-next/package.json b/packages/app-next/package.json index 44602b2438..8e54830cac 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -49,6 +49,7 @@ "@backstage/plugin-api-docs": "workspace:^", "@backstage/plugin-app": "workspace:^", "@backstage/plugin-app-visualizer": "workspace:^", + "@backstage/plugin-auth": "workspace:^", "@backstage/plugin-auth-react": "workspace:^", "@backstage/plugin-catalog": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 9d3c3af1f7..88e4061d10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4264,7 +4264,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-auth@workspace:plugins/auth": +"@backstage/plugin-auth@workspace:^, @backstage/plugin-auth@workspace:plugins/auth": version: 0.0.0-use.local resolution: "@backstage/plugin-auth@workspace:plugins/auth" dependencies: @@ -29954,6 +29954,7 @@ __metadata: "@backstage/plugin-api-docs": "workspace:^" "@backstage/plugin-app": "workspace:^" "@backstage/plugin-app-visualizer": "workspace:^" + "@backstage/plugin-auth": "workspace:^" "@backstage/plugin-auth-react": "workspace:^" "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" From 020d484ac424b9d48668fd1e82f505b381101d72 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 9 Sep 2025 15:00:36 +0000 Subject: [PATCH 144/233] Version Packages (next) --- .changeset/create-app-1757429965.md | 5 + .changeset/pre.json | 26 +- docs/releases/v1.43.0-next.2-changelog.md | 695 ++++++++++++++++++ package.json | 2 +- packages/app-next/CHANGELOG.md | 27 + packages/app-next/package.json | 2 +- packages/app/CHANGELOG.md | 25 + packages/app/package.json | 2 +- packages/backend-defaults/CHANGELOG.md | 11 + packages/backend-defaults/package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- packages/catalog-client/CHANGELOG.md | 23 + packages/catalog-client/package.json | 2 +- packages/cli/CHANGELOG.md | 8 + packages/cli/package.json | 2 +- packages/config-loader/CHANGELOG.md | 6 + packages/config-loader/package.json | 2 +- packages/core-compat-api/CHANGELOG.md | 7 + packages/core-compat-api/package.json | 2 +- packages/core-components/CHANGELOG.md | 7 + packages/core-components/package.json | 2 +- packages/create-app/CHANGELOG.md | 6 + packages/create-app/package.json | 2 +- packages/dev-utils/CHANGELOG.md | 8 + packages/dev-utils/package.json | 2 +- packages/repo-tools/CHANGELOG.md | 7 + packages/repo-tools/package.json | 2 +- packages/ui/CHANGELOG.md | 7 + packages/ui/package.json | 2 +- plugins/api-docs/CHANGELOG.md | 10 + plugins/api-docs/package.json | 2 +- plugins/app-backend/CHANGELOG.md | 10 + plugins/app-backend/package.json | 2 +- plugins/app-node/CHANGELOG.md | 7 + plugins/app-node/package.json | 2 +- plugins/auth-backend/CHANGELOG.md | 10 + plugins/auth-backend/package.json | 2 +- plugins/auth-node/CHANGELOG.md | 8 + plugins/auth-node/package.json | 2 +- plugins/auth-react/CHANGELOG.md | 8 + plugins/auth-react/package.json | 2 +- plugins/auth/CHANGELOG.md | 12 + plugins/auth/package.json | 2 +- .../catalog-backend-module-aws/CHANGELOG.md | 10 + .../catalog-backend-module-aws/package.json | 2 +- .../catalog-backend-module-azure/CHANGELOG.md | 8 + .../catalog-backend-module-azure/package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../catalog-backend-module-gcp/CHANGELOG.md | 7 + .../catalog-backend-module-gcp/package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../catalog-backend-module-gitea/CHANGELOG.md | 8 + .../catalog-backend-module-gitea/package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../catalog-backend-module-ldap/CHANGELOG.md | 7 + .../catalog-backend-module-ldap/package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- plugins/catalog-backend/CHANGELOG.md | 10 + plugins/catalog-backend/package.json | 2 +- plugins/catalog-graph/CHANGELOG.md | 10 + plugins/catalog-graph/package.json | 2 +- plugins/catalog-import/CHANGELOG.md | 11 + plugins/catalog-import/package.json | 2 +- plugins/catalog-node/CHANGELOG.md | 28 + plugins/catalog-node/package.json | 2 +- plugins/catalog-react/CHANGELOG.md | 30 + plugins/catalog-react/package.json | 2 +- plugins/catalog/CHANGELOG.md | 11 + plugins/catalog/package.json | 2 +- plugins/devtools-backend/CHANGELOG.md | 8 + plugins/devtools-backend/package.json | 2 +- plugins/home/CHANGELOG.md | 12 + plugins/home/package.json | 2 +- plugins/kubernetes-backend/CHANGELOG.md | 11 + plugins/kubernetes-backend/package.json | 2 +- plugins/kubernetes-cluster/CHANGELOG.md | 8 + plugins/kubernetes-cluster/package.json | 2 +- plugins/kubernetes/CHANGELOG.md | 9 + plugins/kubernetes/package.json | 2 +- plugins/mcp-actions-backend/CHANGELOG.md | 10 + plugins/mcp-actions-backend/package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- plugins/notifications-backend/CHANGELOG.md | 9 + plugins/notifications-backend/package.json | 2 +- plugins/notifications-node/CHANGELOG.md | 7 + plugins/notifications-node/package.json | 2 +- plugins/org-react/CHANGELOG.md | 9 + plugins/org-react/package.json | 2 +- plugins/org/CHANGELOG.md | 9 + plugins/org/package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- plugins/scaffolder-backend/CHANGELOG.md | 12 + plugins/scaffolder-backend/package.json | 2 +- plugins/scaffolder-react/CHANGELOG.md | 10 + plugins/scaffolder-react/package.json | 2 +- plugins/scaffolder/CHANGELOG.md | 13 + plugins/scaffolder/package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- plugins/search/CHANGELOG.md | 9 + plugins/search/package.json | 2 +- .../techdocs-addons-test-utils/CHANGELOG.md | 9 + .../techdocs-addons-test-utils/package.json | 2 +- plugins/techdocs-backend/CHANGELOG.md | 11 + plugins/techdocs-backend/package.json | 2 +- plugins/techdocs/CHANGELOG.md | 12 + plugins/techdocs/package.json | 2 +- plugins/user-settings/CHANGELOG.md | 10 + plugins/user-settings/package.json | 2 +- 140 files changed, 1486 insertions(+), 71 deletions(-) create mode 100644 .changeset/create-app-1757429965.md create mode 100644 docs/releases/v1.43.0-next.2-changelog.md create mode 100644 plugins/auth/CHANGELOG.md diff --git a/.changeset/create-app-1757429965.md b/.changeset/create-app-1757429965.md new file mode 100644 index 0000000000..b50d431d4b --- /dev/null +++ b/.changeset/create-app-1757429965.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Bumped create-app version. diff --git a/.changeset/pre.json b/.changeset/pre.json index dcb0a48960..b1a840849d 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -201,23 +201,42 @@ "@backstage/plugin-techdocs-react": "1.3.2", "@backstage/plugin-user-settings": "0.8.25", "@backstage/plugin-user-settings-backend": "0.3.5", - "@backstage/plugin-user-settings-common": "0.0.1" + "@backstage/plugin-user-settings-common": "0.0.1", + "@backstage/plugin-auth": "0.0.0" }, "changesets": [ + "better-eagles-tickle", + "big-cameras-turn", "brave-bugs-know", + "brave-jars-speak", + "busy-chairs-itch", "cold-donuts-train", "cool-games-rescue", + "create-app-1757429965", "curvy-sites-rhyme", "easy-wings-turn", + "eleven-doors-down", + "eleven-doors-own", "fine-hands-think", + "fix-select-aria-props", "flat-colts-know", + "funny-eagles-try", + "giant-buttons-flash", "giant-zebras-peel", + "gold-words-smoke", + "heavy-cats-unite", + "heavy-lies-listen", "icy-camels-throw", + "itchy-moons-start", + "late-swans-press", "legal-lemons-attend", "lemon-terms-cheer", "lucky-glasses-slide", "mean-sites-cheer", + "olive-moons-burn", "puny-books-fetch", + "quiet-papayas-mate", + "red-shrimps-fall", "ripe-plants-pump", "sharp-carrots-spend", "sixty-pans-prove", @@ -226,9 +245,12 @@ "slick-worms-drum", "social-beers-unite", "sweet-lemons-wonder", + "tangy-squids-film", "tricky-buses-lead", + "warm-emus-itch", "wet-kiwis-strive", "whole-dingos-lay", - "yellow-dragons-float" + "yellow-dragons-float", + "young-doodles-enter" ] } diff --git a/docs/releases/v1.43.0-next.2-changelog.md b/docs/releases/v1.43.0-next.2-changelog.md new file mode 100644 index 0000000000..ef7ddb1c1f --- /dev/null +++ b/docs/releases/v1.43.0-next.2-changelog.md @@ -0,0 +1,695 @@ +# Release v1.43.0-next.2 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.43.0-next.2](https://backstage.github.io/upgrade-helper/?to=1.43.0-next.2) + +## @backstage/catalog-client@1.12.0-next.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +## @backstage/plugin-auth@0.1.0-next.0 + +### Minor Changes + +- 54ddfef: Initial publish of the `auth` frontend package + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.17.6-next.1 + +## @backstage/plugin-catalog-node@1.19.0-next.1 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + +## @backstage/plugin-catalog-react@1.21.0-next.2 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/backend-defaults@0.12.1-next.1 + +### Patch Changes + +- 4eda590: Fixed cache namespace and key prefix separator configuration to properly use configured values instead of hardcoded plugin ID. The cache manager now correctly combines the configured namespace with plugin IDs using the configured separator for Redis and Valkey. Memcache and memory store continue to use plugin ID as namespace. +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/integration-aws-node@0.1.17 + +## @backstage/backend-dynamic-feature-service@0.7.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/plugin-app-node@0.1.37-next.1 + +## @backstage/cli@0.34.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/integration@1.18.0-next.0 + +## @backstage/config-loader@1.10.3-next.0 + +### Patch Changes + +- a73f495: Allow using `BACKSTAGE_ENV` for loading environment specific config files + +## @backstage/core-compat-api@0.5.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + +## @backstage/core-components@0.17.6-next.1 + +### Patch Changes + +- 1ad3d94: Dependency graph can now be opened in full screen mode +- ae7d426: update about card links style for pretty display with other language + +## @backstage/create-app@0.7.4-next.2 + +### Patch Changes + +- Bumped create-app version. + +## @backstage/dev-utils@1.1.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + +## @backstage/repo-tools@0.15.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + +## @backstage/ui@0.7.1-next.0 + +### Patch Changes + +- 7307930: Add missing class for flex: baseline +- 89da341: Fix Select component to properly attach aria-label and aria-labelledby props to the rendered element for improved accessibility. + +## @backstage/plugin-api-docs@0.12.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-app-backend@0.5.6-next.1 + +### Patch Changes + +- afd368e: Internal update to not expose the old `createRouter`. +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-app-node@0.1.37-next.1 + +## @backstage/plugin-app-node@0.1.37-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + +## @backstage/plugin-auth-backend@0.25.4-next.1 + +### Patch Changes + +- 1d47bf3: Implementing Dynamic Client Registration with the OIDC server. You can enable this by setting `auth.experimentalDynamicClientRegistration.enabled` in `app-config.yaml`. This is highly experimental, but feedback welcome. +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-auth-node@0.6.7-next.1 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + +## @backstage/plugin-auth-react@0.1.19-next.1 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/core-components@0.17.6-next.1 + +## @backstage/plugin-catalog@1.31.3-next.2 + +### Patch Changes + +- 85c5e04: Fix incorrect `defaultTarget` on `createComponentRouteRef`. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-catalog-backend@3.0.2-next.1 + +### Patch Changes + +- 2204f5b: Prevent deadlock in catalog deferred stitching +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-aws@0.4.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/integration-aws-node@0.1.17 + +## @backstage/plugin-catalog-backend-module-azure@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-gcp@0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-github@0.11.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-catalog-backend-module-github@0.11.0-next.1 + +## @backstage/plugin-catalog-backend-module-gitlab@0.7.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-catalog-backend-module-gitlab@0.7.3-next.1 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-ldap@0.11.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-msgraph@0.8.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.14-next.1 + +### Patch Changes + +- afd368e: **BREAKING ALPHA**: The module has been moved from the `/alpha` export to the root of the package. +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.6.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-catalog-graph@0.4.23-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-catalog-import@0.13.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-devtools-backend@0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/backend-defaults@0.12.1-next.1 + +## @backstage/plugin-home@0.8.12-next.2 + +### Patch Changes + +- 929c55a: Fixed race condition in CustomHomepageGrid by waiting for storage to load before rendering custom layout to prevent + rendering of the default content. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-kubernetes@0.12.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-kubernetes-backend@0.20.2-next.2 + +### Patch Changes + +- dd7b6d2: Fix a bug where `getDefault` in the `kubernetesFetcherExtensionPoint` had the wrong `this` value +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration-aws-node@0.1.17 + +## @backstage/plugin-kubernetes-cluster@0.0.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + +## @backstage/plugin-mcp-actions-backend@0.1.3-next.1 + +### Patch Changes + +- 1d47bf3: Proxy `/.well-known/oauth-authorization-server` to `/.well-known/openid-configuration` on `auth-backend` when `auth.experimentalDynamicClientRegistration.enabled` is enabled. +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-notifications-backend@0.5.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-notifications-node@0.2.19-next.1 + +## @backstage/plugin-notifications-backend-module-email@0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration-aws-node@0.1.17 + - @backstage/plugin-notifications-node@0.2.19-next.1 + +## @backstage/plugin-notifications-backend-module-slack@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-notifications-node@0.2.19-next.1 + +## @backstage/plugin-notifications-node@0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + +## @backstage/plugin-org@0.6.44-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-org-react@0.1.42-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + +## @backstage/plugin-scaffolder@1.34.1-next.2 + +### Patch Changes + +- 0d415ae: Render a TechDocs link on the Scaffolder Template List page when templates include either `backstage.io/techdocs-ref` or `backstage.io/techdocs-entity` annotations, using the shared `buildTechDocsURL` helper. Also adds tests to verify both annotations and optional `backstage.io/techdocs-entity-path` are respected. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-scaffolder-backend@2.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12-next.1 + - @backstage/plugin-scaffolder-backend-module-github@0.8.3-next.1 + +## @backstage/plugin-scaffolder-backend-module-github@0.8.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + +## @backstage/plugin-scaffolder-react@1.19.1-next.2 + +### Patch Changes + +- 58fc108: Fix scaffolder task log stream not having a minimum height +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + +## @backstage/plugin-search@1.4.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-search-backend-module-catalog@0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-search-backend-module-techdocs@0.4.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + +## @backstage/plugin-techdocs@1.14.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.53-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + +## @backstage/plugin-techdocs-backend@2.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.4.6-next.1 + +## @backstage/plugin-user-settings@0.8.26-next.2 + +### Patch Changes + +- b713b54: Tool-tip text correction for the Theme selection in settings page +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + +## example-app@0.2.113-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.7.1-next.0 + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/plugin-home@0.8.12-next.2 + - @backstage/plugin-scaffolder@1.34.1-next.2 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-user-settings@0.8.26-next.2 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/cli@0.34.2-next.2 + - @backstage/plugin-catalog-graph@0.4.23-next.2 + - @backstage/plugin-catalog-import@0.13.5-next.2 + - @backstage/plugin-org@0.6.44-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + - @backstage/plugin-api-docs@0.12.11-next.2 + - @backstage/plugin-kubernetes@0.12.11-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.29-next.2 + - @backstage/plugin-search@1.4.30-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28-next.0 + +## example-app-next@0.0.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.7.1-next.0 + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/plugin-auth@0.1.0-next.0 + - @backstage/plugin-home@0.8.12-next.2 + - @backstage/plugin-scaffolder@1.34.1-next.2 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-user-settings@0.8.26-next.2 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/cli@0.34.2-next.2 + - @backstage/plugin-catalog-graph@0.4.23-next.2 + - @backstage/plugin-catalog-import@0.13.5-next.2 + - @backstage/plugin-org@0.6.44-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + - @backstage/core-compat-api@0.5.2-next.2 + - @backstage/plugin-api-docs@0.12.11-next.2 + - @backstage/plugin-kubernetes@0.12.11-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.29-next.2 + - @backstage/plugin-search@1.4.30-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28-next.0 diff --git a/package.json b/package.json index d4ebc22bc1..7aed18ab74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.43.0-next.1", + "version": "1.43.0-next.2", "backstage": { "cli": { "new": { diff --git a/packages/app-next/CHANGELOG.md b/packages/app-next/CHANGELOG.md index 5fc353c09d..a4e2386525 100644 --- a/packages/app-next/CHANGELOG.md +++ b/packages/app-next/CHANGELOG.md @@ -1,5 +1,32 @@ # example-app-next +## 0.0.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.7.1-next.0 + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/plugin-auth@0.1.0-next.0 + - @backstage/plugin-home@0.8.12-next.2 + - @backstage/plugin-scaffolder@1.34.1-next.2 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-user-settings@0.8.26-next.2 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/cli@0.34.2-next.2 + - @backstage/plugin-catalog-graph@0.4.23-next.2 + - @backstage/plugin-catalog-import@0.13.5-next.2 + - @backstage/plugin-org@0.6.44-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + - @backstage/core-compat-api@0.5.2-next.2 + - @backstage/plugin-api-docs@0.12.11-next.2 + - @backstage/plugin-kubernetes@0.12.11-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.29-next.2 + - @backstage/plugin-search@1.4.30-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28-next.0 + ## 0.0.27-next.1 ### Patch Changes diff --git a/packages/app-next/package.json b/packages/app-next/package.json index 8e54830cac..49ce264f06 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -1,6 +1,6 @@ { "name": "example-app-next", - "version": "0.0.27-next.1", + "version": "0.0.27-next.2", "backstage": { "role": "frontend" }, diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 93dd1db16a..f3b911accf 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,30 @@ # example-app +## 0.2.113-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.7.1-next.0 + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/plugin-home@0.8.12-next.2 + - @backstage/plugin-scaffolder@1.34.1-next.2 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-user-settings@0.8.26-next.2 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/cli@0.34.2-next.2 + - @backstage/plugin-catalog-graph@0.4.23-next.2 + - @backstage/plugin-catalog-import@0.13.5-next.2 + - @backstage/plugin-org@0.6.44-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + - @backstage/plugin-api-docs@0.12.11-next.2 + - @backstage/plugin-kubernetes@0.12.11-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.29-next.2 + - @backstage/plugin-search@1.4.30-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28-next.0 + ## 0.2.113-next.1 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 580c401a1b..5e38aa6256 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.113-next.1", + "version": "0.2.113-next.2", "backstage": { "role": "frontend" }, diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index 0375e54232..b4c7393455 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/backend-defaults +## 0.12.1-next.1 + +### Patch Changes + +- 4eda590: Fixed cache namespace and key prefix separator configuration to properly use configured values instead of hardcoded plugin ID. The cache manager now correctly combines the configured namespace with plugin IDs using the configured separator for Redis and Valkey. Memcache and memory store continue to use plugin ID as namespace. +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/integration-aws-node@0.1.17 + ## 0.12.1-next.0 ### Patch Changes diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 09bb4dc85d..1b1f0101c7 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-defaults", - "version": "0.12.1-next.0", + "version": "0.12.1-next.1", "description": "Backend defaults used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/backend-dynamic-feature-service/CHANGELOG.md b/packages/backend-dynamic-feature-service/CHANGELOG.md index 6bd6a7a781..d2e46404ef 100644 --- a/packages/backend-dynamic-feature-service/CHANGELOG.md +++ b/packages/backend-dynamic-feature-service/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/backend-dynamic-feature-service +## 0.7.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/plugin-app-node@0.1.37-next.1 + ## 0.7.4-next.0 ### Patch Changes diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index ff8fc29639..6a7cc2315f 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-dynamic-feature-service", - "version": "0.7.4-next.0", + "version": "0.7.4-next.1", "description": "Backstage dynamic feature service", "backstage": { "role": "node-library" diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 52e2ed04a4..254f37bd69 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/catalog-client +## 1.12.0-next.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + ## 1.11.0 ### Minor Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index a87ed29302..3b9d1c603a 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-client", - "version": "1.11.0", + "version": "1.12.0-next.0", "description": "An isomorphic client for the catalog backend", "backstage": { "role": "common-library" diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 87be49efce..bdaf3a05f6 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/cli +## 0.34.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/integration@1.18.0-next.0 + ## 0.34.2-next.1 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 51090239d8..8956b50713 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli", - "version": "0.34.2-next.1", + "version": "0.34.2-next.2", "description": "CLI for developing Backstage plugins and apps", "backstage": { "role": "cli" diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index 6fe939f3e6..a7b38c24b9 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/config-loader +## 1.10.3-next.0 + +### Patch Changes + +- a73f495: Allow using `BACKSTAGE_ENV` for loading environment specific config files + ## 1.10.2 ### Patch Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index f347af8034..b0e7af3b5e 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/config-loader", - "version": "1.10.2", + "version": "1.10.3-next.0", "description": "Config loading functionality used by Backstage backend, and CLI", "backstage": { "role": "node-library" diff --git a/packages/core-compat-api/CHANGELOG.md b/packages/core-compat-api/CHANGELOG.md index a966469313..ea40b301bb 100644 --- a/packages/core-compat-api/CHANGELOG.md +++ b/packages/core-compat-api/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/core-compat-api +## 0.5.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + ## 0.5.2-next.1 ### Patch Changes diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index 1430973106..db51762180 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-compat-api", - "version": "0.5.2-next.1", + "version": "0.5.2-next.2", "backstage": { "role": "web-library" }, diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 515e6eac5b..10e04ae0c6 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/core-components +## 0.17.6-next.1 + +### Patch Changes + +- 1ad3d94: Dependency graph can now be opened in full screen mode +- ae7d426: update about card links style for pretty display with other language + ## 0.17.6-next.0 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index d93ff26333..5cd6b9f270 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-components", - "version": "0.17.6-next.0", + "version": "0.17.6-next.1", "description": "Core components used by Backstage plugins and apps", "backstage": { "role": "web-library" diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 261e595b30..0e622c2490 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/create-app +## 0.7.4-next.2 + +### Patch Changes + +- Bumped create-app version. + ## 0.7.4-next.1 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 7f2f9e4cd5..59f8498c54 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/create-app", - "version": "0.7.4-next.1", + "version": "0.7.4-next.2", "description": "A CLI that helps you create your own Backstage app", "backstage": { "role": "cli" diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 503ae406ae..45aaf0a814 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/dev-utils +## 1.1.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + ## 1.1.14-next.1 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 55b4c3fdfe..fd5881af44 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/dev-utils", - "version": "1.1.14-next.1", + "version": "1.1.14-next.2", "description": "Utilities for developing Backstage plugins.", "backstage": { "role": "web-library" diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index 569444b4b8..a5a9bb0505 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/repo-tools +## 0.15.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + ## 0.15.2-next.0 ### Patch Changes diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index 9b7c875279..d93785f25e 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/repo-tools", - "version": "0.15.2-next.0", + "version": "0.15.2-next.1", "description": "CLI for Backstage repo tooling ", "backstage": { "role": "cli" diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index a88c4593e1..d251516dcd 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/ui +## 0.7.1-next.0 + +### Patch Changes + +- 7307930: Add missing class for flex: baseline +- 89da341: Fix Select component to properly attach aria-label and aria-labelledby props to the rendered element for improved accessibility. + ## 0.7.0 ### Minor Changes diff --git a/packages/ui/package.json b/packages/ui/package.json index 2ac399fcee..36b51f0bc8 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/ui", - "version": "0.7.0", + "version": "0.7.1-next.0", "backstage": { "role": "web-library" }, diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 25671d8050..c57a56754c 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-api-docs +## 0.12.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.12.11-next.1 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 4fcae79fba..17cf78a69a 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.12.11-next.1", + "version": "0.12.11-next.2", "description": "A Backstage plugin that helps represent API entities in the frontend", "backstage": { "role": "frontend-plugin", diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index bd097b0005..ffe2bd1b71 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-app-backend +## 0.5.6-next.1 + +### Patch Changes + +- afd368e: Internal update to not expose the old `createRouter`. +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-app-node@0.1.37-next.1 + ## 0.5.6-next.0 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index b81e709926..4640a6459b 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.5.6-next.0", + "version": "0.5.6-next.1", "description": "A Backstage backend plugin that serves the Backstage frontend app", "backstage": { "role": "backend-plugin", diff --git a/plugins/app-node/CHANGELOG.md b/plugins/app-node/CHANGELOG.md index 2063c2db48..14b3a215ee 100644 --- a/plugins/app-node/CHANGELOG.md +++ b/plugins/app-node/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-app-node +## 0.1.37-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + ## 0.1.37-next.0 ### Patch Changes diff --git a/plugins/app-node/package.json b/plugins/app-node/package.json index 07340379e9..f28b28f461 100644 --- a/plugins/app-node/package.json +++ b/plugins/app-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-node", - "version": "0.1.37-next.0", + "version": "0.1.37-next.1", "description": "Node.js library for the app plugin", "backstage": { "role": "node-library", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 54dee8262d..7aa33c315a 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-auth-backend +## 0.25.4-next.1 + +### Patch Changes + +- 1d47bf3: Implementing Dynamic Client Registration with the OIDC server. You can enable this by setting `auth.experimentalDynamicClientRegistration.enabled` in `app-config.yaml`. This is highly experimental, but feedback welcome. +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.25.4-next.0 ### Patch Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index bf52739ea4..e5f048ccae 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.25.4-next.0", + "version": "0.25.4-next.1", "description": "A Backstage backend plugin that handles authentication", "backstage": { "role": "backend-plugin", diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index 48554f6932..3c2ceea7c0 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-node +## 0.6.7-next.1 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + ## 0.6.7-next.0 ### Patch Changes diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index a846a868eb..64c12ae977 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.6.7-next.0", + "version": "0.6.7-next.1", "backstage": { "role": "node-library", "pluginId": "auth", diff --git a/plugins/auth-react/CHANGELOG.md b/plugins/auth-react/CHANGELOG.md index 1dc720ab67..9b314d3e4f 100644 --- a/plugins/auth-react/CHANGELOG.md +++ b/plugins/auth-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-react +## 0.1.19-next.1 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/core-components@0.17.6-next.1 + ## 0.1.19-next.0 ### Patch Changes diff --git a/plugins/auth-react/package.json b/plugins/auth-react/package.json index 2c6b5d6ff2..2bfee0dd44 100644 --- a/plugins/auth-react/package.json +++ b/plugins/auth-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-react", - "version": "0.1.19-next.0", + "version": "0.1.19-next.1", "description": "Web library for the auth plugin", "backstage": { "role": "web-library", diff --git a/plugins/auth/CHANGELOG.md b/plugins/auth/CHANGELOG.md new file mode 100644 index 0000000000..d28fef1c9e --- /dev/null +++ b/plugins/auth/CHANGELOG.md @@ -0,0 +1,12 @@ +# @backstage/plugin-auth + +## 0.1.0-next.0 + +### Minor Changes + +- 54ddfef: Initial publish of the `auth` frontend package + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.17.6-next.1 diff --git a/plugins/auth/package.json b/plugins/auth/package.json index 54e87cb7b1..6c8184db35 100644 --- a/plugins/auth/package.json +++ b/plugins/auth/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth", - "version": "0.0.0", + "version": "0.1.0-next.0", "backstage": { "role": "frontend-plugin", "pluginId": "auth", diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index 3ba5538fc5..7ceff47a80 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.4.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/integration-aws-node@0.1.17 + ## 0.4.15-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 9f98b788a7..4dfe5b990d 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", - "version": "0.4.15-next.0", + "version": "0.4.15-next.1", "description": "A Backstage catalog backend module that helps integrate towards AWS", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md index 31364b56b3..0962c2aa28 100644 --- a/plugins/catalog-backend-module-azure/CHANGELOG.md +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-azure +## 0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.3.9-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index 51956ca1a5..dad34fec2a 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-azure", - "version": "0.3.9-next.0", + "version": "0.3.9-next.1", "description": "A Backstage catalog backend module that helps integrate towards Azure", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md index 7f31323ddd..d40c4e03e4 100644 --- a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-backend-module-backstage-openapi +## 0.5.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.5.6-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-backstage-openapi/package.json b/plugins/catalog-backend-module-backstage-openapi/package.json index db55d563de..f004e1bb16 100644 --- a/plugins/catalog-backend-module-backstage-openapi/package.json +++ b/plugins/catalog-backend-module-backstage-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-backstage-openapi", - "version": "0.5.6-next.0", + "version": "0.5.6-next.1", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md index edb0a14cf3..c462210097 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-bitbucket-cloud +## 0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.5.3-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index 56ab813052..97eea0897f 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-cloud", - "version": "0.5.3-next.0", + "version": "0.5.3-next.1", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket Cloud", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md index 42964bad70..66965f3009 100644 --- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-bitbucket-server +## 0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.5.3-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index a3cf31ee9a..98d28131a9 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-server", - "version": "0.5.3-next.0", + "version": "0.5.3-next.1", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-gcp/CHANGELOG.md b/plugins/catalog-backend-module-gcp/CHANGELOG.md index 9cf3bcb98f..fa30682d37 100644 --- a/plugins/catalog-backend-module-gcp/CHANGELOG.md +++ b/plugins/catalog-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-backend-module-gcp +## 0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.3.12-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-gcp/package.json b/plugins/catalog-backend-module-gcp/package.json index ac095bfc4a..5459221795 100644 --- a/plugins/catalog-backend-module-gcp/package.json +++ b/plugins/catalog-backend-module-gcp/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gcp", - "version": "0.3.12-next.0", + "version": "0.3.12-next.1", "description": "A Backstage catalog backend module that helps integrate towards GCP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gerrit/CHANGELOG.md b/plugins/catalog-backend-module-gerrit/CHANGELOG.md index eaf94c852a..ea0a8a0bbb 100644 --- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-gerrit +## 0.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.3.6-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index 62f17f1b26..9dac6c9526 100644 --- a/plugins/catalog-backend-module-gerrit/package.json +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gerrit", - "version": "0.3.6-next.0", + "version": "0.3.6-next.1", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-gitea/CHANGELOG.md b/plugins/catalog-backend-module-gitea/CHANGELOG.md index 036dd6b477..f8b03e496c 100644 --- a/plugins/catalog-backend-module-gitea/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitea/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-gitea +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.1.4-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitea/package.json b/plugins/catalog-backend-module-gitea/package.json index 3aaf419c82..521179be95 100644 --- a/plugins/catalog-backend-module-gitea/package.json +++ b/plugins/catalog-backend-module-gitea/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitea", - "version": "0.1.4-next.0", + "version": "0.1.4-next.1", "license": "Apache-2.0", "description": "The gitea backend module for the catalog plugin.", "main": "src/index.ts", diff --git a/plugins/catalog-backend-module-github-org/CHANGELOG.md b/plugins/catalog-backend-module-github-org/CHANGELOG.md index d6110b1f5a..9007c5ab97 100644 --- a/plugins/catalog-backend-module-github-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-github-org/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-github-org +## 0.3.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-catalog-backend-module-github@0.11.0-next.1 + ## 0.3.14-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-github-org/package.json b/plugins/catalog-backend-module-github-org/package.json index 04029f57b9..ff2b78abc1 100644 --- a/plugins/catalog-backend-module-github-org/package.json +++ b/plugins/catalog-backend-module-github-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github-org", - "version": "0.3.14-next.0", + "version": "0.3.14-next.1", "description": "The github-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md index b412ef8780..444d602783 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-backend-module-github +## 0.11.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.11.0-next.0 ### Minor Changes diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index a61deeb214..42c8446a75 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github", - "version": "0.11.0-next.0", + "version": "0.11.0-next.1", "description": "A Backstage catalog backend module that helps integrate towards GitHub", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md index afd3317620..88d11fb806 100644 --- a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-gitlab-org +## 0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-catalog-backend-module-gitlab@0.7.3-next.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab-org/package.json b/plugins/catalog-backend-module-gitlab-org/package.json index 05b9e75a0c..98d45ebe61 100644 --- a/plugins/catalog-backend-module-gitlab-org/package.json +++ b/plugins/catalog-backend-module-gitlab-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab-org", - "version": "0.2.13-next.0", + "version": "0.2.13-next.1", "description": "The gitlab-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md index 958c3b364a..adbc95ee65 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.7.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.7.3-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 7d8fe59327..14d62a2ad8 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab", - "version": "0.7.3-next.0", + "version": "0.7.3-next.1", "description": "A Backstage catalog backend module that helps integrate towards GitLab", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md index 9139c3e3f1..1d93406c49 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md +++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-incremental-ingestion +## 0.7.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-catalog-backend@3.0.2-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.7.4-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json index 5a2acc53fc..7a83dd50af 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/package.json +++ b/plugins/catalog-backend-module-incremental-ingestion/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-incremental-ingestion", - "version": "0.7.4-next.0", + "version": "0.7.4-next.1", "description": "An entity provider for streaming large asset sources into the catalog", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index 83b25b4f4c..343a87c514 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.11.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.11.9-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 9e32b21bb3..02e2216d09 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", - "version": "0.11.9-next.0", + "version": "0.11.9-next.1", "description": "A Backstage catalog backend module that helps integrate towards LDAP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index b48d58fbd2..0ecb111c7a 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.8.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.8.0-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index facfd228a3..12fcfa0585 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", - "version": "0.8.0-next.1", + "version": "0.8.0-next.2", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md index 6c82466060..4a1c9eca0a 100644 --- a/plugins/catalog-backend-module-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-openapi +## 0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.2.14-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index 3902aafcb7..fec08415cd 100644 --- a/plugins/catalog-backend-module-openapi/package.json +++ b/plugins/catalog-backend-module-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-openapi", - "version": "0.2.14-next.0", + "version": "0.2.14-next.1", "description": "A Backstage catalog backend module that helps with OpenAPI specifications", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md index 141e2d52ad..dee1e885fc 100644 --- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md +++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-puppetdb +## 0.2.14-next.1 + +### Patch Changes + +- afd368e: **BREAKING ALPHA**: The module has been moved from the `/alpha` export to the root of the package. +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.2.14-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index 91e45a63f7..51c43d475f 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-puppetdb", - "version": "0.2.14-next.0", + "version": "0.2.14-next.1", "description": "A Backstage catalog backend module that helps integrate towards PuppetDB", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md index 4a6c195039..9c17f17eaa 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md +++ b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-backend-module-scaffolder-entity-model +## 0.2.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.2.12-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/package.json b/plugins/catalog-backend-module-scaffolder-entity-model/package.json index 6e040f8078..7019619a6c 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/package.json +++ b/plugins/catalog-backend-module-scaffolder-entity-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-scaffolder-entity-model", - "version": "0.2.12-next.0", + "version": "0.2.12-next.1", "description": "Adds support for the scaffolder specific entity model (e.g. the Template kind) to the catalog backend plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md index 28436537b0..ede26e78d1 100644 --- a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md +++ b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-unprocessed +## 0.6.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.6.4-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-unprocessed/package.json b/plugins/catalog-backend-module-unprocessed/package.json index a3de0c75a4..e949240c64 100644 --- a/plugins/catalog-backend-module-unprocessed/package.json +++ b/plugins/catalog-backend-module-unprocessed/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-unprocessed", - "version": "0.6.4-next.0", + "version": "0.6.4-next.1", "description": "Backstage Catalog module to view unprocessed entities", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index ad535f16fd..52ebffab31 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-backend +## 3.0.2-next.1 + +### Patch Changes + +- 2204f5b: Prevent deadlock in catalog deferred stitching +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 3.0.2-next.0 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index d825f29356..7a2df294fd 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "3.0.2-next.0", + "version": "3.0.2-next.1", "description": "The Backstage backend plugin that provides the Backstage catalog", "backstage": { "role": "backend-plugin", diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index a756916c38..5fa578c1ad 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-graph +## 0.4.23-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.4.23-next.1 ### Patch Changes diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index dd3271f468..777359e693 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.4.23-next.1", + "version": "0.4.23-next.2", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-graph", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 77ad27ced9..ecd0d8bc46 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-import +## 0.13.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.13.5-next.1 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 050e8be908..39ed5bcef0 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.13.5-next.1", + "version": "0.13.5-next.2", "description": "A Backstage plugin the helps you import entities into your catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/catalog-node/CHANGELOG.md b/plugins/catalog-node/CHANGELOG.md index 18f44ba88f..14ac349b6d 100644 --- a/plugins/catalog-node/CHANGELOG.md +++ b/plugins/catalog-node/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-catalog-node +## 1.19.0-next.1 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + ## 1.18.1-next.0 ### Patch Changes diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index a61c4b801b..3111765897 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-node", - "version": "1.18.1-next.0", + "version": "1.19.0-next.1", "description": "The plugin-catalog-node module for @backstage/plugin-catalog-backend", "backstage": { "role": "node-library", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index d6eb0dc5fe..7ab8dcd967 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-catalog-react +## 1.21.0-next.2 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 1.20.2-next.1 ### Patch Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 3912d9ce9d..0b072f51ce 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-react", - "version": "1.20.2-next.1", + "version": "1.21.0-next.2", "description": "A frontend library that helps other Backstage plugins interact with the catalog", "backstage": { "role": "web-library", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 19c35e2137..cddfdf18bc 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog +## 1.31.3-next.2 + +### Patch Changes + +- 85c5e04: Fix incorrect `defaultTarget` on `createComponentRouteRef`. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 1.31.3-next.1 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 9e1b0fcc05..fd0c1c4375 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "1.31.3-next.1", + "version": "1.31.3-next.2", "description": "The Backstage plugin for browsing the Backstage catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/devtools-backend/CHANGELOG.md b/plugins/devtools-backend/CHANGELOG.md index 4da746e10a..dbd27d37d9 100644 --- a/plugins/devtools-backend/CHANGELOG.md +++ b/plugins/devtools-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-devtools-backend +## 0.5.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3-next.0 + - @backstage/backend-defaults@0.12.1-next.1 + ## 0.5.9-next.0 ### Patch Changes diff --git a/plugins/devtools-backend/package.json b/plugins/devtools-backend/package.json index 038fcbce82..5bd90bdd41 100644 --- a/plugins/devtools-backend/package.json +++ b/plugins/devtools-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-backend", - "version": "0.5.9-next.0", + "version": "0.5.9-next.1", "backstage": { "role": "backend-plugin", "pluginId": "devtools", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index ea7326d9b3..1d2709648f 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-home +## 0.8.12-next.2 + +### Patch Changes + +- 929c55a: Fixed race condition in CustomHomepageGrid by waiting for storage to load before rendering custom layout to prevent + rendering of the default content. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.8.12-next.1 ### Patch Changes diff --git a/plugins/home/package.json b/plugins/home/package.json index 21469df7cd..bc8d71d23b 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home", - "version": "0.8.12-next.1", + "version": "0.8.12-next.2", "description": "A Backstage plugin that helps you build a home page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 761e87e5e0..0986e613f8 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-kubernetes-backend +## 0.20.2-next.2 + +### Patch Changes + +- dd7b6d2: Fix a bug where `getDefault` in the `kubernetesFetcherExtensionPoint` had the wrong `this` value +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration-aws-node@0.1.17 + ## 0.20.2-next.1 ### Patch Changes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index bae94407b5..15722c373d 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.20.2-next.1", + "version": "0.20.2-next.2", "description": "A Backstage backend plugin that integrates towards Kubernetes", "backstage": { "role": "backend-plugin", diff --git a/plugins/kubernetes-cluster/CHANGELOG.md b/plugins/kubernetes-cluster/CHANGELOG.md index 63ee864a46..af3a23232d 100644 --- a/plugins/kubernetes-cluster/CHANGELOG.md +++ b/plugins/kubernetes-cluster/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-kubernetes-cluster +## 0.0.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + ## 0.0.29-next.1 ### Patch Changes diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json index 6488b959f6..0f9ab60401 100644 --- a/plugins/kubernetes-cluster/package.json +++ b/plugins/kubernetes-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-cluster", - "version": "0.0.29-next.1", + "version": "0.0.29-next.2", "description": "A Backstage plugin that shows details of Kubernetes clusters", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 6f76f6bc2a..5a1643287e 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-kubernetes +## 0.12.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.12.11-next.1 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 3fd141319c..d0b553a4eb 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.12.11-next.1", + "version": "0.12.11-next.2", "description": "A Backstage plugin that integrates towards Kubernetes", "backstage": { "role": "frontend-plugin", diff --git a/plugins/mcp-actions-backend/CHANGELOG.md b/plugins/mcp-actions-backend/CHANGELOG.md index 0ca964fbd1..e7578dff07 100644 --- a/plugins/mcp-actions-backend/CHANGELOG.md +++ b/plugins/mcp-actions-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-mcp-actions-backend +## 0.1.3-next.1 + +### Patch Changes + +- 1d47bf3: Proxy `/.well-known/oauth-authorization-server` to `/.well-known/openid-configuration` on `auth-backend` when `auth.experimentalDynamicClientRegistration.enabled` is enabled. +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.1.3-next.0 ### Patch Changes diff --git a/plugins/mcp-actions-backend/package.json b/plugins/mcp-actions-backend/package.json index 2a7fbdf8a3..4ed2250925 100644 --- a/plugins/mcp-actions-backend/package.json +++ b/plugins/mcp-actions-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-mcp-actions-backend", - "version": "0.1.3-next.0", + "version": "0.1.3-next.1", "backstage": { "role": "backend-plugin", "pluginId": "mcp-actions", diff --git a/plugins/notifications-backend-module-email/CHANGELOG.md b/plugins/notifications-backend-module-email/CHANGELOG.md index 9a46dad0bc..8619edd6f4 100644 --- a/plugins/notifications-backend-module-email/CHANGELOG.md +++ b/plugins/notifications-backend-module-email/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-notifications-backend-module-email +## 0.3.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration-aws-node@0.1.17 + - @backstage/plugin-notifications-node@0.2.19-next.1 + ## 0.3.13-next.0 ### Patch Changes diff --git a/plugins/notifications-backend-module-email/package.json b/plugins/notifications-backend-module-email/package.json index 602edf0e66..6844a77d3c 100644 --- a/plugins/notifications-backend-module-email/package.json +++ b/plugins/notifications-backend-module-email/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-email", - "version": "0.3.13-next.0", + "version": "0.3.13-next.1", "description": "The email backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/notifications-backend-module-slack/CHANGELOG.md b/plugins/notifications-backend-module-slack/CHANGELOG.md index e38e8f9d07..a9d8074e5f 100644 --- a/plugins/notifications-backend-module-slack/CHANGELOG.md +++ b/plugins/notifications-backend-module-slack/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-notifications-backend-module-slack +## 0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-notifications-node@0.2.19-next.1 + ## 0.1.5-next.0 ### Patch Changes diff --git a/plugins/notifications-backend-module-slack/package.json b/plugins/notifications-backend-module-slack/package.json index 227201984b..6b32e1b31d 100644 --- a/plugins/notifications-backend-module-slack/package.json +++ b/plugins/notifications-backend-module-slack/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-slack", - "version": "0.1.5-next.0", + "version": "0.1.5-next.1", "description": "The slack backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/notifications-backend/CHANGELOG.md b/plugins/notifications-backend/CHANGELOG.md index 41905ec918..3391980f44 100644 --- a/plugins/notifications-backend/CHANGELOG.md +++ b/plugins/notifications-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-notifications-backend +## 0.5.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/plugin-notifications-node@0.2.19-next.1 + ## 0.5.10-next.0 ### Patch Changes diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index 94c42005c0..a7933b0fc8 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend", - "version": "0.5.10-next.0", + "version": "0.5.10-next.1", "backstage": { "role": "backend-plugin", "pluginId": "notifications", diff --git a/plugins/notifications-node/CHANGELOG.md b/plugins/notifications-node/CHANGELOG.md index af3c020fd3..77e35d0996 100644 --- a/plugins/notifications-node/CHANGELOG.md +++ b/plugins/notifications-node/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-notifications-node +## 0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + ## 0.2.19-next.0 ### Patch Changes diff --git a/plugins/notifications-node/package.json b/plugins/notifications-node/package.json index c20cf4c820..234d9d342f 100644 --- a/plugins/notifications-node/package.json +++ b/plugins/notifications-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-node", - "version": "0.2.19-next.0", + "version": "0.2.19-next.1", "description": "Node.js library for the notifications plugin", "backstage": { "role": "node-library", diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md index 410b9da982..6cf5609da0 100644 --- a/plugins/org-react/CHANGELOG.md +++ b/plugins/org-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-org-react +## 0.1.42-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + ## 0.1.42-next.1 ### Patch Changes diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json index f8410a3f77..06c4135396 100644 --- a/plugins/org-react/package.json +++ b/plugins/org-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org-react", - "version": "0.1.42-next.1", + "version": "0.1.42-next.2", "backstage": { "role": "web-library", "pluginId": "org", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index ee5def4fcf..9df2d654e3 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-org +## 0.6.44-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.6.44-next.1 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index df5b58446c..96a4ab9e62 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org", - "version": "0.6.44-next.1", + "version": "0.6.44-next.2", "description": "A Backstage plugin that helps you create entity pages for your organization", "backstage": { "role": "frontend-plugin", diff --git a/plugins/scaffolder-backend-module-github/CHANGELOG.md b/plugins/scaffolder-backend-module-github/CHANGELOG.md index 9e99278912..2c396607d5 100644 --- a/plugins/scaffolder-backend-module-github/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-github/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-scaffolder-backend-module-github +## 0.8.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + ## 0.8.3-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index 15dedf5ac8..2d28caf5c6 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-github", - "version": "0.8.3-next.0", + "version": "0.8.3-next.1", "description": "The github module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 5fbb8edcc2..305afa351c 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend +## 2.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/plugin-auth-node@0.6.7-next.1 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12-next.1 + - @backstage/plugin-scaffolder-backend-module-github@0.8.3-next.1 + ## 2.2.1-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 6b2f9ef0cb..2e77b90177 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "2.2.1-next.0", + "version": "2.2.1-next.1", "description": "The Backstage backend plugin that helps you create new things", "backstage": { "role": "backend-plugin", diff --git a/plugins/scaffolder-react/CHANGELOG.md b/plugins/scaffolder-react/CHANGELOG.md index 78d40108f4..00c948a51b 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-react +## 1.19.1-next.2 + +### Patch Changes + +- 58fc108: Fix scaffolder task log stream not having a minimum height +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + ## 1.19.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index 1d8737db72..25c8ea0ec6 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-react", - "version": "1.19.1-next.1", + "version": "1.19.1-next.2", "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", "backstage": { "role": "web-library", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index bbb826965c..420938d6fd 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder +## 1.34.1-next.2 + +### Patch Changes + +- 0d415ae: Render a TechDocs link on the Scaffolder Template List page when templates include either `backstage.io/techdocs-ref` or `backstage.io/techdocs-entity` annotations, using the shared `buildTechDocsURL` helper. Also adds tests to verify both annotations and optional `backstage.io/techdocs-entity-path` are respected. +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/plugin-scaffolder-react@1.19.1-next.2 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + ## 1.34.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index d9a63c7661..695afd2244 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "1.34.1-next.1", + "version": "1.34.1-next.2", "description": "The Backstage plugin that helps you create new things", "backstage": { "role": "frontend-plugin", diff --git a/plugins/search-backend-module-catalog/CHANGELOG.md b/plugins/search-backend-module-catalog/CHANGELOG.md index e9a56fa5f9..8f60690872 100644 --- a/plugins/search-backend-module-catalog/CHANGELOG.md +++ b/plugins/search-backend-module-catalog/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-backend-module-catalog +## 0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.3.8-next.0 ### Patch Changes diff --git a/plugins/search-backend-module-catalog/package.json b/plugins/search-backend-module-catalog/package.json index 9bcc2287ff..0ebd03caa6 100644 --- a/plugins/search-backend-module-catalog/package.json +++ b/plugins/search-backend-module-catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-catalog", - "version": "0.3.8-next.0", + "version": "0.3.8-next.1", "description": "A module for the search backend that exports catalog modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-techdocs/CHANGELOG.md b/plugins/search-backend-module-techdocs/CHANGELOG.md index 1938883b9e..ead9c04a7b 100644 --- a/plugins/search-backend-module-techdocs/CHANGELOG.md +++ b/plugins/search-backend-module-techdocs/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-backend-module-techdocs +## 0.4.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + ## 0.4.6-next.0 ### Patch Changes diff --git a/plugins/search-backend-module-techdocs/package.json b/plugins/search-backend-module-techdocs/package.json index 1faffa2b90..d1b784a4d8 100644 --- a/plugins/search-backend-module-techdocs/package.json +++ b/plugins/search-backend-module-techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-techdocs", - "version": "0.4.6-next.0", + "version": "0.4.6-next.1", "description": "A module for the search backend that exports techdocs modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 8449247680..8e9ca88787 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-search +## 1.4.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 1.4.30-next.1 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index 7600d61b86..b58993b65c 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "1.4.30-next.1", + "version": "1.4.30-next.2", "description": "The Backstage plugin that provides your backstage app with search", "backstage": { "role": "frontend-plugin", diff --git a/plugins/techdocs-addons-test-utils/CHANGELOG.md b/plugins/techdocs-addons-test-utils/CHANGELOG.md index a6b33f9837..714fd56c8f 100644 --- a/plugins/techdocs-addons-test-utils/CHANGELOG.md +++ b/plugins/techdocs-addons-test-utils/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-techdocs-addons-test-utils +## 1.0.53-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/plugin-catalog@1.31.3-next.2 + - @backstage/plugin-techdocs@1.14.2-next.2 + ## 1.0.53-next.1 ### Patch Changes diff --git a/plugins/techdocs-addons-test-utils/package.json b/plugins/techdocs-addons-test-utils/package.json index eebe45a88a..94fd865e02 100644 --- a/plugins/techdocs-addons-test-utils/package.json +++ b/plugins/techdocs-addons-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-addons-test-utils", - "version": "1.0.53-next.1", + "version": "1.0.53-next.2", "backstage": { "role": "web-library", "pluginId": "techdocs-addons", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 036ba4387c..98b42c0b84 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-techdocs-backend +## 2.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-node@1.19.0-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.4.6-next.1 + ## 2.1.0-next.0 ### Minor Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index bc6bfd6175..fd1ed4cadf 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "2.1.0-next.0", + "version": "2.1.0-next.1", "description": "The Backstage backend plugin that renders technical documentation for your components", "backstage": { "role": "backend-plugin", diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index c4f6ed3cef..02593351ee 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-techdocs +## 1.14.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-react@0.1.19-next.1 + - @backstage/catalog-client@1.12.0-next.0 + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/integration@1.18.0-next.0 + - @backstage/core-compat-api@0.5.2-next.2 + ## 1.14.2-next.1 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 7ecec3cd0a..ca867db802 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "1.14.2-next.1", + "version": "1.14.2-next.2", "description": "The Backstage plugin that renders technical documentation for your components", "backstage": { "role": "frontend-plugin", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index 5d4f80079f..cfb4c0b712 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-user-settings +## 0.8.26-next.2 + +### Patch Changes + +- b713b54: Tool-tip text correction for the Theme selection in settings page +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0-next.2 + - @backstage/core-components@0.17.6-next.1 + - @backstage/core-compat-api@0.5.2-next.2 + ## 0.8.26-next.1 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index d5224ac8b6..3ce96f9146 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings", - "version": "0.8.26-next.1", + "version": "0.8.26-next.2", "description": "A Backstage plugin that provides a settings page", "backstage": { "role": "frontend-plugin", From 442fe8ff4840c361373383a3c1cfce167b2879f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Tue, 9 Sep 2025 22:52:16 +0200 Subject: [PATCH 145/233] Removed the onPostTransformation hook and all debug-related logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/catalog-graph/report.api.md | 8 ----- .../EntityRelationsGraph.tsx | 4 --- .../useEntityRelationNodesAndEdges.ts | 20 ----------- plugins/catalog-graph/src/index.ts | 5 +-- .../src/lib/graph-transformations/index.ts | 7 +--- .../src/lib/graph-transformations/types.ts | 35 ------------------- 6 files changed, 2 insertions(+), 77 deletions(-) diff --git a/plugins/catalog-graph/report.api.md b/plugins/catalog-graph/report.api.md index 883c79acf4..8194209cf5 100644 --- a/plugins/catalog-graph/report.api.md +++ b/plugins/catalog-graph/report.api.md @@ -181,16 +181,8 @@ export type EntityRelationsGraphProps = { renderLabel?: DependencyGraphTypes.RenderLabelFunction; curve?: 'curveStepBefore' | 'curveMonotoneX'; showArrowHeads?: boolean; - onPostTransformation?: GraphTransformationDebugger; }; -// @public -export type GraphTransformationDebugger = ( - transformation: string | undefined, - transformationContext: TransformationContext, - clonedContext: TransformationContext, -) => void; - // @public export type RelationPairs = [string, string][]; diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx index 47ce365cb6..f77ca4b37d 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx @@ -33,7 +33,6 @@ import { DefaultRenderNode } from './DefaultRenderNode'; import { RelationPairs } from '../../lib/types'; import { Direction, EntityEdge, EntityNode } from '../../lib/types'; import { useEntityRelationNodesAndEdges } from './useEntityRelationNodesAndEdges'; -import { GraphTransformationDebugger } from '../../lib/graph-transformations'; /** @public */ export type EntityRelationsGraphClassKey = 'progress' | 'container' | 'graph'; @@ -92,7 +91,6 @@ export type EntityRelationsGraphProps = { renderLabel?: DependencyGraphTypes.RenderLabelFunction; curve?: 'curveStepBefore' | 'curveMonotoneX'; showArrowHeads?: boolean; - onPostTransformation?: GraphTransformationDebugger; }; /** @@ -118,7 +116,6 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => { renderLabel, curve, showArrowHeads, - onPostTransformation, } = props; const theme = useTheme(); @@ -142,7 +139,6 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => { entityFilter, onNodeClick, relationPairs, - onPostTransformation, }); useEffect(() => { diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts index 2aa301fef9..7a6f8361e5 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts @@ -24,8 +24,6 @@ import { buildGraph } from '../../lib/graph'; import { BuiltInTransformations, builtInTransformations, - cloneTransformationContext, - GraphTransformationDebugger, GraphTransformer, TransformationContext, } from '../../lib/graph-transformations'; @@ -43,7 +41,6 @@ export function useEntityRelationNodesAndEdges({ entityFilter, onNodeClick, relationPairs: incomingRelationPairs, - onPostTransformation, }: { rootEntityRefs: string[]; maxDepth?: number; @@ -54,7 +51,6 @@ export function useEntityRelationNodesAndEdges({ entityFilter?: (entity: Entity) => boolean; onNodeClick?: (value: EntityNode, event: MouseEvent) => void; relationPairs?: RelationPairs; - onPostTransformation?: GraphTransformationDebugger; }): { loading: boolean; nodes?: EntityNode[]; @@ -129,20 +125,6 @@ export function useEntityRelationNodesAndEdges({ maxDepth, }; - const debugTransformation = ( - transformation: BuiltInTransformations | GraphTransformer | undefined, - ) => { - onPostTransformation?.( - typeof transformation === 'function' - ? transformation.name - : transformation, - transformationContext, - cloneTransformationContext(transformationContext), - ); - }; - - debugTransformation(undefined); - const runTransformation = ( transformation: BuiltInTransformations | GraphTransformer, ) => { @@ -151,8 +133,6 @@ export function useEntityRelationNodesAndEdges({ } else { builtInTransformations[transformation](transformationContext); } - - debugTransformation(transformation); }; runTransformation('reduce-edges'); diff --git a/plugins/catalog-graph/src/index.ts b/plugins/catalog-graph/src/index.ts index d020e96989..95ac5048cb 100644 --- a/plugins/catalog-graph/src/index.ts +++ b/plugins/catalog-graph/src/index.ts @@ -35,7 +35,4 @@ export type { EntityNode, } from './lib/types'; export { Direction } from './lib/types'; -export type { - TransformationContext, - GraphTransformationDebugger, -} from './lib/graph-transformations'; +export type { TransformationContext } from './lib/graph-transformations'; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/index.ts b/plugins/catalog-graph/src/lib/graph-transformations/index.ts index 3ed4cdba21..764a20a8d2 100644 --- a/plugins/catalog-graph/src/lib/graph-transformations/index.ts +++ b/plugins/catalog-graph/src/lib/graph-transformations/index.ts @@ -14,12 +14,7 @@ * limitations under the License. */ -export type { - GraphTransformer, - TransformationContext, - GraphTransformationDebugger, -} from './types'; -export { cloneTransformationContext } from './types'; +export type { GraphTransformer, TransformationContext } from './types'; import { reduceEdges } from './reduce-edges'; import { setDistances } from './set-distance'; diff --git a/plugins/catalog-graph/src/lib/graph-transformations/types.ts b/plugins/catalog-graph/src/lib/graph-transformations/types.ts index 9e498ba027..dc13057a04 100644 --- a/plugins/catalog-graph/src/lib/graph-transformations/types.ts +++ b/plugins/catalog-graph/src/lib/graph-transformations/types.ts @@ -45,38 +45,3 @@ export interface TransformationContext { * @public */ export type GraphTransformer = (context: TransformationContext) => void; - -/** - * A function that debugs a graph transformation. - * It is given the three arguments: - * * The transformation that was just applied (or undefined before the first - * transformation). If the transformation is a function, its `.name` property - * will be used. - * * The current state (context) of the graph, which _can_ be mutated - * * A cloned copy of the context, useful for logging (as transformations are - * made in place, the original context is modified and the logs will be - * confusing) - * - * @public - */ -export type GraphTransformationDebugger = ( - transformation: string | undefined, - transformationContext: TransformationContext, - clonedContext: TransformationContext, -) => void; - -/** @internal */ -export function cloneTransformationContext( - transformationContext: TransformationContext, -): TransformationContext { - const clonesContext = JSON.parse( - JSON.stringify(transformationContext), - ) as TransformationContext; - clonesContext.edges = clonesContext.edges.sort((a, b) => { - return a.from.localeCompare(b.from) || a.to.localeCompare(b.to); - }); - - clonesContext.nodeDistances = new Map(transformationContext.nodeDistances); - - return clonesContext; -} From 33bb3c2305f1d72a45c13ef3dab3008a2cecc8e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 19:22:45 +0000 Subject: [PATCH 146/233] fix(deps): update dependency dompurify to v3.2.4 [security] Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index 50a73bb657..42cf3d4a3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21537,10 +21537,10 @@ __metadata: languageName: node linkType: hard -"@types/trusted-types@npm:*": - version: 2.0.3 - resolution: "@types/trusted-types@npm:2.0.3" - checksum: 10/4794804bc4a4a173d589841b6d26cf455ff5dc4f3e704e847de7d65d215f2e7043d8757e4741ce3a823af3f08260a8d04a1a6e9c5ec9b20b7b04586956a6b005 +"@types/trusted-types@npm:*, @types/trusted-types@npm:^2.0.7": + version: 2.0.7 + resolution: "@types/trusted-types@npm:2.0.7" + checksum: 10/8e4202766a65877efcf5d5a41b7dd458480b36195e580a3b1085ad21e948bc417d55d6f8af1fd2a7ad008015d4117d5fdfe432731157da3c68678487174e4ba3 languageName: node linkType: hard @@ -28454,9 +28454,14 @@ __metadata: linkType: hard "dompurify@npm:^3.0.0, dompurify@npm:^3.1.7": - version: 3.1.7 - resolution: "dompurify@npm:3.1.7" - checksum: 10/dc637a064306f83cf911caa267ffe1f973552047602020e3b6723c90f67962813edf8a65a0b62e8c9bc13fcd173a2691212a3719bc116226967f46bcd6181277 + version: 3.2.6 + resolution: "dompurify@npm:3.2.6" + dependencies: + "@types/trusted-types": "npm:^2.0.7" + dependenciesMeta: + "@types/trusted-types": + optional: true + checksum: 10/b91631ed0e4d17fae950ef53613cc009ed7e73adc43ac94a41dd52f35483f7538d13caebdafa7626e0da145fc8184e7ac7935f14f25b7e841b32fda777e40447 languageName: node linkType: hard From d821c01c5ecca6873c6c2a33e3e205dd9c27c4c0 Mon Sep 17 00:00:00 2001 From: Jackson Chen Date: Tue, 9 Sep 2025 17:31:19 -0400 Subject: [PATCH 147/233] refactor and fix dompurify tsc errors Signed-off-by: Jackson Chen --- .../transformers/html/hooks/attributes.ts | 32 +++++++++++++++ .../reader/transformers/html/hooks/iframes.ts | 7 +++- .../reader/transformers/html/hooks/index.ts | 2 + .../reader/transformers/html/hooks/links.ts | 8 ++-- .../transformers/html/hooks/metatags.ts | 41 +++++++++++++++++++ .../reader/transformers/html/transformer.ts | 30 ++++---------- .../src/reader/transformers/html/utils.ts | 19 +++++++++ 7 files changed, 113 insertions(+), 26 deletions(-) create mode 100644 plugins/techdocs/src/reader/transformers/html/hooks/attributes.ts create mode 100644 plugins/techdocs/src/reader/transformers/html/hooks/metatags.ts create mode 100644 plugins/techdocs/src/reader/transformers/html/utils.ts diff --git a/plugins/techdocs/src/reader/transformers/html/hooks/attributes.ts b/plugins/techdocs/src/reader/transformers/html/hooks/attributes.ts new file mode 100644 index 0000000000..ae0b4e2ce9 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/html/hooks/attributes.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { UponSanitizeAttributeHook } from 'dompurify'; + +/** + * Removes attributes that should only be present on meta tags from other elements. + * This ensures that http-equiv and content attributes are only allowed on meta tags + * where they are required for the redirect feature. + */ +export const removeRestrictedAttributes: UponSanitizeAttributeHook = ( + node, + data, +) => { + if (node.tagName !== 'META') { + if (data.attrName === 'http-equiv' || data.attrName === 'content') { + node.removeAttribute(data.attrName); + } + } +}; diff --git a/plugins/techdocs/src/reader/transformers/html/hooks/iframes.ts b/plugins/techdocs/src/reader/transformers/html/hooks/iframes.ts index 25259dbc43..23711b52b7 100644 --- a/plugins/techdocs/src/reader/transformers/html/hooks/iframes.ts +++ b/plugins/techdocs/src/reader/transformers/html/hooks/iframes.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { isElement } from '../utils'; + /** * Checks whether a node is iframe or not. * @param node - can be any element. @@ -42,9 +44,10 @@ const isSafe = (node: Element, hosts: string[]) => { * @param node - can be any element. * @param hosts - list of allowed hosts. */ -export const removeUnsafeIframes = (hosts: string[]) => (node: Element) => { +export const removeUnsafeIframes = (hosts: string[]) => (node: Node) => { + if (!isElement(node)) return; + if (isIframe(node) && !isSafe(node, hosts)) { node.remove(); } - return node; }; diff --git a/plugins/techdocs/src/reader/transformers/html/hooks/index.ts b/plugins/techdocs/src/reader/transformers/html/hooks/index.ts index a4356db2e9..ce237ec6d2 100644 --- a/plugins/techdocs/src/reader/transformers/html/hooks/index.ts +++ b/plugins/techdocs/src/reader/transformers/html/hooks/index.ts @@ -16,3 +16,5 @@ export { removeUnsafeLinks } from './links'; export { removeUnsafeIframes } from './iframes'; +export { removeUnsafeMetaTags } from './metatags'; +export { removeRestrictedAttributes } from './attributes'; diff --git a/plugins/techdocs/src/reader/transformers/html/hooks/links.ts b/plugins/techdocs/src/reader/transformers/html/hooks/links.ts index 38f775cfbe..eb2cbfc6de 100644 --- a/plugins/techdocs/src/reader/transformers/html/hooks/links.ts +++ b/plugins/techdocs/src/reader/transformers/html/hooks/links.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { isElement } from '../utils'; + const MKDOCS_CSS = /main\.[A-Fa-f0-9]{8}\.min\.css$/; const GOOGLE_FONTS = /^https:\/\/fonts\.googleapis\.com/; const GSTATIC_FONTS = /^https:\/\/fonts\.gstatic\.com/; @@ -41,11 +43,11 @@ const isSafe = (node: Element) => { /** * Function that removes unsafe link nodes. * @param node - can be any element. - * @param hosts - list of allowed hosts. */ -export const removeUnsafeLinks = (node: Element) => { +export const removeUnsafeLinks = (node: Node) => { + if (!isElement(node)) return; + if (isLink(node) && !isSafe(node)) { node.remove(); } - return node; }; diff --git a/plugins/techdocs/src/reader/transformers/html/hooks/metatags.ts b/plugins/techdocs/src/reader/transformers/html/hooks/metatags.ts new file mode 100644 index 0000000000..a207a5bca2 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/html/hooks/metatags.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { UponSanitizeElementHook } from 'dompurify'; +import { isElement } from '../utils'; + +/** + * Checks if a meta tag is a refresh redirect tag that should be allowed. + * These tags are required for the TechDocs redirect feature. + */ +const isAllowedMetaRefreshTag = (element: Element): boolean => { + const httpEquiv = element.getAttribute('http-equiv'); + const content = element.getAttribute('content'); + + return httpEquiv === 'refresh' && content?.includes('url=') === true; +}; + +/** + * Removes unsafe meta tags from the DOM while preserving allowed refresh redirect tags. + * Only meta tags used for page refreshing/redirects are allowed as they are required + * for the TechDocs redirect feature. + */ +export const removeUnsafeMetaTags: UponSanitizeElementHook = (node, data) => { + if (!isElement(node)) return; + + if (data.tagName === 'meta' && !isAllowedMetaRefreshTag(node)) { + node.parentNode?.removeChild(node); + } +}; diff --git a/plugins/techdocs/src/reader/transformers/html/transformer.ts b/plugins/techdocs/src/reader/transformers/html/transformer.ts index d4b31a34e5..6ebf53f26e 100644 --- a/plugins/techdocs/src/reader/transformers/html/transformer.ts +++ b/plugins/techdocs/src/reader/transformers/html/transformer.ts @@ -20,7 +20,12 @@ import { useCallback, useMemo } from 'react'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { Transformer } from '../transformer'; -import { removeUnsafeIframes, removeUnsafeLinks } from './hooks'; +import { + removeRestrictedAttributes, + removeUnsafeIframes, + removeUnsafeLinks, + removeUnsafeMetaTags, +} from './hooks'; /** * Returns html sanitizer configuration @@ -51,26 +56,9 @@ export const useSanitizerTransformer = (): Transformer => { DOMPurify.addHook('beforeSanitizeElements', removeUnsafeIframes(hosts)); } - // Only allow meta tags if they are used for refreshing the page. They are required for the redirect feature. - DOMPurify.addHook('uponSanitizeElement', (currNode, data) => { - if (data.tagName === 'meta') { - const isMetaRefreshTag = - currNode.getAttribute('http-equiv') === 'refresh' && - currNode.getAttribute('content')?.includes('url='); - if (!isMetaRefreshTag) { - currNode.parentNode?.removeChild(currNode); - } - } - }); + DOMPurify.addHook('uponSanitizeElement', removeUnsafeMetaTags); - // Only allow http-equiv and content attributes on meta tags. They are required for the redirect feature. - DOMPurify.addHook('uponSanitizeAttribute', (currNode, data) => { - if (currNode.tagName !== 'META') { - if (data.attrName === 'http-equiv' || data.attrName === 'content') { - currNode.removeAttribute(data.attrName); - } - } - }); + DOMPurify.addHook('uponSanitizeAttribute', removeRestrictedAttributes); const tagNameCheck = config?.getOptionalString( 'allowedCustomElementTagNameRegExp', @@ -122,7 +110,7 @@ export const useSanitizerTransformer = (): Transformer => { ? new RegExp(attributeNameCheck) : undefined, }, - }); + }) as Element; }, [config], ); diff --git a/plugins/techdocs/src/reader/transformers/html/utils.ts b/plugins/techdocs/src/reader/transformers/html/utils.ts new file mode 100644 index 0000000000..b97b63743c --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/html/utils.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const isElement = (node: Node): node is Element => { + return node.nodeType === Node.ELEMENT_NODE; +}; From 313cec7bed81039ea9473227c035f57396fa7a97 Mon Sep 17 00:00:00 2001 From: Jackson Chen Date: Tue, 9 Sep 2025 17:47:35 -0400 Subject: [PATCH 148/233] add changeset Signed-off-by: Jackson Chen --- .changeset/chilly-llamas-attend.md | 5 +++++ plugins/techdocs/package.json | 2 +- yarn.lock | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changeset/chilly-llamas-attend.md diff --git a/.changeset/chilly-llamas-attend.md b/.changeset/chilly-llamas-attend.md new file mode 100644 index 0000000000..0a0876ccac --- /dev/null +++ b/.changeset/chilly-llamas-attend.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Updated dependency `dompurify` to `^3.2.4`. diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index ca867db802..7777133862 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -81,7 +81,7 @@ "@material-ui/lab": "4.0.0-alpha.61", "@material-ui/styles": "^4.10.0", "@microsoft/fetch-event-source": "^2.0.1", - "dompurify": "^3.0.0", + "dompurify": "^3.2.4", "git-url-parse": "^15.0.0", "jss": "~10.10.0", "lodash": "^4.17.21", diff --git a/yarn.lock b/yarn.lock index 42cf3d4a3d..0e5078d824 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7289,7 +7289,7 @@ __metadata: "@testing-library/user-event": "npm:^14.0.0" "@types/dompurify": "npm:^3.0.0" "@types/react": "npm:^18.0.0" - dompurify: "npm:^3.0.0" + dompurify: "npm:^3.2.4" git-url-parse: "npm:^15.0.0" jss: "npm:~10.10.0" lodash: "npm:^4.17.21" @@ -28453,7 +28453,7 @@ __metadata: languageName: node linkType: hard -"dompurify@npm:^3.0.0, dompurify@npm:^3.1.7": +"dompurify@npm:^3.1.7, dompurify@npm:^3.2.4": version: 3.2.6 resolution: "dompurify@npm:3.2.6" dependencies: From 7bd14534b86fab32d5dfbf0f4d5fcc2b6e8c9557 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Date: Wed, 10 Sep 2025 11:10:39 +0530 Subject: [PATCH 149/233] Made the chages to the software catalog Link Signed-off-by: Aditya Kumar --- docs/frontend-system/building-plugins/01-index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/building-plugins/01-index.md b/docs/frontend-system/building-plugins/01-index.md index 03da20ea73..283c162fa7 100644 --- a/docs/frontend-system/building-plugins/01-index.md +++ b/docs/frontend-system/building-plugins/01-index.md @@ -185,7 +185,7 @@ export const examplePlugin = createFrontendPlugin({ ## Plugin specific extensions -There are many different plugins that you can extend with additional functionality through extensions. One such plugin is [the catalog plugin](https://backstage.io/docs/features/software-catalog/), one of the core features of Backstage. It lets you catalog the software in your organization, where each item in the catalog has its own page that can be populated with tools and information relating to that catalog entity. In this example we will explore how our plugin can provide such a tool to display on an entity page. +There are many different plugins that you can extend with additional functionality through extensions. One such plugin is [the catalog plugin](../../features/software-catalog/), one of the core features of Backstage. It lets you catalog the software in your organization, where each item in the catalog has its own page that can be populated with tools and information relating to that catalog entity. In this example we will explore how our plugin can provide such a tool to display on an entity page. ```tsx title="in src/plugin.ts - An example entity content extension" import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; From e72c879b09c344602f330545f8700e84a2c8c896 Mon Sep 17 00:00:00 2001 From: Aditya Kumar Date: Wed, 10 Sep 2025 12:16:48 +0530 Subject: [PATCH 150/233] Updated Note format Signed-off-by: Aditya Kumar --- plugins/catalog-backend/src/schema/openapi.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend/src/schema/openapi.yaml b/plugins/catalog-backend/src/schema/openapi.yaml index baaba44d48..e4282f1700 100644 --- a/plugins/catalog-backend/src/schema/openapi.yaml +++ b/plugins/catalog-backend/src/schema/openapi.yaml @@ -6,8 +6,9 @@ info: The API surface consists of a few distinct groups of functionality. Each has a dedicated section below. - > **Note:** This page only describes some of the most commonly used parts of the - > API, and is a work in progress. + :::note Note + This page only describes some of the most commonly used parts of the API, and is a work in progress. + ::: All of the URL paths in this article are assumed to be on top of some base URL pointing at your catalog installation. For example, if the path given in a From fffd4347fa133ec286bb650e6b01d5fa76638056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Erik=20Bergstr=C3=B6m?= Date: Tue, 9 Sep 2025 19:43:23 +0200 Subject: [PATCH 151/233] fix(module-federation): disallow imported fallback modules in mf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Carl-Erik Bergström --- .changeset/angry-heads-design.md | 5 +++++ packages/cli/src/modules/build/lib/bundler/config.ts | 8 ++++++++ 2 files changed, 13 insertions(+) create mode 100644 .changeset/angry-heads-design.md diff --git a/.changeset/angry-heads-design.md b/.changeset/angry-heads-design.md new file mode 100644 index 0000000000..0b4a866fb1 --- /dev/null +++ b/.changeset/angry-heads-design.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Disallow import fallback of critical shared dependencies in module federation. diff --git a/packages/cli/src/modules/build/lib/bundler/config.ts b/packages/cli/src/modules/build/lib/bundler/config.ts index c4011ede48..bf78d04b4a 100644 --- a/packages/cli/src/modules/build/lib/bundler/config.ts +++ b/packages/cli/src/modules/build/lib/bundler/config.ts @@ -264,24 +264,30 @@ export async function createConfig( singleton: true, requiredVersion: '*', eager: !isRemote, + import: false, }, 'react-dom': { singleton: true, requiredVersion: '*', eager: !isRemote, + import: false, }, // React Router 'react-router': { singleton: true, requiredVersion: '*', eager: !isRemote, + import: false, }, 'react-router-dom': { singleton: true, requiredVersion: '*', eager: !isRemote, + import: false, }, // MUI v4 + // not setting import: false for MUI packages as this + // will break once Backstage moves to BUI '@material-ui/core/styles': { singleton: true, requiredVersion: '*', @@ -293,6 +299,8 @@ export async function createConfig( eager: !isRemote, }, // MUI v5 + // not setting import: false for MUI packages as this + // will break once Backstage moves to BUI '@mui/material/styles/': { singleton: true, requiredVersion: '*', From 184d03a4cfaab326c09ac66a6c2e1dcb03cb5cf1 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 10 Sep 2025 10:34:24 +0200 Subject: [PATCH 152/233] clean up api-docs community-plugins Signed-off-by: Vincenzo Scamporlino --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7aed18ab74..5534155276 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,9 @@ "scripts": { "build-storybook": "storybook build --output-dir dist-storybook", "build:all": "backstage-cli repo build --all", - "build:api-docs": "LANG=en_EN yarn build:api-reports --docs --exclude 'plugins/@(adr|adr-backend|adr-common|airbrake|airbrake-backend|allure|analytics-module-ga|analytics-module-ga4|analytics-module-newrelic-browser|apache-airflow|api-docs|api-docs-module-protoc-gen-doc|apollo-explorer|app-visualizer|azure-devops|azure-devops-backend|azure-devops-common|azure-sites|azure-sites-backend|azure-sites-common|badges|badges-backend|bazaar|bazaar-backend|bitbucket-cloud-common|bitrise|catalog-graph|catalog-graphql|catalog-import|catalog-unprocessed-entities|cicd-statistics|cicd-statistics-module-gitlab|circleci|cloudbuild|code-climate|code-coverage|code-coverage-backend|codescene|config-schema|cost-insights|cost-insights-common|dynatrace|entity-feedback|entity-feedback-backend|entity-feedback-common|entity-validation|example-todo-list|example-todo-list-backend|example-todo-list-common|firehydrant|fossa|gcalendar|gcp-projects|git-release-manager|github-actions|github-deployments|github-issues|github-pull-requests-board|gitops-profiles|gocd|graphiql|graphql-backend|graphql-voyager|ilert|jenkins|jenkins-backend|jenkins-common|kafka|kafka-backend|lighthouse|lighthouse-backend|lighthouse-common|linguist|linguist-backend|linguist-common|microsoft-calendar|newrelic|newrelic-dashboard|nomad|nomad-backend|octopus-deploy|opencost|pagerduty|periskop|periskop-backend|playlist|playlist-backend|playlist-common|proxy-backend|puppetdb|rollbar|rollbar-backend|sentry|shortcuts|splunk-on-call|stack-overflow|stack-overflow-backend|stackstorm|tech-radar|tech-radar-2|todo|todo-backend|xcmetrics)'", + "build:api-docs": "LANG=en_EN yarn build:api-reports --docs --exclude 'plugins/@(api-docs|api-docs-module-protoc-gen-doc|app-visualizer|catalog-graph|catalog-import|catalog-unprocessed-entities|config-schema|example-todo-list|example-todo-list-backend)'", "build:api-reports": "yarn build:api-reports:only --tsc", - "build:api-reports:only": "LANG=en_US.UTF-8 NODE_OPTIONS=--max-old-space-size=8192 backstage-repo-tools api-reports --sql-reports --allow-warnings 'packages/backend-app-api,packages/core-components,plugins/+(catalog|catalog-import|git-release-manager|jenkins|kubernetes)' -o ae-undocumented,ae-wrong-input-file-type --validate-release-tags", + "build:api-reports:only": "LANG=en_US.UTF-8 NODE_OPTIONS=--max-old-space-size=8192 backstage-repo-tools api-reports --sql-reports --allow-warnings 'packages/backend-app-api,packages/core-components,plugins/+(catalog|catalog-import|kubernetes)' -o ae-undocumented,ae-wrong-input-file-type --validate-release-tags", "build:backend": "yarn workspace example-backend build", "build:knip-reports": "backstage-repo-tools knip-reports", "build:plugins-report": "node ./scripts/build-plugins-report", From d03010ec9be71f0f756cfbc860de3120ff87f0e7 Mon Sep 17 00:00:00 2001 From: Hayato Kihara <14058454+gumimin@users.noreply.github.com> Date: Thu, 11 Sep 2025 03:23:23 +0900 Subject: [PATCH 153/233] docs: Add missing TOC entries to TechDocs FAQ (#30900) Signed-off-by: Hayato Kihara --- .github/vale/config/vocabularies/Backstage/accept.txt | 1 + docs/features/techdocs/FAQ.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index ec43e1fbbc..516eec516f 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -146,6 +146,7 @@ Expedia facto failover Fargate +faqs featureful Figma firehydrant diff --git a/docs/features/techdocs/FAQ.md b/docs/features/techdocs/FAQ.md index 24777e5763..9c78e3e123 100644 --- a/docs/features/techdocs/FAQ.md +++ b/docs/features/techdocs/FAQ.md @@ -12,6 +12,8 @@ This page answers frequently asked questions about [TechDocs](README.md). - [What static site generator is TechDocs using?](#what-static-site-generator-is-techdocs-using) - [What is the mkdocs-techdocs-core plugin?](#what-is-the-mkdocs-techdocs-core-plugin) - [Does TechDocs support file formats other than Markdown (e.g. RST, AsciiDoc)?](#does-techdocs-support-file-formats-other-than-markdown-eg-rst-asciidoc-) +- [What should be the value of `backstage.io/techdocs-ref` when using external build and storage?](#what-should-be-the-value-of-backstageiotechdocs-ref-when-using-external-build-and-storage) +- [Is it possible for users to suggest changes or provide feedback on a TechDocs page?](#is-it-possible-for-users-to-suggest-changes-or-provide-feedback-on-a-techdocs-page) #### What static site generator is TechDocs using? From 8d18d23e347ef24ca30c26fe4fb4efe3e870227f Mon Sep 17 00:00:00 2001 From: Luna Stadler Date: Wed, 10 Sep 2025 20:34:58 +0200 Subject: [PATCH 154/233] Improve TechDocs page titles (especially for nested pages) (#31054) * Replace underscores in techdocs titles Signed-off-by: Luna Stadler * Make techdocs titles similar to component titles The pattern for components is entity name, page/tab and then app title. This ordering makes it easier to distinguish tabs at a glance. Signed-off-by: Luna Stadler * Abbreviate nested pages in techdocs A deeply nested page like `/really/very/deeply/nested/page`, will now become "Really | ... | Nested | Page". This should preserve some of the context and support docs whith deeply nested pages. Signed-off-by: Luna Stadler * Add changeset for TechDocs page title improvements Signed-off-by: Luna Stadler * Display the full title based on all parts of the path Signed-off-by: Luna Stadler --------- Signed-off-by: Luna Stadler --- .changeset/clear-houses-wonder.md | 5 +++ .../TechDocsReaderPageHeader.test.tsx | 33 ++++++++++++++++--- .../TechDocsReaderPageHeader.tsx | 5 ++- 3 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 .changeset/clear-houses-wonder.md diff --git a/.changeset/clear-houses-wonder.md b/.changeset/clear-houses-wonder.md new file mode 100644 index 0000000000..46ada62fbe --- /dev/null +++ b/.changeset/clear-houses-wonder.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +TechDocs page titles have been improved, especially for deeply nested pages. diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx index 6f6ee81c8b..3c10b26f7f 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx @@ -52,13 +52,11 @@ const mockTechDocsMetadata = { site_description: 'test-site-desc', }; -const mockUseParams = jest.fn(); -mockUseParams.mockReturnValue({ '*': 'foo/bar/baz/' }); - +let useParamsPath = '/'; jest.mock('react-router-dom', () => { return { ...(jest.requireActual('react-router-dom') as any), - useParams: () => mockUseParams(), + useParams: () => ({ '*': useParamsPath }), }; }); @@ -189,6 +187,7 @@ describe('', () => { getEntityMetadata.mockResolvedValue(mockEntityMetadata); getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata); + useParamsPath = 'foo/bar/baz/'; await renderInTestApp( @@ -203,7 +202,31 @@ describe('', () => { await waitFor(() => { expect(document.title).toEqual( - 'Backstage | Test Entity | Foo | Bar | Baz', + 'Test Entity | Foo | Bar | Baz | Backstage', + ); + }); + }); + + it('The header title is abbreviated if path is too long', async () => { + getEntityMetadata.mockResolvedValue(mockEntityMetadata); + getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata); + + useParamsPath = 'foo/bar/baz/qux/quux/'; + await renderInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name/*': entityRouteRef, + '/docs': rootRouteRef, + }, + }, + ); + + await waitFor(() => { + expect(document.title).toEqual( + 'Test Entity | Foo | Bar | Baz | Qux | Quux | Backstage', ); }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx index bafcdd270c..6328becc73 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx @@ -172,17 +172,16 @@ export const TechDocsReaderPageHeader = ( const removeTrailingSlash = (str: string) => str.replace(/\/$/, ''); const normalizeAndSpace = (str: string) => - str.replace(/-/g, ' ').split(' ').map(capitalize).join(' '); + str.replace(/[-_]/g, ' ').split(' ').map(capitalize).join(' '); let techdocsTabTitleItems: string[] = []; if (path !== '') techdocsTabTitleItems = removeTrailingSlash(path) .split('/') - .slice(0, 3) .map(normalizeAndSpace); - const tabTitleItems = [appTitle, entityDisplayName, ...techdocsTabTitleItems]; + const tabTitleItems = [entityDisplayName, ...techdocsTabTitleItems, appTitle]; const tabTitle = tabTitleItems.join(' | '); return ( From 79ff318518479ed193d50f37fddfbfe65070ac4a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Sep 2025 00:53:22 +0200 Subject: [PATCH 155/233] catalog-react: removed entity card type deprecation warning Signed-off-by: Patrik Oldsberg --- .changeset/tough-cars-cry.md | 5 +++++ .../src/alpha/blueprints/EntityCardBlueprint.ts | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 .changeset/tough-cars-cry.md diff --git a/.changeset/tough-cars-cry.md b/.changeset/tough-cars-cry.md new file mode 100644 index 0000000000..9235553da7 --- /dev/null +++ b/.changeset/tough-cars-cry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Removed the deprecation warning when not passing an explicit type to `EntityCardBlueprint`. Omitting the type is now intended, allowing the layout to pick the default type instead, typically `content`. diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.ts b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.ts index b2f64c19e6..fed6ced70f 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.ts +++ b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.ts @@ -75,11 +75,6 @@ export const EntityCardBlueprint = createExtensionBlueprint({ const finalType = config.type ?? type; if (finalType) { yield entityCardTypeDataRef(finalType); - } else { - // eslint-disable-next-line no-console - console.warn( - `DEPRECATION WARNING: Not providing type for entity cards is deprecated. Missing from '${node.spec.id}'`, - ); } }, }); From 56897d717e9445eb7aacfceba3f17bfe782f4809 Mon Sep 17 00:00:00 2001 From: Lee Standen Date: Wed, 10 Sep 2025 15:57:16 -0700 Subject: [PATCH 156/233] Fixes issue with organization name case sensitivity when using allowedInstallationOwners Signed-off-by: Lee Standen --- .changeset/thin-phones-press.md | 5 +++ ...eInstanceGithubCredentialsProvider.test.ts | 36 +++++++++++++++++++ ...SingleInstanceGithubCredentialsProvider.ts | 10 ++++-- 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 .changeset/thin-phones-press.md diff --git a/.changeset/thin-phones-press.md b/.changeset/thin-phones-press.md new file mode 100644 index 0000000000..8b3f985772 --- /dev/null +++ b/.changeset/thin-phones-press.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Fixes issue with Github credentials provider which fails to match organization name if using allowedInstallationOwners diff --git a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts index ec07d1b465..0468e96e14 100644 --- a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts +++ b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.test.ts @@ -231,6 +231,42 @@ describe('SingleInstanceGithubCredentialsProvider tests', () => { expect(token).toEqual(undefined); }); + it('should not fail to issue tokens for an organization when there is a case mismatch in the organization name', async () => { + octokit.apps.listInstallations.mockResolvedValue({ + headers: { + etag: '123', + }, + data: [ + { + id: 1, + repository_selection: 'selected', + account: { + login: 'backstage', + }, + }, + ], + } as RestEndpointMethodTypes['apps']['listInstallations']['response']); + + octokit.apps.createInstallationAccessToken.mockResolvedValueOnce({ + data: { + expires_at: DateTime.local().plus({ hours: 1 }).toString(), + token: 'secret_token', + repository_selection: 'selected', + }, + } as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']); + + octokit.apps.listReposAccessibleToInstallation.mockReturnValue({ + data: [{ name: 'some-repo' }], + } as unknown as RestEndpointMethodTypes['apps']['listReposAccessibleToInstallation']['response']); + + const { token, headers } = await github.getCredentials({ + url: 'https://github.com/Backstage', + }); + const expectedToken = 'secret_token'; + expect(headers).toEqual({ Authorization: `Bearer ${expectedToken}` }); + expect(token).toEqual('secret_token'); + }); + it('should not fail to issue tokens for an organization when the app is installed for a single repo', async () => { octokit.apps.listInstallations.mockResolvedValue({ headers: { diff --git a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts index fd3c02486f..eaaabe8b0f 100644 --- a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts +++ b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts @@ -102,7 +102,9 @@ class GithubAppManager { private readonly allowedInstallationOwners: string[] | undefined; // undefined allows all installations constructor(config: GithubAppConfig, baseUrl?: string) { - this.allowedInstallationOwners = config.allowedInstallationOwners; + this.allowedInstallationOwners = config.allowedInstallationOwners?.map( + owner => owner.toLocaleLowerCase('en-US'), + ); this.baseUrl = baseUrl; this.baseAuthConfig = { appId: config.appId, @@ -121,7 +123,11 @@ class GithubAppManager { repo?: string, ): Promise<{ accessToken: string | undefined }> { if (this.allowedInstallationOwners) { - if (!this.allowedInstallationOwners?.includes(owner)) { + if ( + !this.allowedInstallationOwners?.includes( + owner.toLocaleLowerCase('en-US'), + ) + ) { return { accessToken: undefined }; // An empty token allows anonymous access to public repos } } From 8f9d0f947cdf710c629ae647c46c2ce63ece8ebd Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Thu, 11 Sep 2025 08:54:18 +0300 Subject: [PATCH 157/233] feat(microsite): add search for plugins Signed-off-by: Hellgren Heikki --- .../pluginsSearch/pluginsSearch.tsx | 19 +++++ microsite/src/pages/plugins/index.tsx | 84 ++++++++++++------- .../src/pages/plugins/plugins.module.scss | 14 ++++ 3 files changed, 88 insertions(+), 29 deletions(-) create mode 100644 microsite/src/components/pluginsSearch/pluginsSearch.tsx diff --git a/microsite/src/components/pluginsSearch/pluginsSearch.tsx b/microsite/src/components/pluginsSearch/pluginsSearch.tsx new file mode 100644 index 0000000000..68f9a2831a --- /dev/null +++ b/microsite/src/components/pluginsSearch/pluginsSearch.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +type Props = { + searchTerm: string; + onSearchTermChange: (newTerm: string) => void; +}; + +export const PluginsSearch = (props: Props) => { + const { searchTerm, onSearchTermChange } = props; + return ( + onSearchTermChange((e.target as HTMLInputElement).value)} + value={searchTerm} + placeholder="Search plugins..." + className="DocSearch-Input search" + /> + ); +}; diff --git a/microsite/src/pages/plugins/index.tsx b/microsite/src/pages/plugins/index.tsx index 2abdbabf4b..1a87f9c0af 100644 --- a/microsite/src/pages/plugins/index.tsx +++ b/microsite/src/pages/plugins/index.tsx @@ -5,10 +5,11 @@ import { truncateDescription } from '@site/src/util/truncateDescription'; import { ChipCategory } from '@site/src/util/types'; import Layout from '@theme/Layout'; import clsx from 'clsx'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { IPluginData, PluginCard } from './_pluginCard'; import pluginsStyles from './plugins.module.scss'; +import { PluginsSearch } from '@site/src/components/pluginsSearch/pluginsSearch'; interface IPluginsList { corePlugins: IPluginData[]; @@ -55,6 +56,7 @@ const Plugins = () => { const [selectedCategories, setSelectedCategories] = useState([]); const [showCoreFeatures, setShowCoreFeatures] = useState(true); const [showOtherPlugins, setShowOtherPlugins] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); const handleChipClick = (categoryName: string) => { const isSelected = @@ -97,6 +99,34 @@ const Plugins = () => { } }; + const matchesSearch = (pluginData: IPluginData, term: string) => { + if (!term) return true; + const lowerTerm = term.toLowerCase(); + return ( + pluginData.title.toLowerCase().includes(lowerTerm) || + pluginData.description.toLowerCase().includes(lowerTerm) || + pluginData.category.toLowerCase().includes(lowerTerm) || + (pluginData.author && pluginData.author.toLowerCase().includes(lowerTerm)) + ); + }; + + const matchesCategory = (pluginData: IPluginData, categories: string[]) => { + if (categories.length === 0) return true; + return categories.includes(pluginData.category); + }; + + const corePlugins = useMemo(() => { + return plugins.corePlugins + .filter(pluginData => matchesCategory(pluginData, selectedCategories)) + .filter(pluginData => matchesSearch(pluginData, searchTerm)); + }, [selectedCategories, searchTerm]); + + const otherPlugins = useMemo(() => { + return plugins.otherPlugins + .filter(pluginData => matchesCategory(pluginData, selectedCategories)) + .filter(pluginData => matchesSearch(pluginData, searchTerm)); + }, [selectedCategories, searchTerm]); + return (
{ categories={categories} handleChipClick={handleChipClick} /> +
- {showCoreFeatures && ( + {corePlugins.length === 0 && otherPlugins.length === 0 && ( +
+

No plugins found

+

+ We couldn't find any plugins matching your criteria. Please try + adjusting your search or filter settings. +

+
+ )} + + {showCoreFeatures && corePlugins.length > 0 && (
-

Core Features

+

Core Features ({corePlugins.length})

- {plugins.corePlugins - .filter( - pluginData => - !selectedCategories.length || - selectedCategories.includes(pluginData.category), - ) - .map(pluginData => ( - - ))} + {corePlugins.map(pluginData => ( + + ))}
)} - {showOtherPlugins && ( + {showOtherPlugins && otherPlugins.length > 0 && (
-

All Plugins

+

All Plugins ({otherPlugins.length})

Friendly reminder: While we love the variety and contributions of our open source plugins, they haven't been fully vetted by the @@ -158,18 +193,9 @@ const Plugins = () => { your due diligence before installing. Happy exploring!

- {plugins.otherPlugins - .filter( - pluginData => - !selectedCategories.length || - selectedCategories.includes(pluginData.category), - ) - .map(pluginData => ( - - ))} + {otherPlugins.map(pluginData => ( + + ))}
)} diff --git a/microsite/src/pages/plugins/plugins.module.scss b/microsite/src/pages/plugins/plugins.module.scss index fb17e90625..a17e4bc5c0 100644 --- a/microsite/src/pages/plugins/plugins.module.scss +++ b/microsite/src/pages/plugins/plugins.module.scss @@ -76,6 +76,20 @@ height: 1rem; } + :global(.search) { + float: right; + margin-bottom: 1rem; + margin-right: 1rem; + font-size: calc(0.875rem * var(--ifm-button-size-multiplier)); + font-weight: var(--ifm-button-font-weight); + background-color: transparent; + border: var(--ifm-button-border-width) solid var(--ifm-color-primary); + border-radius: var(--ifm-button-border-radius); + color: var(--ifm-color-primary); + width: 220px; + height: 2.2rem; + } + :global(.dropdown) { float: right; margin-bottom: 1rem; From cb9e2d11552773a0b323575bbd94daed400c86a8 Mon Sep 17 00:00:00 2001 From: Joshua Rogers Date: Thu, 11 Sep 2025 08:26:59 +0200 Subject: [PATCH 158/233] Revert "techdocs: Disallow javascript URLs" This reverts commit 067fdcd0a5b6b63a689f9fe4fa322c690ed263fb. Signed-off-by: Joshua Rogers --- .changeset/great-adults-stare.md | 5 ---- .../transformers/rewriteDocLinks.test.ts | 27 ------------------- .../reader/transformers/rewriteDocLinks.ts | 11 -------- 3 files changed, 43 deletions(-) delete mode 100644 .changeset/great-adults-stare.md diff --git a/.changeset/great-adults-stare.md b/.changeset/great-adults-stare.md deleted file mode 100644 index 05d196ac62..0000000000 --- a/.changeset/great-adults-stare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': minor ---- - -Ensure that techdocs rewritten URLs do not contain potentially dangerous javascript: URLs. diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts index 864129164c..66fc5f29c1 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts @@ -71,33 +71,6 @@ describe('rewriteDocLinks', () => { expect(getSample(shadowDom, 'a', 'href')).toEqual([]); expect(shadowDom.innerHTML).toContain(expectedText); }); - - it('should rewrite javascript hrefs as text', async () => { - const samples: Array<[string, string]> = [ - // eslint-disable-next-line no-script-url - ['javascript:alert(1)', 'JS 1'], - [' javascript:alert(2)', 'JS 2 (leading space)'], - ['\n\tjavascript:alert(3)', 'JS 3 (whitespace)'], - // eslint-disable-next-line no-script-url - ['JaVaScRiPt:alert(4)', 'JS 4 (mixed case)'], - ['javascript:alert(5)', 'JS 5 (entity-encoded colon)'], - ]; - - const html = samples - .map(([href, text]) => `${text}`) - .join('\n'); - - const shadowDom = await createTestShadowDom(html, { - preTransformers: [rewriteDocLinks()], - postTransformers: [], - }); - - // There should be no
tags, but the link text should remain. - expect(getSample(shadowDom, 'a', 'href')).toEqual([]); - for (const [, text] of samples) { - expect(shadowDom.innerHTML).toContain(text); - } - }); }); describe('normalizeUrl', () => { diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts index a1005fe1d6..e43dda1815 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts @@ -16,11 +16,6 @@ import type { Transformer } from './transformer'; -// See https://github.com/facebook/react/blob/f0cf832e1d0c8544c36aa8b310960885a11a847c/packages/react-dom-bindings/src/shared/sanitizeURL.js -const scriptProtocolPattern = - // eslint-disable-next-line no-control-regex - /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i; - export const rewriteDocLinks = (): Transformer => { return dom => { const updateDom = ( @@ -38,12 +33,6 @@ export const rewriteDocLinks = (): Transformer => { } try { - if (scriptProtocolPattern.test(elemAttribute)) { - throw new TypeError( - `Invalid location ref '${elemAttribute}', target is a javascript: URL`, - ); - } - const normalizedWindowLocation = normalizeUrl( window.location.href, ); From b87e54c355dac759cb006c5fd6a34a47b051a196 Mon Sep 17 00:00:00 2001 From: Joshua Rogers Date: Thu, 11 Sep 2025 08:42:51 +0200 Subject: [PATCH 159/233] tests(techdocs): add transformer tests for sanitizing javascript: hrefs Signed-off-by: Joshua Rogers --- .../html/transformer.sanitizer.test.tsx | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/plugins/techdocs/src/reader/transformers/html/transformer.sanitizer.test.tsx b/plugins/techdocs/src/reader/transformers/html/transformer.sanitizer.test.tsx index 39d3d229aa..ed02f86060 100644 --- a/plugins/techdocs/src/reader/transformers/html/transformer.sanitizer.test.tsx +++ b/plugins/techdocs/src/reader/transformers/html/transformer.sanitizer.test.tsx @@ -143,4 +143,35 @@ describe('Transformers > Html > Sanitizer Custom Elements', () => { expect(elements).toHaveLength(1); expect(elements[0].hasAttribute('dominant-baseline')).toBe(true); }); + + it('removes javascript: hrefs while preserving link text', async () => { + const { result } = renderHook(() => useSanitizerTransformer(), { wrapper }); + const dirtyDom = document.createElement('html'); + const dirtyHTML = ` + + + JS 1 + JS 2 (leading space) + JS 3 (whitespace) + + JS 4 (mixed case) + JS 5 (entity-encoded colon) + `; + dirtyDom.innerHTML = dirtyHTML; + const clearDom = await result.current(dirtyDom); // calling html transformer + const elements = Array.from( + clearDom.querySelectorAll('body > a'), + ); + expect(elements).toHaveLength(5); + for (const el of elements) { + // DOMPurify strips the dangerous href attribute + expect(el.getAttribute('href')).toBeNull(); + } + // link text remains + expect(clearDom.textContent).toContain('JS 1'); + expect(clearDom.textContent).toContain('JS 2 (leading space)'); + expect(clearDom.textContent).toContain('JS 3 (whitespace)'); + expect(clearDom.textContent).toContain('JS 4 (mixed case)'); + expect(clearDom.textContent).toContain('JS 5 (entity-encoded colon)'); + }); }); From 8c51c700202c573e6ce50b2b0e26c0295babb22e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 11 Sep 2025 13:16:16 +0200 Subject: [PATCH 160/233] fix a non-working example template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../sample-templates/notifications-demo/template.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml b/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml index abd18b1d42..56a7e7f5c6 100644 --- a/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/notifications-demo/template.yaml @@ -32,7 +32,7 @@ spec: title: Title type: string description: Notification title - description: + info: title: Description type: string description: Notification longer description @@ -67,7 +67,7 @@ spec: recipients: ${{ parameters.recipients }} entityRefs: ${{ parameters.entityRefs }} title: ${{ parameters.title }} - description: ${{ parameters.description }} + info: ${{ parameters.info }} link: ${{ parameters.link }} severity: ${{ parameters.severity }} topic: ${{ parameters.topic }} From 4815b120f8d45ae0741419ef227d79e6c8893046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 11 Sep 2025 13:49:15 +0200 Subject: [PATCH 161/233] fix notifications rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/cold-garlics-care.md | 5 +++++ plugins/notifications/src/alpha.tsx | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 .changeset/cold-garlics-care.md diff --git a/.changeset/cold-garlics-care.md b/.changeset/cold-garlics-care.md new file mode 100644 index 0000000000..960ebe6bd4 --- /dev/null +++ b/.changeset/cold-garlics-care.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications': patch +--- + +Fixed missing app context when rendering the notifications view diff --git a/plugins/notifications/src/alpha.tsx b/plugins/notifications/src/alpha.tsx index 6d32150c35..ca4be58209 100644 --- a/plugins/notifications/src/alpha.tsx +++ b/plugins/notifications/src/alpha.tsx @@ -23,6 +23,7 @@ import { } from '@backstage/frontend-plugin-api'; import { rootRouteRef } from './routes'; import { + compatWrapper, convertLegacyRouteRef, convertLegacyRouteRefs, } from '@backstage/core-compat-api'; @@ -33,9 +34,9 @@ const page = PageBlueprint.make({ path: '/notifications', routeRef: convertLegacyRouteRef(rootRouteRef), loader: () => - import('./components/NotificationsPage').then(m => ( - - )), + import('./components/NotificationsPage').then(m => + compatWrapper(), + ), }, }); From 987cd8a6f564e68ff748ca3d49cc530a44db438f Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Thu, 11 Sep 2025 13:31:41 +0200 Subject: [PATCH 162/233] chore: make the catalog:get-catalog-entity action readOnly Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- .../src/actions/createGetCatalogEntityAction.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/catalog-backend/src/actions/createGetCatalogEntityAction.ts b/plugins/catalog-backend/src/actions/createGetCatalogEntityAction.ts index a1c7cefe78..b9f98291fd 100644 --- a/plugins/catalog-backend/src/actions/createGetCatalogEntityAction.ts +++ b/plugins/catalog-backend/src/actions/createGetCatalogEntityAction.ts @@ -28,6 +28,11 @@ export const createGetCatalogEntityAction = ({ actionsRegistry.register({ name: 'get-catalog-entity', title: 'Get Catalog Entity', + attributes: { + destructive: false, + readOnly: true, + idempotent: true, + }, description: ` This allows you to get a single entity from the software catalog. Each entity in the software catalog has a unique name, kind, and namespace. The default namespace is "default". From 2c0894a64b0d73a4f973d92cf482bca05c8f9d30 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Thu, 11 Sep 2025 17:10:30 +0100 Subject: [PATCH 163/233] bui: fix TextField story Signed-off-by: MT Lewis --- packages/ui/src/components/TextField/TextField.stories.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/components/TextField/TextField.stories.tsx b/packages/ui/src/components/TextField/TextField.stories.tsx index 57cc2b2909..eb64e95514 100644 --- a/packages/ui/src/components/TextField/TextField.stories.tsx +++ b/packages/ui/src/components/TextField/TextField.stories.tsx @@ -113,6 +113,7 @@ export const DisabledWithIcon: Story = { ...WithIcon.args, isDisabled: true, }, + render: WithIcon.render, }; export const ShowError: Story = { From e934a2777571f7c04f01f1c78ca3372971f8a031 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 12 Sep 2025 08:58:17 +0200 Subject: [PATCH 164/233] chore: added the ability to pull actions from actions registry Signed-off-by: benjdlambert --- .changeset/dark-teams-give.md | 5 ++ .../src/ScaffolderPlugin.ts | 4 ++ .../scaffolder-backend/src/service/router.ts | 46 ++++++++++++++++++- 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 .changeset/dark-teams-give.md diff --git a/.changeset/dark-teams-give.md b/.changeset/dark-teams-give.md new file mode 100644 index 0000000000..692f015618 --- /dev/null +++ b/.changeset/dark-teams-give.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Updating `catalog:get-catalog-entity` action to be `readOnly` and non destructive diff --git a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts index 7ef3abcbe3..a00da9e524 100644 --- a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts +++ b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts @@ -56,6 +56,7 @@ import { convertFiltersToRecord, convertGlobalsToRecord, } from './util/templating'; +import { actionsServiceRef } from '@backstage/backend-plugin-api/alpha'; /** * Scaffolder plugin @@ -139,6 +140,7 @@ export const scaffolderPlugin = createBackendPlugin({ auditor: coreServices.auditor, catalog: catalogServiceRef, events: eventsServiceRef, + actionsRegistry: actionsServiceRef, }, async init({ logger, @@ -153,6 +155,7 @@ export const scaffolderPlugin = createBackendPlugin({ permissions, events, auditor, + actionsRegistry, }) { const log = loggerToWinstonLogger(logger); const integrations = ScmIntegrations.fromConfig(config); @@ -222,6 +225,7 @@ export const scaffolderPlugin = createBackendPlugin({ additionalWorkspaceProviders, events, auditor, + actionsRegistry, }); httpRouter.use(router); }, diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 8296a6003f..2c9df0c8c4 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -26,6 +26,7 @@ import { resolveSafeChildPath, SchedulerService, } from '@backstage/backend-plugin-api'; +import { Schema } from 'jsonschema'; import { CompoundEntityRef, Entity, @@ -131,6 +132,8 @@ import { } from './rules'; import { TaskFilters } from '@backstage/plugin-scaffolder-node'; +import { ActionsService } from '@backstage/backend-plugin-api/alpha'; +import { isObject } from 'lodash'; /** * RouterOptions @@ -163,6 +166,7 @@ export interface RouterOptions { events?: EventsService; auditor?: AuditorService; autocompleteHandlers?: Record; + actionsRegistry: ActionsService; } function isSupportedTemplate(entity: TemplateEntityV1beta3) { @@ -198,7 +202,7 @@ export async function createRouter( config, database, catalog, - actions, + actions = [], scheduler, additionalTemplateFilters, additionalTemplateGlobals, @@ -210,6 +214,7 @@ export async function createRouter( auth, httpAuth, auditor, + actionsRegistry, } = options; const concurrentTasksLimit = @@ -301,7 +306,44 @@ export async function createRouter( workers.push(worker); } - actions?.forEach(action => actionRegistry.register(action)); + // TODO(blam): it's a little unfortunate that you have to restart the scaffolder + // backend in order to pick these up. We should really just make `ActionsRegistry.get()` async + // and then we can move this logic into the there instead. + const { actions: distributedActions } = await actionsRegistry.list({ + credentials: await auth.getOwnServiceCredentials(), + }); + + for (const action of actions) { + actionRegistry.register(action); + } + + for (const action of distributedActions) { + actionRegistry.register({ + id: action.id, + description: action.description, + examples: [], + supportsDryRun: + action.attributes?.readOnly === true && + action.attributes?.destructive === false, + handler: async ctx => { + const { output } = await actionsRegistry.invoke({ + id: action.id, + input: ctx.input, + credentials: await ctx.getInitiatorCredentials(), + }); + + if (isObject(output)) { + for (const [key, value] of Object.entries(output)) { + ctx.output(key as keyof typeof output, value); + } + } + }, + schema: { + input: action.schema.input as Schema, + output: action.schema.output as Schema, + }, + }); + } const launchWorkers = () => workers.forEach(worker => worker.start()); From e0db9b8534c968daa98d2c8fbb9bfa028545e263 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Thu, 11 Sep 2025 19:15:19 +0200 Subject: [PATCH 165/233] fix(cli): identify custom patterns that extend the default pattern If a custom pattern is used, `backstage.json` will not be updated. However, if the custom pattern extends the default pattern, `backstage.json` will be updated, too. Example: - `@backstage/*` (default) - `@{backstage,backstage-community}/*` - `@{extra1,backstage,extra2}/*` Fixes: #28839 Signed-off-by: Patrick Jungermann --- .changeset/fast-corners-roll.md | 11 +++++++++++ .../modules/migrate/commands/versions/bump.test.ts | 3 ++- .../src/modules/migrate/commands/versions/bump.ts | 13 +++++++++++-- 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 .changeset/fast-corners-roll.md diff --git a/.changeset/fast-corners-roll.md b/.changeset/fast-corners-roll.md new file mode 100644 index 0000000000..f883774aef --- /dev/null +++ b/.changeset/fast-corners-roll.md @@ -0,0 +1,11 @@ +--- +'@backstage/cli': patch +--- + +Modify the `backstage.json` also for custom patterns if it extends the default pattern. + +Examples: + +- `@backstage/*` (default pattern) +- `@{backstage,backstage-community}/*` +- `@{extra1,backstage,extra2}/*` diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts index a309ddb192..1fb28d0289 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts @@ -772,6 +772,7 @@ describe('bump', () => { res( ctx.status(200), ctx.json({ + releaseVersion: '1.0.0', packages: [], }), ), @@ -797,7 +798,7 @@ describe('bump', () => { 'bumping @backstage-extra/custom in b to ^1.1.0', 'bumping @backstage-extra/custom-two in b to ^2.0.0', 'bumping @backstage/theme in b to ^2.0.0', - 'Skipping backstage.json update as custom pattern is used', + 'Your project is now at version 1.0.0, which has been written to backstage.json', 'Running yarn install to install new versions', 'Checking for moved packages to the @backstage-community namespace...', '⚠️ The following packages may have breaking changes:', diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli/src/modules/migrate/commands/versions/bump.ts index 23202ee1b5..391ccf8f22 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.ts @@ -18,6 +18,7 @@ maybeBootstrapProxy(); import fs from 'fs-extra'; import chalk from 'chalk'; +import { minimatch } from 'minimatch'; import semver from 'semver'; import { OptionValues } from 'commander'; import yaml from 'yaml'; @@ -78,6 +79,14 @@ type PkgVersionInfo = { location: string; }; +function extendsDefaultPattern(pattern: string): boolean { + if (!pattern.endsWith('/*')) { + return false; + } + + return minimatch('@backstage/', pattern.slice(0, -1)); +} + export default async (opts: OptionValues) => { const lockfilePath = paths.resolveTargetRoot('yarn.lock'); const lockfile = await Lockfile.load(lockfilePath); @@ -245,8 +254,8 @@ export default async (opts: OptionValues) => { console.log(); - // Do not update backstage.json when upgrade patterns are used. - if (pattern === DEFAULT_PATTERN_GLOB) { + // Do not update backstage.json when default pattern is not covered + if (extendsDefaultPattern(pattern)) { await bumpBackstageJsonVersion( releaseManifest.releaseVersion, hasYarnPlugin, From a3edb2eb408793779e71e105d9d7439a0df6364f Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 12 Sep 2025 08:59:59 +0200 Subject: [PATCH 166/233] chore: added comment] Signed-off-by: benjdlambert --- plugins/scaffolder-backend/src/service/router.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 2c9df0c8c4..a48bcb46b0 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -309,6 +309,9 @@ export async function createRouter( // TODO(blam): it's a little unfortunate that you have to restart the scaffolder // backend in order to pick these up. We should really just make `ActionsRegistry.get()` async // and then we can move this logic into the there instead. + // But we can't make those changes until next major. + // Alternatively, we could look at setting up a periodic task that refreshes the actions registry, but + // not feeling that it's worth the complexity. const { actions: distributedActions } = await actionsRegistry.list({ credentials: await auth.getOwnServiceCredentials(), }); From 53e19ab6d84851f954e9fd4092c82900986d68c1 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 12 Sep 2025 09:10:58 +0200 Subject: [PATCH 167/233] chore: added tests Signed-off-by: benjdlambert --- .../src/service/router.test.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 4144c552fb..60fcce0739 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -68,6 +68,8 @@ 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 { ActionsService } from '@backstage/backend-plugin-api/alpha'; function createDatabase(): DatabaseService { return DatabaseManager.fromConfig( @@ -178,6 +180,7 @@ const createTestRouter = async ( | Record | CreatedTemplateGlobal[]; autocompleteHandlers?: Record; + actionsRegistry?: ActionsService; } = {}, ) => { const logger = mockServices.logger.mock({ @@ -246,6 +249,7 @@ const createTestRouter = async ( }), createDebugLogAction(), ], + actionsRegistry: overrides.actionsRegistry ?? actionsRegistryServiceMock(), }); router.use(mockErrorHandler()); @@ -275,6 +279,49 @@ describe('scaffolder router', () => { expect(response.body[0].id).toBeDefined(); expect(response.body.length).toBe(2); }); + + it('should include actiosn from the remote actions registry', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + mockActionsRegistry.register({ + name: 'my-demo-action', + title: 'Test', + description: 'Test', + schema: { + input: z => z.object({ name: z.string() }), + output: z => z.object({ name: z.string() }), + }, + action: async () => ({ output: { name: 'test' } }), + }); + const { router } = await createTestRouter({ + actionsRegistry: mockActionsRegistry, + }); + const response = await request(router).get('/v2/actions').send(); + + expect(response.status).toEqual(200); + expect(response.body.length).toBe(3); + + expect(response.body).toContainEqual({ + description: 'Test', + examples: [], + id: 'test:my-demo-action', + schema: { + input: { + $schema: 'http://json-schema.org/draft-07/schema#', + additionalProperties: false, + properties: { name: { type: 'string' } }, + required: ['name'], + type: 'object', + }, + output: { + $schema: 'http://json-schema.org/draft-07/schema#', + additionalProperties: false, + properties: { name: { type: 'string' } }, + required: ['name'], + type: 'object', + }, + }, + }); + }); }); describe('GET /v2/templating-extensions', () => { From a57185fd484bd563f1db6887d9807222231dfa64 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 12 Sep 2025 09:11:40 +0200 Subject: [PATCH 168/233] chore: changeset Signed-off-by: benjdlambert --- .changeset/bumpy-eyes-divide.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/bumpy-eyes-divide.md diff --git a/.changeset/bumpy-eyes-divide.md b/.changeset/bumpy-eyes-divide.md new file mode 100644 index 0000000000..6300f7aaf6 --- /dev/null +++ b/.changeset/bumpy-eyes-divide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Added support for executing actions from the `ActionsRegistry` in the `scaffolder-backend` From ca0aeea3427591f3f76161b5b2c8d52b791a5501 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 12 Sep 2025 10:23:20 +0200 Subject: [PATCH 169/233] chore: code-review comments Signed-off-by: benjdlambert --- plugins/scaffolder-backend/src/service/router.test.ts | 2 +- plugins/scaffolder-backend/src/service/router.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 60fcce0739..16a6000224 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -280,7 +280,7 @@ describe('scaffolder router', () => { expect(response.body.length).toBe(2); }); - it('should include actiosn from the remote actions registry', async () => { + it('should include actions from the remote actions registry', async () => { const mockActionsRegistry = actionsRegistryServiceMock(); mockActionsRegistry.register({ name: 'my-demo-action', diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index a48bcb46b0..2a0792346d 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -133,7 +133,7 @@ import { import { TaskFilters } from '@backstage/plugin-scaffolder-node'; import { ActionsService } from '@backstage/backend-plugin-api/alpha'; -import { isObject } from 'lodash'; +import { isPlainObject } from 'lodash'; /** * RouterOptions @@ -335,8 +335,8 @@ export async function createRouter( credentials: await ctx.getInitiatorCredentials(), }); - if (isObject(output)) { - for (const [key, value] of Object.entries(output)) { + if (isPlainObject(output)) { + for (const [key, value] of Object.entries(output as JsonObject)) { ctx.output(key as keyof typeof output, value); } } From 31ef3f023cf2ab90d7ba5e02ec0987cb864604bb Mon Sep 17 00:00:00 2001 From: chonpisitkong01 <112919164+chonpisitkong01@users.noreply.github.com> Date: Fri, 12 Sep 2025 17:30:14 +0700 Subject: [PATCH 170/233] fix: correct import path for catalogPermissionRules.ts Update incorrect file path reference in catalogPermissionRules.ts import statement. Signed-off-by: chonpisitkong01 <112919164+chonpisitkong01@users.noreply.github.com> --- docs/permissions/custom-rules.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index 61b9a5326a..dd11f4d773 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -184,7 +184,7 @@ To install custom rules in a plugin, we need to use the [`PermissionsRegistrySer import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), ); /* highlight-add-next-line */ - backend.add(import('./extensions/catalogPermissionRules')); + backend.add(import('./modules/catalogPermissionRules.ts')); ``` 5. Now when you run you Backstage instance - `yarn start` - the rule will be added to the catalog plugin. From 99bd91dcf72a2ea2a42bcee6e070915a9b2fcbf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 12 Sep 2025 14:10:19 +0200 Subject: [PATCH 171/233] Update docs/permissions/custom-rules.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- docs/permissions/custom-rules.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index dd11f4d773..7bfcd1220a 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -184,7 +184,7 @@ To install custom rules in a plugin, we need to use the [`PermissionsRegistrySer import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), ); /* highlight-add-next-line */ - backend.add(import('./modules/catalogPermissionRules.ts')); + backend.add(import('./modules/catalogPermissionRules')); ``` 5. Now when you run you Backstage instance - `yarn start` - the rule will be added to the catalog plugin. From 0fc9b1c71f4ebd234431a31cf9fec06000114965 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Sep 2025 14:15:45 +0200 Subject: [PATCH 172/233] create-app: remove redundant lock seed entries Signed-off-by: Patrik Oldsberg --- packages/create-app/seed-yarn.lock | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/packages/create-app/seed-yarn.lock b/packages/create-app/seed-yarn.lock index 21b2aa543d..00a86105cc 100644 --- a/packages/create-app/seed-yarn.lock +++ b/packages/create-app/seed-yarn.lock @@ -16,28 +16,3 @@ // package: the name of the package, e.g. @testing-library/react // query: the version query to pin the version for, e.g. ^14.0.0 // version: the version to pin to, must be in range of the query, e.g. 14.11.0 - -"@google-cloud/storage@^7.0.0": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-7.14.0.tgz#eda9715f68507949214af804c906eba6d168a214" - integrity sha512-H41bPL2cMfSi4EEnFzKvg7XSb7T67ocSXrmF7MPjfgFB0L6CKGzfIYJheAZi1iqXjz6XaCT1OBf6HCG5vDBTOQ== - -"@octokit/types@npm:^13.0.0": - version "13.6.2" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.6.2.tgz#e10fc4d2bdd65d836d1ced223b03ad4cfdb525bd" - integrity sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA== - -"@octokit/types@npm:^13.1.0": - version "13.6.2" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.6.2.tgz#e10fc4d2bdd65d836d1ced223b03ad4cfdb525bd" - integrity sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA== - -"@octokit/types@npm:^13.5.0": - version "13.6.2" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.6.2.tgz#e10fc4d2bdd65d836d1ced223b03ad4cfdb525bd" - integrity sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA== - -"@octokit/openapi-types@^22.2.0": - version "22.2.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-22.2.0.tgz#75aa7dcd440821d99def6a60b5f014207ae4968e" - integrity sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg== From c68cb321aeaab40d83e0a0672c5d6f7b548b13ce Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 12 Sep 2025 15:06:25 +0200 Subject: [PATCH 173/233] chore: exit pre-release Signed-off-by: benjdlambert --- .changeset/pre.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index b1a840849d..174818edf4 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,5 +1,5 @@ { - "mode": "pre", + "mode": "exit", "tag": "next", "initialVersions": { "example-app": "0.2.112", From 7151260d275694c08e689c81366ef76457314a05 Mon Sep 17 00:00:00 2001 From: Alec Jacobs Date: Fri, 12 Sep 2025 14:20:12 -0700 Subject: [PATCH 174/233] fix(scaffolder): prevent MultiEntityPicker from removing existing options from form data Signed-off-by: Alec Jacobs --- .changeset/two-coins-stop.md | 5 ++ .../MultiEntityPicker.test.tsx | 65 +++++++++++++++++++ .../MultiEntityPicker/MultiEntityPicker.tsx | 47 +++++++++----- 3 files changed, 99 insertions(+), 18 deletions(-) create mode 100644 .changeset/two-coins-stop.md diff --git a/.changeset/two-coins-stop.md b/.changeset/two-coins-stop.md new file mode 100644 index 0000000000..005ea81121 --- /dev/null +++ b/.changeset/two-coins-stop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Prevent the MultiEntityPicker from removing options present in form state when new options are selected diff --git a/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.test.tsx b/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.test.tsx index a02d793a34..fc824f20f9 100644 --- a/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.test.tsx @@ -262,6 +262,71 @@ describe('', () => { }); }); + describe('with existing form data', () => { + beforeEach(() => { + uiSchema = { 'ui:options': {} }; + props = { + onChange, + schema, + required, + uiSchema, + rawErrors, + formData: ['group:default/team-a'], + } as unknown as FieldProps; + }); + + it('preserves existing data on blur', async () => { + const { getByRole } = await renderInTestApp( + + + , + ); + + const input = getByRole('textbox'); + + fireEvent.change(input, { target: { value: 'squ' } }); + fireEvent.blur(input); + + expect(onChange).toHaveBeenCalledWith(['group:default/team-a', 'squ']); + }); + + it('preserves existing data on value create', async () => { + const { getByRole } = await renderInTestApp( + + + , + ); + + const input = getByRole('textbox'); + + fireEvent.change(input, { target: { value: 'squ' } }); + fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); + + expect(onChange).toHaveBeenCalledWith(['group:default/team-a', 'squ']); + }); + + it('preserves existing data on selecting an existing option', async () => { + catalogApi.getEntities.mockResolvedValue({ items: entities }); + + const { getByRole } = await renderInTestApp( + + + , + ); + + const input = getByRole('textbox'); + + fireEvent.mouseDown(input); + const optionA = screen.getByText('squad-b'); + await userEvent.click(optionA as HTMLElement); + + expect(onChange).toHaveBeenCalledWith([ + 'group:default/team-a', + 'group:default/squad-b', + ]); + }); + }); + describe('uses full entity ref', () => { beforeEach(() => { uiSchema = { diff --git a/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.tsx b/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.tsx index 57c4b87d6d..39130c4b78 100644 --- a/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/MultiEntityPicker/MultiEntityPicker.tsx @@ -50,6 +50,12 @@ import { scaffolderTranslationRef } from '../../../translation'; export { MultiEntityPickerSchema } from './schema'; +// AutocompleteChangeReason events that can be triggered when a user inputs a freeSolo option +const FREE_SOLO_EVENTS: readonly AutocompleteChangeReason[] = [ + 'blur', + 'create-option', +]; + /** * The underlying component that is rendered in the form for the `MultiEntityPicker` * field extension. @@ -110,29 +116,34 @@ export const MultiEntityPicker = (props: MultiEntityPickerProps) => { (_: any, refs: (string | Entity)[], reason: AutocompleteChangeReason) => { const values = refs .map(ref => { + // If the ref is not a string, then it was a selected option in the picker if (typeof ref !== 'string') { // if ref does not exist: pass 'undefined' to trigger validation for required value return ref ? stringifyEntityRef(ref as Entity) : undefined; } - if (reason === 'blur' || reason === 'create-option') { - // Add in default namespace, etc. - let entityRef = ref; - try { - // Attempt to parse the entity ref into it's full form. - entityRef = stringifyEntityRef( - parseEntityRef(ref as string, { - defaultKind, - defaultNamespace, - }), - ); - } catch (err) { - // If the passed in value isn't an entity ref, do nothing. - } - // We need to check against formData here as that's the previous value for this field. - if (formData?.includes(ref) || allowArbitraryValues) { - return entityRef; - } + // Add in default namespace, etc. + let entityRef = ref; + try { + // Attempt to parse the entity ref into it's full form. + entityRef = stringifyEntityRef( + parseEntityRef(ref as string, { + defaultKind, + defaultNamespace, + }), + ); + } catch (err) { + // If the passed in value isn't an entity ref, do nothing. + } + + // We need to check against formData here as that's the previous value for this field. + if ( + // If value already matches what exists in form data, allow it + formData?.includes(ref) || + // If arbitrary values are allowed and the reason is a free solo event, allow it + (allowArbitraryValues && FREE_SOLO_EVENTS.includes(reason)) + ) { + return entityRef; } return undefined; From 173fdbda7f6741b6797e6f4bfa33fc28ba1dbf58 Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Sun, 14 Sep 2025 13:41:04 +0200 Subject: [PATCH 175/233] Create kwirthmetrics.yaml Add a new plugin for showing Kubernetes live-streaming resource usage charts on Backstage. Signed-off-by: Julio Fernandez --- microsite/data/plugins/kwirthmetrics.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 microsite/data/plugins/kwirthmetrics.yaml diff --git a/microsite/data/plugins/kwirthmetrics.yaml b/microsite/data/plugins/kwirthmetrics.yaml new file mode 100644 index 0000000000..9a8c069eda --- /dev/null +++ b/microsite/data/plugins/kwirthmetrics.yaml @@ -0,0 +1,10 @@ +--- +title: KwirthMetrics for Backstage +author: Julio Fernandez +authorUrl: https://github.com/jfvilas +category: Monitoring +description: Shows live-streaming charts of Kubernetes resources metrics like CPU, memory, I/O... (you can group and merge data). +documentation: https://github.com/jfvilas/plugin-kwirth-metrics +iconUrl: https://raw.githubusercontent.com/jfvilas/plugin-kwirth-metrics/master/src/assets/kwirthmetrics-logo.png +npmPackageName: '@jfvilas/plugin-kwirth-metrics' +addedDate: '2025-09-14' From ffac5071fe09da15cd0d3e2f61c11d4dcc588b27 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sun, 14 Sep 2025 14:37:55 +0100 Subject: [PATCH 176/233] Upgrade Storybook to version 9 Signed-off-by: Charles de Dreuille --- .storybook/main.ts | 67 +- .storybook/msw-browser-shim.js | 47 + .storybook/msw-http-shim.js | 47 + .storybook/preview.tsx | 12 +- package.json | 13 +- packages/ui/package.json | 6 +- .../src/components/Avatar/Avatar.stories.tsx | 2 +- .../ui/src/components/Box/Box.stories.tsx | 2 +- .../src/components/Button/Button.stories.tsx | 2 +- .../ButtonIcon/ButtonIcon.stories.tsx | 2 +- .../ButtonLink/ButtonLink.stories.tsx | 2 +- .../ui/src/components/Card/Card.stories.tsx | 2 +- .../components/Checkbox/Checkbox.stories.tsx | 2 +- .../Collapsible/Collapsible.stories.tsx | 2 +- .../Container/Container.stories.tsx | 2 +- .../FieldError/FieldError.stories.tsx | 2 +- .../FieldLabel/FieldLabel.stories.tsx | 2 +- .../ui/src/components/Flex/Flex.stories.tsx | 2 +- .../ui/src/components/Grid/Grid.stories.tsx | 4 +- .../src/components/Header/Header.stories.tsx | 2 +- .../HeaderPage/HeaderPage.stories.tsx | 2 +- .../ui/src/components/Icon/Icon.stories.tsx | 2 +- .../ui/src/components/Link/Link.stories.tsx | 2 +- .../ui/src/components/Menu/Menu.stories.tsx | 2 +- .../RadioGroup/RadioGroup.stories.tsx | 2 +- .../ScrollArea/ScrollArea.stories.tsx | 2 +- .../SearchField/SearchField.stories.tsx | 2 +- .../src/components/Select/Select.stories.tsx | 2 +- .../components/Skeleton/Skeleton.stories.tsx | 2 +- .../src/components/Switch/Switch.stories.tsx | 2 +- .../ui/src/components/Table/Table.stories.tsx | 2 +- .../TablePagination.stories.tsx | 2 +- .../ui/src/components/Tabs/Tabs.stories.tsx | 2 +- .../components/TagGroup/TagGroup.stories.tsx | 2 +- .../ui/src/components/Text/Text.stories.tsx | 2 +- .../TextField/TextField.stories.tsx | 2 +- .../components/Tooltip/Tooltip.stories.tsx | 22 +- yarn.lock | 959 ++++++------------ 38 files changed, 506 insertions(+), 729 deletions(-) create mode 100644 .storybook/msw-browser-shim.js create mode 100644 .storybook/msw-http-shim.js diff --git a/.storybook/main.ts b/.storybook/main.ts index caf2d744e3..74d84b55ab 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -35,15 +35,76 @@ const config: StorybookConfig = { stories, addons: [ getAbsolutePath('@storybook/addon-links'), - getAbsolutePath('@storybook/addon-essentials'), - getAbsolutePath('@storybook/addon-interactions'), getAbsolutePath('@storybook/addon-themes'), - getAbsolutePath('@storybook/addon-storysource'), + getAbsolutePath('@storybook/addon-docs'), ], framework: { name: getAbsolutePath('@storybook/react-vite'), options: {}, }, + viteFinal: async config => { + // Add Node.js polyfills for browser compatibility + // + // When upgrading from Storybook 8 to 9 with the react-vite framework, + // Node.js polyfills are no longer automatically included by Vite. + // This causes "ReferenceError: process is not defined" errors in the browser + // when code tries to access Node.js globals like `process` and `util`. + // + // The @vitest/mocker (included with Storybook 9) expects MSW v2 APIs, + // but we want to keep MSW v1 for the rest of the monorepo to avoid + // breaking changes. This configuration provides the necessary polyfills + // and handles the MSW compatibility issue specifically for Storybook. + // + // These polyfills provide browser-compatible versions of Node.js globals: + // - process: Node.js process object with env + // - global -> globalThis: Maps Node.js global to browser's globalThis + // + // Without these, Backstage components that rely on Node.js APIs will fail + // to load in Storybook's browser environment. + config.define = { + ...config.define, + global: 'globalThis', + 'process.env': '{}', + process: '{ env: {}, browser: true }', + }; + + config.resolve = { + ...config.resolve, + alias: { + ...config.resolve?.alias, + // Provide Node.js polyfills for browser + process: 'process/browser', + util: 'util', + buffer: 'buffer', + stream: 'stream-browserify', + // Fix MSW v2 imports for @vitest/mocker compatibility + // @vitest/mocker expects MSW v2 APIs but we want to keep MSW v1 for the rest of the monorepo + 'msw/browser': join(__dirname, 'msw-browser-shim.js'), + 'msw/core/http': join(__dirname, 'msw-http-shim.js'), + }, + }; + + // Optimize dependencies for better performance + config.optimizeDeps = { + ...config.optimizeDeps, + include: [ + ...(config.optimizeDeps?.include || []), + 'process/browser', + 'util', + 'buffer', + 'stream-browserify', + ], + // Exclude MSW to prevent optimization conflicts with our shims + exclude: [ + ...(config.optimizeDeps?.exclude || []), + 'msw', + 'msw/browser', + 'msw/core/http', + ], + }; + + return config; + }, }; export default config; diff --git a/.storybook/msw-browser-shim.js b/.storybook/msw-browser-shim.js new file mode 100644 index 0000000000..e0c71e3d3e --- /dev/null +++ b/.storybook/msw-browser-shim.js @@ -0,0 +1,47 @@ +// MSW v2 browser compatibility shim for @vitest/mocker +// This provides MSW v2 exports using MSW v1 APIs to maintain compatibility +// while keeping the rest of the monorepo on MSW v1 + +try { + // Try to import from MSW v1 + const { setupWorker, rest } = require('msw'); + + // Export in MSW v2 format expected by @vitest/mocker + module.exports = { + setupWorker, + // MSW v2 uses 'http' instead of 'rest', but we provide both for compatibility + http: rest, + rest, // Keep rest for any MSW v1 code + }; +} catch (error) { + // Fallback if MSW is not available - provide minimal mock + console.warn( + 'MSW not available, providing minimal mock for @vitest/mocker compatibility', + ); + module.exports = { + setupWorker: () => ({ + start: () => Promise.resolve(), + stop: () => {}, + use: () => {}, + resetHandlers: () => {}, + }), + http: { + get: () => {}, + post: () => {}, + put: () => {}, + delete: () => {}, + patch: () => {}, + head: () => {}, + options: () => {}, + }, + rest: { + get: () => {}, + post: () => {}, + put: () => {}, + delete: () => {}, + patch: () => {}, + head: () => {}, + options: () => {}, + }, + }; +} diff --git a/.storybook/msw-http-shim.js b/.storybook/msw-http-shim.js new file mode 100644 index 0000000000..8653256960 --- /dev/null +++ b/.storybook/msw-http-shim.js @@ -0,0 +1,47 @@ +// MSW v2 http compatibility shim for @vitest/mocker +// This provides MSW v2 http exports using MSW v1 APIs + +try { + // Try to import from MSW v1 + const { rest } = require('msw'); + + // Export MSW v1 'rest' as MSW v2 'http' for @vitest/mocker compatibility + module.exports = { + http: rest, + // Provide individual methods that @vitest/mocker might expect + get: rest.get, + post: rest.post, + put: rest.put, + delete: rest.delete, + patch: rest.patch, + head: rest.head, + options: rest.options, + all: rest.all, + }; +} catch (error) { + // Fallback if MSW is not available + console.warn( + 'MSW not available, providing minimal http mock for @vitest/mocker compatibility', + ); + const noop = () => {}; + module.exports = { + http: { + get: noop, + post: noop, + put: noop, + delete: noop, + patch: noop, + head: noop, + options: noop, + all: noop, + }, + get: noop, + post: noop, + put: noop, + delete: noop, + patch: noop, + head: noop, + options: noop, + all: noop, + }; +} diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index d81c2e9932..d934412521 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -2,8 +2,8 @@ import React, { useEffect } from 'react'; import { TestApiProvider } from '@backstage/test-utils'; import { Content, AlertDisplay } from '@backstage/core-components'; import { apis } from './support/apis'; -import type { Decorator, Preview } from '@storybook/react'; -import { useGlobals } from '@storybook/preview-api'; +import type { Decorator, Preview } from '@storybook/react-vite'; +import { useGlobals } from 'storybook/preview-api'; import { UnifiedThemeProvider, themes } from '@backstage/theme'; // Default Backstage theme CSS (from packages/ui) @@ -52,20 +52,24 @@ const preview: Preview = { }, parameters: { layout: 'fullscreen', + backgrounds: { disable: true, }, + controls: { matchers: { color: /(background|color)$/i, date: /Date$/i, }, }, + options: { storySort: { order: ['Backstage UI', 'Plugins', 'Layout', 'Navigation'], }, }, + viewport: { viewports: { initial: { @@ -82,6 +86,10 @@ const preview: Preview = { }, }, }, + + docs: { + codePanel: true, + }, }, decorators: [ Story => { diff --git a/package.json b/package.json index 7aed18ab74..a96c13a76a 100644 --- a/package.json +++ b/package.json @@ -129,13 +129,10 @@ "@octokit/rest": "^19.0.3", "@playwright/test": "^1.32.3", "@spotify/eslint-plugin": "^15.0.0", - "@storybook/addon-essentials": "^8.6.12", - "@storybook/addon-interactions": "^8.6.12", - "@storybook/addon-links": "^8.6.12", - "@storybook/addon-storysource": "^8.6.12", - "@storybook/addon-themes": "^8.6.12", - "@storybook/react": "^8.6.12", - "@storybook/react-vite": "^8.6.12", + "@storybook/addon-docs": "^9.1.5", + "@storybook/addon-links": "^9.1.5", + "@storybook/addon-themes": "^9.1.5", + "@storybook/react-vite": "^9.1.5", "@techdocs/cli": "workspace:*", "@types/cacheable-request": "^8.3.6", "@types/memjs": "^1.3.3", @@ -160,7 +157,7 @@ "shx": "^0.4.0", "sloc": "^0.3.1", "sort-package-json": "^2.8.0", - "storybook": "^8.6.12", + "storybook": "^9.1.5", "typedoc": "^0.28.0", "typescript": "~5.7.0", "vite": "^7.1.2" diff --git a/packages/ui/package.json b/packages/ui/package.json index 36b51f0bc8..7b14af29e1 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -40,18 +40,16 @@ "dependencies": { "@base-ui-components/react": "1.0.0-alpha.7", "@remixicon/react": "^4.6.0", - "@storybook/test": "^8.6.12", "@tanstack/react-table": "^8.21.3", "clsx": "^2.1.1", "react-aria-components": "^1.10.1" }, "devDependencies": { "@backstage/cli": "workspace:^", - "@storybook/react": "^8.6.12", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "chalk": "^5.4.1", - "eslint-plugin-storybook": "^0.12.0", + "eslint-plugin-storybook": "^9.1.5", "glob": "^11.0.1", "globals": "^15.11.0", "lightningcss": "^1.29.1", @@ -59,7 +57,7 @@ "react": "^18.0.2", "react-dom": "^18.0.2", "react-router-dom": "^6.3.0", - "storybook": "^8.6.12" + "storybook": "^9.1.5" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0", diff --git a/packages/ui/src/components/Avatar/Avatar.stories.tsx b/packages/ui/src/components/Avatar/Avatar.stories.tsx index 69dcd5d369..c91fe736db 100644 --- a/packages/ui/src/components/Avatar/Avatar.stories.tsx +++ b/packages/ui/src/components/Avatar/Avatar.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Avatar } from './index'; import { Flex } from '../..'; diff --git a/packages/ui/src/components/Box/Box.stories.tsx b/packages/ui/src/components/Box/Box.stories.tsx index 6c31f63f96..f0086c3d25 100644 --- a/packages/ui/src/components/Box/Box.stories.tsx +++ b/packages/ui/src/components/Box/Box.stories.tsx @@ -15,7 +15,7 @@ */ import { ReactNode } from 'react'; -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Box } from './Box'; import { Flex } from '../Flex'; diff --git a/packages/ui/src/components/Button/Button.stories.tsx b/packages/ui/src/components/Button/Button.stories.tsx index 962db2e898..db8e999f98 100644 --- a/packages/ui/src/components/Button/Button.stories.tsx +++ b/packages/ui/src/components/Button/Button.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Button } from './Button'; import { Flex } from '../Flex'; import { Text } from '../Text'; diff --git a/packages/ui/src/components/ButtonIcon/ButtonIcon.stories.tsx b/packages/ui/src/components/ButtonIcon/ButtonIcon.stories.tsx index 10e57fbb45..4dcc858084 100644 --- a/packages/ui/src/components/ButtonIcon/ButtonIcon.stories.tsx +++ b/packages/ui/src/components/ButtonIcon/ButtonIcon.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { ButtonIcon } from './ButtonIcon'; import { Flex } from '../Flex'; import { Text } from '../Text'; diff --git a/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx b/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx index 0ded9ef741..b4667f017b 100644 --- a/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx +++ b/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { ButtonLink } from './ButtonLink'; import { Flex } from '../Flex'; import { Text } from '../Text'; diff --git a/packages/ui/src/components/Card/Card.stories.tsx b/packages/ui/src/components/Card/Card.stories.tsx index 2b8466f145..382de8fb8a 100644 --- a/packages/ui/src/components/Card/Card.stories.tsx +++ b/packages/ui/src/components/Card/Card.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Card, CardHeader, CardBody, CardFooter } from './Card'; import { IconNames, Text } from '../..'; diff --git a/packages/ui/src/components/Checkbox/Checkbox.stories.tsx b/packages/ui/src/components/Checkbox/Checkbox.stories.tsx index a8aa46fc07..080cae40b6 100644 --- a/packages/ui/src/components/Checkbox/Checkbox.stories.tsx +++ b/packages/ui/src/components/Checkbox/Checkbox.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Checkbox } from './Checkbox'; import { Flex } from '../Flex'; import { Text } from '../Text'; diff --git a/packages/ui/src/components/Collapsible/Collapsible.stories.tsx b/packages/ui/src/components/Collapsible/Collapsible.stories.tsx index 6f2c761519..7271e6f582 100644 --- a/packages/ui/src/components/Collapsible/Collapsible.stories.tsx +++ b/packages/ui/src/components/Collapsible/Collapsible.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Collapsible } from './Collapsible'; import { Button } from '../Button'; import { Box } from '../Box'; diff --git a/packages/ui/src/components/Container/Container.stories.tsx b/packages/ui/src/components/Container/Container.stories.tsx index 7dfbd9ea6e..504cb3de87 100644 --- a/packages/ui/src/components/Container/Container.stories.tsx +++ b/packages/ui/src/components/Container/Container.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Box } from '../Box/Box'; import { Container } from './Container'; diff --git a/packages/ui/src/components/FieldError/FieldError.stories.tsx b/packages/ui/src/components/FieldError/FieldError.stories.tsx index 2fad9708d8..1922e12a0d 100644 --- a/packages/ui/src/components/FieldError/FieldError.stories.tsx +++ b/packages/ui/src/components/FieldError/FieldError.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { TextField, Input, Form } from 'react-aria-components'; import { FieldError } from './FieldError'; diff --git a/packages/ui/src/components/FieldLabel/FieldLabel.stories.tsx b/packages/ui/src/components/FieldLabel/FieldLabel.stories.tsx index 59a0200711..d0e8f3e4fb 100644 --- a/packages/ui/src/components/FieldLabel/FieldLabel.stories.tsx +++ b/packages/ui/src/components/FieldLabel/FieldLabel.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { FieldLabel } from './FieldLabel'; const meta = { diff --git a/packages/ui/src/components/Flex/Flex.stories.tsx b/packages/ui/src/components/Flex/Flex.stories.tsx index e557d4700b..40f9339ad6 100644 --- a/packages/ui/src/components/Flex/Flex.stories.tsx +++ b/packages/ui/src/components/Flex/Flex.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Flex } from './Flex'; import { Text } from '../Text'; diff --git a/packages/ui/src/components/Grid/Grid.stories.tsx b/packages/ui/src/components/Grid/Grid.stories.tsx index 26cce0387b..66ea346b4f 100644 --- a/packages/ui/src/components/Grid/Grid.stories.tsx +++ b/packages/ui/src/components/Grid/Grid.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Grid } from './Grid'; import type { GridItemProps } from './types'; import { Box } from '../Box/Box'; @@ -23,7 +23,7 @@ import { Flex } from '../Flex'; const meta = { title: 'Backstage UI/Grid', component: Grid.Root, -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; diff --git a/packages/ui/src/components/Header/Header.stories.tsx b/packages/ui/src/components/Header/Header.stories.tsx index 9386ea5001..7ae834f82e 100644 --- a/packages/ui/src/components/Header/Header.stories.tsx +++ b/packages/ui/src/components/Header/Header.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj, StoryFn } from '@storybook/react'; +import type { Meta, StoryObj, StoryFn } from '@storybook/react-vite'; import { Header } from './Header'; import type { HeaderTab } from './types'; import { diff --git a/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx b/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx index f819ec369d..c4849ddc20 100644 --- a/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx +++ b/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj, StoryFn } from '@storybook/react'; +import type { Meta, StoryObj, StoryFn } from '@storybook/react-vite'; import { HeaderPage } from './HeaderPage'; import type { HeaderTab } from '../Header/types'; import { MemoryRouter } from 'react-router-dom'; diff --git a/packages/ui/src/components/Icon/Icon.stories.tsx b/packages/ui/src/components/Icon/Icon.stories.tsx index b4eb01f2ff..83dc62c173 100644 --- a/packages/ui/src/components/Icon/Icon.stories.tsx +++ b/packages/ui/src/components/Icon/Icon.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Meta, StoryObj } from '@storybook/react'; +import { Meta, StoryObj } from '@storybook/react-vite'; import { Icon } from './Icon'; import { IconProvider } from './provider'; import { icons } from './icons'; diff --git a/packages/ui/src/components/Link/Link.stories.tsx b/packages/ui/src/components/Link/Link.stories.tsx index 027869d660..37f142c4dc 100644 --- a/packages/ui/src/components/Link/Link.stories.tsx +++ b/packages/ui/src/components/Link/Link.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryFn, StoryObj } from '@storybook/react'; +import type { Meta, StoryFn, StoryObj } from '@storybook/react-vite'; import { Link } from './Link'; import { Flex } from '../Flex'; import { Text } from '../Text'; diff --git a/packages/ui/src/components/Menu/Menu.stories.tsx b/packages/ui/src/components/Menu/Menu.stories.tsx index 585eabe737..272d2c5342 100644 --- a/packages/ui/src/components/Menu/Menu.stories.tsx +++ b/packages/ui/src/components/Menu/Menu.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { MenuTrigger, SubmenuTrigger, diff --git a/packages/ui/src/components/RadioGroup/RadioGroup.stories.tsx b/packages/ui/src/components/RadioGroup/RadioGroup.stories.tsx index b9c4c26f08..df7a826c50 100644 --- a/packages/ui/src/components/RadioGroup/RadioGroup.stories.tsx +++ b/packages/ui/src/components/RadioGroup/RadioGroup.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { RadioGroup, Radio } from './RadioGroup'; const meta = { diff --git a/packages/ui/src/components/ScrollArea/ScrollArea.stories.tsx b/packages/ui/src/components/ScrollArea/ScrollArea.stories.tsx index 95a7fc3329..48a329eea4 100644 --- a/packages/ui/src/components/ScrollArea/ScrollArea.stories.tsx +++ b/packages/ui/src/components/ScrollArea/ScrollArea.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { ScrollArea } from './ScrollArea'; import { Text } from '../Text/Text'; diff --git a/packages/ui/src/components/SearchField/SearchField.stories.tsx b/packages/ui/src/components/SearchField/SearchField.stories.tsx index c8867215a1..14f265b243 100644 --- a/packages/ui/src/components/SearchField/SearchField.stories.tsx +++ b/packages/ui/src/components/SearchField/SearchField.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { SearchField } from './SearchField'; import { Form } from 'react-aria-components'; import { Icon } from '../Icon'; diff --git a/packages/ui/src/components/Select/Select.stories.tsx b/packages/ui/src/components/Select/Select.stories.tsx index 5c3ed242dc..6e96e37f08 100644 --- a/packages/ui/src/components/Select/Select.stories.tsx +++ b/packages/ui/src/components/Select/Select.stories.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Select } from './Select'; import { Flex } from '../Flex'; import { Form } from 'react-aria-components'; diff --git a/packages/ui/src/components/Skeleton/Skeleton.stories.tsx b/packages/ui/src/components/Skeleton/Skeleton.stories.tsx index 95f3e57547..bbe25942f0 100644 --- a/packages/ui/src/components/Skeleton/Skeleton.stories.tsx +++ b/packages/ui/src/components/Skeleton/Skeleton.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Skeleton } from './Skeleton'; import { Flex } from '../Flex'; diff --git a/packages/ui/src/components/Switch/Switch.stories.tsx b/packages/ui/src/components/Switch/Switch.stories.tsx index a8d12bccca..6d841bf772 100644 --- a/packages/ui/src/components/Switch/Switch.stories.tsx +++ b/packages/ui/src/components/Switch/Switch.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Switch } from './Switch'; const meta = { diff --git a/packages/ui/src/components/Table/Table.stories.tsx b/packages/ui/src/components/Table/Table.stories.tsx index 903caba25d..564357bfde 100644 --- a/packages/ui/src/components/Table/Table.stories.tsx +++ b/packages/ui/src/components/Table/Table.stories.tsx @@ -15,7 +15,7 @@ */ import { useState } from 'react'; -import type { Meta, StoryFn, StoryObj } from '@storybook/react'; +import type { Meta, StoryFn, StoryObj } from '@storybook/react-vite'; import { Table, TableHeader, diff --git a/packages/ui/src/components/TablePagination/TablePagination.stories.tsx b/packages/ui/src/components/TablePagination/TablePagination.stories.tsx index 2782837428..4550d00abb 100644 --- a/packages/ui/src/components/TablePagination/TablePagination.stories.tsx +++ b/packages/ui/src/components/TablePagination/TablePagination.stories.tsx @@ -16,7 +16,7 @@ // TODO: Bring useArgs() back when we update Storybook to 9 // import { useArgs } from 'storybook/preview-api'; -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { TablePagination } from './TablePagination'; const meta = { diff --git a/packages/ui/src/components/Tabs/Tabs.stories.tsx b/packages/ui/src/components/Tabs/Tabs.stories.tsx index 8ff08b8c5a..cbfe3d8562 100644 --- a/packages/ui/src/components/Tabs/Tabs.stories.tsx +++ b/packages/ui/src/components/Tabs/Tabs.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryFn, StoryObj } from '@storybook/react'; +import type { Meta, StoryFn, StoryObj } from '@storybook/react-vite'; import { Tabs, TabList, Tab, TabPanel } from './Tabs'; import { MemoryRouter } from 'react-router-dom'; import { Box } from '../Box'; diff --git a/packages/ui/src/components/TagGroup/TagGroup.stories.tsx b/packages/ui/src/components/TagGroup/TagGroup.stories.tsx index 03b5de23e9..409956e049 100644 --- a/packages/ui/src/components/TagGroup/TagGroup.stories.tsx +++ b/packages/ui/src/components/TagGroup/TagGroup.stories.tsx @@ -15,7 +15,7 @@ */ import { useState } from 'react'; -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { TagGroup, Tag } from '.'; import type { Selection } from 'react-aria-components'; import { Flex, Icon, IconNames } from '../../'; diff --git a/packages/ui/src/components/Text/Text.stories.tsx b/packages/ui/src/components/Text/Text.stories.tsx index 8329ed70dc..7f95f4d5a1 100644 --- a/packages/ui/src/components/Text/Text.stories.tsx +++ b/packages/ui/src/components/Text/Text.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Text } from './Text'; import { Flex } from '../Flex'; diff --git a/packages/ui/src/components/TextField/TextField.stories.tsx b/packages/ui/src/components/TextField/TextField.stories.tsx index eb64e95514..86da34d147 100644 --- a/packages/ui/src/components/TextField/TextField.stories.tsx +++ b/packages/ui/src/components/TextField/TextField.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { TextField } from './TextField'; import { Form } from 'react-aria-components'; import { Icon } from '../Icon'; diff --git a/packages/ui/src/components/Tooltip/Tooltip.stories.tsx b/packages/ui/src/components/Tooltip/Tooltip.stories.tsx index ff2e19f2a2..cc6b513ed4 100644 --- a/packages/ui/src/components/Tooltip/Tooltip.stories.tsx +++ b/packages/ui/src/components/Tooltip/Tooltip.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Placement } from '@react-types/overlays'; import { TooltipTrigger, Tooltip } from './Tooltip'; import { Button } from '../Button/Button'; @@ -41,7 +41,7 @@ const meta = { control: { type: 'number' }, }, }, - render: ({ tooltip, isOpen, isDisabled, placement, delay, closeDelay }) => ( + render: ({ children, isOpen, isDisabled, placement, delay, closeDelay }) => ( - {tooltip ?? 'I am a tooltip'} + {children ?? 'I am a tooltip'} ), } as Meta<{ - tooltip?: string; + children?: string; isOpen?: boolean; isDisabled?: boolean; placement?: Placement; @@ -66,7 +66,7 @@ type Story = StoryObj; export const Default: Story = { args: { - tooltip: 'I am a tooltip', + children: 'I am a tooltip', }, }; @@ -102,14 +102,14 @@ export const OrthogonalPlacements: Story = { ...Default.args, isOpen: true, }, - render: ({ isOpen, tooltip }) => { + render: ({ isOpen, children }) => { return ( - {tooltip} - {tooltip} - {tooltip} - {tooltip} + {children} + {children} + {children} + {children} ); }, @@ -119,7 +119,7 @@ export const WithLongText: Story = { args: { ...Default.args, isOpen: true, - tooltip: + children: 'I am a tooltip with a very long text. orem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', }, }; diff --git a/yarn.lock b/yarn.lock index 0e5078d824..3041ed63d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -170,16 +170,6 @@ __metadata: languageName: node linkType: hard -"@ampproject/remapping@npm:^2.2.0": - version: 2.2.1 - resolution: "@ampproject/remapping@npm:2.2.1" - dependencies: - "@jridgewell/gen-mapping": "npm:^0.3.0" - "@jridgewell/trace-mapping": "npm:^0.3.9" - checksum: 10/e15fecbf3b54c988c8b4fdea8ef514ab482537e8a080b2978cc4b47ccca7140577ca7b65ad3322dcce65bc73ee6e5b90cbfe0bbd8c766dad04d5c62ec9634c42 - languageName: node - linkType: hard - "@apidevtools/json-schema-ref-parser@npm:9.0.6": version: 9.0.6 resolution: "@apidevtools/json-schema-ref-parser@npm:9.0.6" @@ -1914,39 +1904,39 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.24.7": - version: 7.28.0 - resolution: "@babel/core@npm:7.28.0" +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.24.7, @babel/core@npm:^7.28.0": + version: 7.28.4 + resolution: "@babel/core@npm:7.28.4" dependencies: - "@ampproject/remapping": "npm:^2.2.0" "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.28.0" + "@babel/generator": "npm:^7.28.3" "@babel/helper-compilation-targets": "npm:^7.27.2" - "@babel/helper-module-transforms": "npm:^7.27.3" - "@babel/helpers": "npm:^7.27.6" - "@babel/parser": "npm:^7.28.0" + "@babel/helper-module-transforms": "npm:^7.28.3" + "@babel/helpers": "npm:^7.28.4" + "@babel/parser": "npm:^7.28.4" "@babel/template": "npm:^7.27.2" - "@babel/traverse": "npm:^7.28.0" - "@babel/types": "npm:^7.28.0" + "@babel/traverse": "npm:^7.28.4" + "@babel/types": "npm:^7.28.4" + "@jridgewell/remapping": "npm:^2.3.5" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10/1c86eec8d76053f7b1c5f65296d51d7b8ac00f80d169ff76d3cd2e7d85ab222eb100d40cc3314f41b96c8cc06e9abab21c63d246161f0f3f70ef14c958419c33 + checksum: 10/0593295241fac9be567145ef16f3858d34fc91390a9438c6d47476be9823af4cc0488c851c59702dd46b968e9fd46d17ddf0105ea30195ca85f5a66b4044c519 languageName: node linkType: hard -"@babel/generator@npm:^7.28.0, @babel/generator@npm:^7.7.2": - version: 7.28.0 - resolution: "@babel/generator@npm:7.28.0" +"@babel/generator@npm:^7.28.3, @babel/generator@npm:^7.7.2": + version: 7.28.3 + resolution: "@babel/generator@npm:7.28.3" dependencies: - "@babel/parser": "npm:^7.28.0" - "@babel/types": "npm:^7.28.0" + "@babel/parser": "npm:^7.28.3" + "@babel/types": "npm:^7.28.2" "@jridgewell/gen-mapping": "npm:^0.3.12" "@jridgewell/trace-mapping": "npm:^0.3.28" jsesc: "npm:^3.0.2" - checksum: 10/064c5ba4c07ecd7600377bd0022d5f6bdb3b35e9ff78d9378f6bd1e656467ca902c091647222ab2f0d2967f6d6c0ca33157d37dd9b1c51926c9b0e1527ab9b92 + checksum: 10/d00d1e6b51059e47594aab7920b88ec6fcef6489954a9172235ab57ad2e91b39c95376963a6e2e4cc7e8b88fa4f931018f71f9ab32bbc9c0bc0de35a0231f26c languageName: node linkType: hard @@ -2016,16 +2006,16 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.24.8, @babel/helper-module-transforms@npm:^7.27.3": - version: 7.27.3 - resolution: "@babel/helper-module-transforms@npm:7.27.3" +"@babel/helper-module-transforms@npm:^7.24.8, @babel/helper-module-transforms@npm:^7.28.3": + version: 7.28.3 + resolution: "@babel/helper-module-transforms@npm:7.28.3" dependencies: "@babel/helper-module-imports": "npm:^7.27.1" "@babel/helper-validator-identifier": "npm:^7.27.1" - "@babel/traverse": "npm:^7.27.3" + "@babel/traverse": "npm:^7.28.3" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/47abc90ceb181b4bdea9bf1717adf536d1b5e5acb6f6d8a7a4524080318b5ca8a99e6d58677268c596bad71077d1d98834d2c3815f2443e6d3f287962300f15d + checksum: 10/598fdd8aa5b91f08542d0ba62a737847d0e752c8b95ae2566bc9d11d371856d6867d93e50db870fb836a6c44cfe481c189d8a2b35ca025a224f070624be9fa87 languageName: node linkType: hard @@ -2099,13 +2089,13 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.27.6": - version: 7.28.2 - resolution: "@babel/helpers@npm:7.28.2" +"@babel/helpers@npm:^7.28.4": + version: 7.28.4 + resolution: "@babel/helpers@npm:7.28.4" dependencies: "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.28.2" - checksum: 10/09fd7965e83d4777a4331a082677a1a2261cec451bf3307cb0fb62b2d32c83d55fb1cac494a5dab5c6ad9da459883b8d4e49142812b10ef3e36b54022b2de3a4 + "@babel/types": "npm:^7.28.4" + checksum: 10/5a70a82e196cf8808f8a449cc4780c34d02edda2bb136d39ce9d26e63b615f18e89a95472230c3ce7695db0d33e7026efeee56f6454ed43480f223007ed205eb languageName: node linkType: hard @@ -2121,14 +2111,14 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.24.7, @babel/parser@npm:^7.26.7, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.5, @babel/parser@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/parser@npm:7.28.0" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.24.7, @babel/parser@npm:^7.26.7, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.5, @babel/parser@npm:^7.28.3, @babel/parser@npm:^7.28.4": + version: 7.28.4 + resolution: "@babel/parser@npm:7.28.4" dependencies: - "@babel/types": "npm:^7.28.0" + "@babel/types": "npm:^7.28.4" bin: parser: ./bin/babel-parser.js - checksum: 10/2c14a0d2600bae9ab81924df0a85bbd34e427caa099c260743f7c6c12b2042e743e776043a0d1a2573229ae648f7e66a80cfb26fc27e2a9eb59b55932d44c817 + checksum: 10/f54c46213ef180b149f6a17ea765bf40acc1aebe2009f594e2a283aec69a190c6dda1fdf24c61a258dbeb903abb8ffb7a28f1a378f8ab5d333846ce7b7e23bf1 languageName: node linkType: hard @@ -2439,7 +2429,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.28.2 resolution: "@babel/runtime@npm:7.28.2" checksum: 10/a0965fbdd6aaa40709290923bbe05e1c4314021f0cef608eb1d69f04f717c41829e50a53d79c4a0f461512b4be9b3c0190dc19387b219bcdaacdd793b2fe1b8a @@ -2457,28 +2447,28 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.24.7, @babel/traverse@npm:^7.24.8, @babel/traverse@npm:^7.25.0, @babel/traverse@npm:^7.25.4, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.27.3, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.4.5": - version: 7.28.0 - resolution: "@babel/traverse@npm:7.28.0" +"@babel/traverse@npm:^7.24.7, @babel/traverse@npm:^7.24.8, @babel/traverse@npm:^7.25.0, @babel/traverse@npm:^7.25.4, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4, @babel/traverse@npm:^7.4.5": + version: 7.28.4 + resolution: "@babel/traverse@npm:7.28.4" dependencies: "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.28.0" + "@babel/generator": "npm:^7.28.3" "@babel/helper-globals": "npm:^7.28.0" - "@babel/parser": "npm:^7.28.0" + "@babel/parser": "npm:^7.28.4" "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.28.0" + "@babel/types": "npm:^7.28.4" debug: "npm:^4.3.1" - checksum: 10/c1c24b12b6cb46241ec5d11ddbd2989d6955c282715cbd8ee91a09fe156b3bdb0b88353ac33329c2992113e3dfb5198f616c834f8805bb3fa85da1f864bec5f3 + checksum: 10/c3099364b7b1c36bcd111099195d4abeef16499e5defb1e56766b754e8b768c252e856ed9041665158aa1b31215fc6682632756803c8fa53405381ec08c4752b languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.27.1, @babel/types@npm:^7.28.0, @babel/types@npm:^7.28.2, @babel/types@npm:^7.3.3": - version: 7.28.2 - resolution: "@babel/types@npm:7.28.2" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.27.1, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4, @babel/types@npm:^7.3.3": + version: 7.28.4 + resolution: "@babel/types@npm:7.28.4" dependencies: "@babel/helper-string-parser": "npm:^7.27.1" "@babel/helper-validator-identifier": "npm:^7.27.1" - checksum: 10/a8de404a2e3109651f346d892dc020ce2c82046068f4ce24de7f487738dfbfa7bd716b35f1dcd6d6c32dde96208dc74a56b7f56a2c0bcb5af0ddc56cbee13533 + checksum: 10/db50bf257aafa5d845ad16dae0587f57d596e4be4cbb233ea539976a4c461f9fbcc0bf3d37adae3f8ce5dcb4001462aa608f3558161258b585f6ce6ce21a2e45 languageName: node linkType: hard @@ -7547,14 +7537,12 @@ __metadata: "@backstage/cli": "workspace:^" "@base-ui-components/react": "npm:1.0.0-alpha.7" "@remixicon/react": "npm:^4.6.0" - "@storybook/react": "npm:^8.6.12" - "@storybook/test": "npm:^8.6.12" "@tanstack/react-table": "npm:^8.21.3" "@types/react": "npm:^18.0.0" "@types/react-dom": "npm:^18.0.0" chalk: "npm:^5.4.1" clsx: "npm:^2.1.1" - eslint-plugin-storybook: "npm:^0.12.0" + eslint-plugin-storybook: "npm:^9.1.5" glob: "npm:^11.0.1" globals: "npm:^15.11.0" lightningcss: "npm:^1.29.1" @@ -7563,7 +7551,7 @@ __metadata: react-aria-components: "npm:^1.10.1" react-dom: "npm:^18.0.2" react-router-dom: "npm:^6.3.0" - storybook: "npm:^8.6.12" + storybook: "npm:^9.1.5" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -7657,16 +7645,6 @@ __metadata: languageName: node linkType: hard -"@bundled-es-modules/tough-cookie@npm:^0.1.6": - version: 0.1.6 - resolution: "@bundled-es-modules/tough-cookie@npm:0.1.6" - dependencies: - "@types/tough-cookie": "npm:^4.0.5" - tough-cookie: "npm:^4.1.4" - checksum: 10/4f24a820f02c08c3ca0ff21272317357152093f76f9c8cc182517f61fa426ae53dadc4d68a3d6da5078e8d73f0ff8c0907a9f994c0be756162ba9c7358533e57 - languageName: node - linkType: hard - "@changesets/apply-release-plan@npm:^7.0.12": version: 7.0.12 resolution: "@changesets/apply-release-plan@npm:7.0.12" @@ -10078,24 +10056,24 @@ __metadata: languageName: node linkType: hard -"@joshwooding/vite-plugin-react-docgen-typescript@npm:0.5.0": - version: 0.5.0 - resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.5.0" +"@joshwooding/vite-plugin-react-docgen-typescript@npm:0.6.1": + version: 0.6.1 + resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.6.1" dependencies: glob: "npm:^10.0.0" - magic-string: "npm:^0.27.0" + magic-string: "npm:^0.30.0" react-docgen-typescript: "npm:^2.2.2" peerDependencies: typescript: ">= 4.3.x" - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 10/1dcb03f2df1723799a7a9c75ac8360990f75c44fd2425d2d52a9e21882fc3054d372892ab1cad0927864d8a934ad5b347f4ae00b01785649e2a8f1c4b861aa67 + checksum: 10/a82b6005378ccda13250fcfeaa04ec2ba17d1c2923b5bba5907d5b2fd658c661b29c1215eb1c0fe305b390bee89ec77c684aa68506262734c3bb3cd76d8a6963 languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.2": +"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.2, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.13 resolution: "@jridgewell/gen-mapping@npm:0.3.13" dependencies: @@ -10105,6 +10083,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/remapping@npm:^2.3.5": + version: 2.3.5 + resolution: "@jridgewell/remapping@npm:2.3.5" + dependencies: + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10/c2bb01856e65b506d439455f28aceacf130d6c023d1d4e3b48705e88def3571753e1a887daa04b078b562316c92d26ce36408a60534bceca3f830aec88a339ad + languageName: node + linkType: hard + "@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.1 resolution: "@jridgewell/resolve-uri@npm:3.1.1" @@ -10122,7 +10110,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.13, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15, @jridgewell/sourcemap-codec@npm:^1.5.0": +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15, @jridgewell/sourcemap-codec@npm:^1.5.0": version: 1.5.5 resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" checksum: 10/5d9d207b462c11e322d71911e55e21a4e2772f71ffe8d6f1221b8eb5ae6774458c1d242f897fb0814e8714ca9a6b498abfa74dfe4f434493342902b1a48b33a5 @@ -11143,9 +11131,9 @@ __metadata: languageName: node linkType: hard -"@mswjs/interceptors@npm:^0.37.0": - version: 0.37.1 - resolution: "@mswjs/interceptors@npm:0.37.1" +"@mswjs/interceptors@npm:^0.39.1": + version: 0.39.6 + resolution: "@mswjs/interceptors@npm:0.39.6" dependencies: "@open-draft/deferred-promise": "npm:^2.2.0" "@open-draft/logger": "npm:^0.3.0" @@ -11153,7 +11141,7 @@ __metadata: is-node-process: "npm:^1.2.0" outvariant: "npm:^1.4.3" strict-event-emitter: "npm:^0.5.1" - checksum: 10/332d8aa50beb4834ccbda6a800ca00b1204adc0eba23e1c1f7bb9f4e564a92707e563f7a2424d4a8607404ec91424e5d8c34a87c250b191ca7b24dff12eba2c5 + checksum: 10/c87d3edf08353bde825c87b151b24d538070540ab419206cef1774c932e888af0f920183182fb7c94c3eee42068da5a0a5855853fded8514f33c870921ef37ec languageName: node linkType: hard @@ -18333,277 +18321,70 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-actions@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-actions@npm:8.6.14" - dependencies: - "@storybook/global": "npm:^5.0.0" - "@types/uuid": "npm:^9.0.1" - dequal: "npm:^2.0.2" - polished: "npm:^4.2.2" - uuid: "npm:^9.0.0" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/5d96c1519ffb78ce663612e8ad850523fefe6f8aae3add662ab461015dfdd4e64c62720655526d471893018efdc5049807c3ec49998fb51dfab6cfb5aed30b73 - languageName: node - linkType: hard - -"@storybook/addon-backgrounds@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-backgrounds@npm:8.6.14" - dependencies: - "@storybook/global": "npm:^5.0.0" - memoizerific: "npm:^1.11.3" - ts-dedent: "npm:^2.0.0" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/67343f7b2567b1831db44f3fe6872c9d72261da20eb43390a020ad4ec4420b5e44f1e8c60594bf39ddd0d290a6d05319874167692bd2676615a4f3902b7c897e - languageName: node - linkType: hard - -"@storybook/addon-controls@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-controls@npm:8.6.14" - dependencies: - "@storybook/global": "npm:^5.0.0" - dequal: "npm:^2.0.2" - ts-dedent: "npm:^2.0.0" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/2b63d6fb516886eec1342e18191dfc61f32203313be75bce547f30d69f8bc5084e002aae16275a0c355ffe49d476fe48497738f7b8c50272b1aac0e823b415c6 - languageName: node - linkType: hard - -"@storybook/addon-docs@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-docs@npm:8.6.14" +"@storybook/addon-docs@npm:^9.1.5": + version: 9.1.5 + resolution: "@storybook/addon-docs@npm:9.1.5" dependencies: "@mdx-js/react": "npm:^3.0.0" - "@storybook/blocks": "npm:8.6.14" - "@storybook/csf-plugin": "npm:8.6.14" - "@storybook/react-dom-shim": "npm:8.6.14" + "@storybook/csf-plugin": "npm:9.1.5" + "@storybook/icons": "npm:^1.4.0" + "@storybook/react-dom-shim": "npm:9.1.5" react: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" react-dom: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.6.14 - checksum: 10/a3d08b6418c93776091bd1619f0c821abd5dc620973f0f52025cc342c5d38377455ccd69a1df3f68f7e4e82dc57e039e627a1f4b6a4e856c4eef68a33fe0cbb4 + storybook: ^9.1.5 + checksum: 10/dcc2ed8df4593044667fc2a3bc62be2e6f7395e002eabee218d5376c0d0b38684508988823b5c75973a315ae1821d365879494c52e0c17daa814d9c0992c7ac3 languageName: node linkType: hard -"@storybook/addon-essentials@npm:^8.6.12": - version: 8.6.14 - resolution: "@storybook/addon-essentials@npm:8.6.14" - dependencies: - "@storybook/addon-actions": "npm:8.6.14" - "@storybook/addon-backgrounds": "npm:8.6.14" - "@storybook/addon-controls": "npm:8.6.14" - "@storybook/addon-docs": "npm:8.6.14" - "@storybook/addon-highlight": "npm:8.6.14" - "@storybook/addon-measure": "npm:8.6.14" - "@storybook/addon-outline": "npm:8.6.14" - "@storybook/addon-toolbars": "npm:8.6.14" - "@storybook/addon-viewport": "npm:8.6.14" - ts-dedent: "npm:^2.0.0" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/d32f2bad60736a277f5deef02adde28ec3aa188322760fe6ff4a0d726d1a9286e27e5442f3e7c55580514b0b64b08f3cd37eb12516cb7f035bafd4e26675e3bc - languageName: node - linkType: hard - -"@storybook/addon-highlight@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-highlight@npm:8.6.14" +"@storybook/addon-links@npm:^9.1.5": + version: 9.1.5 + resolution: "@storybook/addon-links@npm:9.1.5" dependencies: "@storybook/global": "npm:^5.0.0" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/bc3f9770c525ace543bd2b1967f11e460c11ff6379c488908d57fd0593cac66171cd5643809cca878559af2db51bb7b7c8556cfa847363b177c40d4e8435629e - languageName: node - linkType: hard - -"@storybook/addon-interactions@npm:^8.6.12": - version: 8.6.14 - resolution: "@storybook/addon-interactions@npm:8.6.14" - dependencies: - "@storybook/global": "npm:^5.0.0" - "@storybook/instrumenter": "npm:8.6.14" - "@storybook/test": "npm:8.6.14" - polished: "npm:^4.2.2" - ts-dedent: "npm:^2.2.0" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/e23724e61be80eae962c90ea60356b9a969f84ecf509a58e2a9bd8845a1fc284488b90d2376f514abc42de947b41231a5a3b470036e01e36d89346b18338fa81 - languageName: node - linkType: hard - -"@storybook/addon-links@npm:^8.6.12": - version: 8.6.14 - resolution: "@storybook/addon-links@npm:8.6.14" - dependencies: - "@storybook/global": "npm:^5.0.0" - ts-dedent: "npm:^2.0.0" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.14 + storybook: ^9.1.5 peerDependenciesMeta: react: optional: true - checksum: 10/d807623e9578f8793a71dc9a3d6988fc22784e4bfebfdd3dc1293cece3e6020caf711cf8fe5171b5ef4340856b50abcddb3a4b89adba0d16582b5f910dbdf884 + checksum: 10/c33630c61a4bbbc74ddfa5f2c9dc5abe310d0de39064df739f4b441208b4df0900edd800ecd8b275f3613a9a8412f8f250dc322ec7fc5aa7aad8c6cf94efae3f languageName: node linkType: hard -"@storybook/addon-measure@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-measure@npm:8.6.14" - dependencies: - "@storybook/global": "npm:^5.0.0" - tiny-invariant: "npm:^1.3.1" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/13fba65df34858fab4976d9dde00ada07a6a80825ec3dc0cf43ccc1fdfbb64d2df4d188a61478b4f648609ddb63de0152ec93666c737bca37dfbb5823b8051fc - languageName: node - linkType: hard - -"@storybook/addon-outline@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-outline@npm:8.6.14" - dependencies: - "@storybook/global": "npm:^5.0.0" - ts-dedent: "npm:^2.0.0" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/8d0351f86cb974fd90cf26041aebbb3683b9ed9b2088d4a3e72939921dde4f81825a3454210b537733ee615c2f113a37f153a510ab8abdd465ab0b97e70abfd0 - languageName: node - linkType: hard - -"@storybook/addon-storysource@npm:^8.6.12": - version: 8.6.14 - resolution: "@storybook/addon-storysource@npm:8.6.14" - dependencies: - "@storybook/source-loader": "npm:8.6.14" - estraverse: "npm:^5.2.0" - tiny-invariant: "npm:^1.3.1" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/46ade211014668fb028f810d4d82fe1ba1ebed7bff89e5e5a96fc189be7d5406ae5598da07b358a77551809b4659a951b025daf732a20f781c4dbfbf487c5716 - languageName: node - linkType: hard - -"@storybook/addon-themes@npm:^8.6.12": - version: 8.6.14 - resolution: "@storybook/addon-themes@npm:8.6.14" +"@storybook/addon-themes@npm:^9.1.5": + version: 9.1.5 + resolution: "@storybook/addon-themes@npm:9.1.5" dependencies: ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.6.14 - checksum: 10/b2f5e813bf95afc9b3c050cd4e06f620392a8cdb2102c60fae26ec6aa61bc11e1a16c5a2ce2bf0eaf6a608411c60c601df65958f0b9ff7af0131e9d5b83d0948 + storybook: ^9.1.5 + checksum: 10/c214cbfe5c94a9c58d0240f6009c62dc3189164dae19679d5503fe8cbf2603cfcf9b58cb500be63c6d92897563cbccef63b9413f14513b1be9f51f8c5b60438e languageName: node linkType: hard -"@storybook/addon-toolbars@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-toolbars@npm:8.6.14" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/890ea37b9270580ea531209d58a3552b87810364ed16f8a6898e339d00a9af6da755421aad2ae6e306ba0272f9cebe2e3d666db6135f7aa3108b2aa3c13bd351 - languageName: node - linkType: hard - -"@storybook/addon-viewport@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/addon-viewport@npm:8.6.14" +"@storybook/builder-vite@npm:9.1.5": + version: 9.1.5 + resolution: "@storybook/builder-vite@npm:9.1.5" dependencies: - memoizerific: "npm:^1.11.3" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/4b3d1ead7fa827de9c97fea41ae4926ab27980dac5651c209a28d8b9fad69799f230e3fafc38b98a1b6d95c0e6b51d5ca00b413da2d7f43eed23fd3c9c794e4e - languageName: node - linkType: hard - -"@storybook/blocks@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/blocks@npm:8.6.14" - dependencies: - "@storybook/icons": "npm:^1.2.12" + "@storybook/csf-plugin": "npm:9.1.5" ts-dedent: "npm:^2.0.0" peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^8.6.14 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - checksum: 10/9bf85f9dd40d8eeb0f012d13a7c88d3d828b680f47c86e8472b76edda3127a87e9dbbf4e08cd8478a855558e5e58204479a08102a97ddf7b834850e760ba3f1c + storybook: ^9.1.5 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + checksum: 10/adf191098abd4685a9451cc9a4da17ecde59e6be951c049add6ab379c26325710d2f15e4191709029b055560bbf59e2845225b85f8d1fd49c1a0d04e82e4d57f languageName: node linkType: hard -"@storybook/builder-vite@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/builder-vite@npm:8.6.14" - dependencies: - "@storybook/csf-plugin": "npm:8.6.14" - browser-assert: "npm:^1.2.1" - ts-dedent: "npm:^2.0.0" - peerDependencies: - storybook: ^8.6.14 - vite: ^4.0.0 || ^5.0.0 || ^6.0.0 - checksum: 10/d49212238277b911c205b06cff12ea0c65158d089a4f29b0e5184c7e484f0b3d2e83845cf99df48b5f427e12b436d69bb9d8f9535e115a0ba8727bd2c8961bc4 - languageName: node - linkType: hard - -"@storybook/components@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/components@npm:8.6.14" - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/d3647505510313aa3c32fd1f8f202eda723ad99bd353b20aa929c5ecab4edc3c86ad06d0eac02f3c6d948e88f13f65ca5fa35ef27c079ef1b92abe8692f23699 - languageName: node - linkType: hard - -"@storybook/core@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/core@npm:8.6.14" - dependencies: - "@storybook/theming": "npm:8.6.14" - better-opn: "npm:^3.0.2" - browser-assert: "npm:^1.2.1" - esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0" - esbuild-register: "npm:^3.5.0" - jsdoc-type-pratt-parser: "npm:^4.0.0" - process: "npm:^0.11.10" - recast: "npm:^0.23.5" - semver: "npm:^7.6.2" - util: "npm:^0.12.5" - ws: "npm:^8.2.3" - peerDependencies: - prettier: ^2 || ^3 - peerDependenciesMeta: - prettier: - optional: true - checksum: 10/8f8c811edd4ea8dcedcc63a79b3168dc83aa0401e3760990cb5995ea6beaac4026c6ccc1046182f41eed7c09333005f3348d8b42f89fc5dd5b2ce011a9d2a48f - languageName: node - linkType: hard - -"@storybook/csf-plugin@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/csf-plugin@npm:8.6.14" +"@storybook/csf-plugin@npm:9.1.5": + version: 9.1.5 + resolution: "@storybook/csf-plugin@npm:9.1.5" dependencies: unplugin: "npm:^1.3.1" peerDependencies: - storybook: ^8.6.14 - checksum: 10/a0983268e6e77ff1bd6b06ad5a895d22ae534fab9cec74960e70a70023dcee44b2d6be4a77ad692a8ecb2e05e3d65bf4ef58dd0bdc52329b0599ba1e093a07c7 - languageName: node - linkType: hard - -"@storybook/csf@npm:^0.1.11": - version: 0.1.13 - resolution: "@storybook/csf@npm:0.1.13" - dependencies: - type-fest: "npm:^2.19.0" - checksum: 10/8a590703c44180798869fd12c1f314cb96de18349415a33bcfe30ef6af11fdc1cdb755ea620dedfd5eb7666cf05af5647b77fe28b63000aa52b53b0dc3c77bb5 + storybook: ^9.1.5 + checksum: 10/f86e178a51d9ea72903392767a7f2fdef98074e251ef90ef50035c9b74f40d9cc62d4ff74079b5e9e0aa9d68d337559beea77f3458ba928b379bfdedda2e5258 languageName: node linkType: hard @@ -18614,7 +18395,7 @@ __metadata: languageName: node linkType: hard -"@storybook/icons@npm:^1.2.12": +"@storybook/icons@npm:^1.4.0": version: 1.4.0 resolution: "@storybook/icons@npm:1.4.0" peerDependencies: @@ -18624,134 +18405,54 @@ __metadata: languageName: node linkType: hard -"@storybook/instrumenter@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/instrumenter@npm:8.6.14" - dependencies: - "@storybook/global": "npm:^5.0.0" - "@vitest/utils": "npm:^2.1.1" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/7e2381ffbbd9065c021892f6f24fbf950aa57857da33a215cd7ffd662b76a282df3da7c664bf3eff14858995e44dc649b4d62069b694d8c69b8de0e43d31efc7 - languageName: node - linkType: hard - -"@storybook/manager-api@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/manager-api@npm:8.6.14" - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/4544b317050b81574f1cd6f911dcb99ee2adbd7190555209171a7dd2233cd331165fa11c16a5977ebe58eeaf26c86bbfcb23f701cfd79a10f0d034dae65197bd - languageName: node - linkType: hard - -"@storybook/preview-api@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/preview-api@npm:8.6.14" - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/9b77288f2f627a7c70cfd3e88bdc4348c1425fab6b333ed77efb913e45881bcdf2ca67f1174e47fe978a7993c2a653fb8accf518ba51441cfb9450145101e9d8 - languageName: node - linkType: hard - -"@storybook/react-dom-shim@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/react-dom-shim@npm:8.6.14" +"@storybook/react-dom-shim@npm:9.1.5": + version: 9.1.5 + resolution: "@storybook/react-dom-shim@npm:9.1.5" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.14 - checksum: 10/d14d970b98dc8266d4df80225538fb537449c15877f0f54465dfe52242f0c3f041c975824cc2894f72a3a8023fdffa0a281d90325b95297858e3684913b3cc15 + storybook: ^9.1.5 + checksum: 10/8aa85b15a2d5584da0af285e52cf78b132fc772846cc2d4369349ecf6d966331ee05a8162c5b1ecf42acefe8d9659f7e0f7cfdd2e1230dc983cb989eefcfb7fe languageName: node linkType: hard -"@storybook/react-vite@npm:^8.6.12": - version: 8.6.14 - resolution: "@storybook/react-vite@npm:8.6.14" +"@storybook/react-vite@npm:^9.1.5": + version: 9.1.5 + resolution: "@storybook/react-vite@npm:9.1.5" dependencies: - "@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.5.0" + "@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.6.1" "@rollup/pluginutils": "npm:^5.0.2" - "@storybook/builder-vite": "npm:8.6.14" - "@storybook/react": "npm:8.6.14" - find-up: "npm:^5.0.0" + "@storybook/builder-vite": "npm:9.1.5" + "@storybook/react": "npm:9.1.5" + find-up: "npm:^7.0.0" magic-string: "npm:^0.30.0" - react-docgen: "npm:^7.0.0" + react-docgen: "npm:^8.0.0" resolve: "npm:^1.22.8" tsconfig-paths: "npm:^4.2.0" peerDependencies: - "@storybook/test": 8.6.14 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.14 - vite: ^4.0.0 || ^5.0.0 || ^6.0.0 - peerDependenciesMeta: - "@storybook/test": - optional: true - checksum: 10/061be89410b3835bb19e82e6770c93dee14cb2fa00616321cdd360e4f808f9927d951bca2e714be255e8b1aaff72dbc1af97e3e7c18878f0685858fd3cc85ec7 + storybook: ^9.1.5 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + checksum: 10/77ed64e8187636299b6b7de1a4db96aa2fed8b5be5cee844bbdeeab64b4b231aeb5c6bc5b84014983da095a4e3be782e5ea167126e7867c26d7c5d47263b20be languageName: node linkType: hard -"@storybook/react@npm:8.6.14, @storybook/react@npm:^8.6.12": - version: 8.6.14 - resolution: "@storybook/react@npm:8.6.14" +"@storybook/react@npm:9.1.5": + version: 9.1.5 + resolution: "@storybook/react@npm:9.1.5" dependencies: - "@storybook/components": "npm:8.6.14" "@storybook/global": "npm:^5.0.0" - "@storybook/manager-api": "npm:8.6.14" - "@storybook/preview-api": "npm:8.6.14" - "@storybook/react-dom-shim": "npm:8.6.14" - "@storybook/theming": "npm:8.6.14" + "@storybook/react-dom-shim": "npm:9.1.5" peerDependencies: - "@storybook/test": 8.6.14 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.14 - typescript: ">= 4.2.x" + storybook: ^9.1.5 + typescript: ">= 4.9.x" peerDependenciesMeta: - "@storybook/test": - optional: true typescript: optional: true - checksum: 10/a8710dcb80da9df4a78cfb3560a8c0c31c6f6c04902aa95446d4e40f6a9179770ead6f9a0a06fa44d00677cd1cd92eb60fdc57f433035ec722275847139671a4 - languageName: node - linkType: hard - -"@storybook/source-loader@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/source-loader@npm:8.6.14" - dependencies: - es-toolkit: "npm:^1.22.0" - estraverse: "npm:^5.2.0" - prettier: "npm:^3.1.1" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/8c96c3e39f19944db297d1ea8a8560b101f3b7be6ab8e2ac1222155454f8fb3ff3a4db3c6646bad514eaf5c28fce51b037f3cc55f34edd419c4d8502caec88ea - languageName: node - linkType: hard - -"@storybook/test@npm:8.6.14, @storybook/test@npm:^8.6.12": - version: 8.6.14 - resolution: "@storybook/test@npm:8.6.14" - dependencies: - "@storybook/global": "npm:^5.0.0" - "@storybook/instrumenter": "npm:8.6.14" - "@testing-library/dom": "npm:10.4.0" - "@testing-library/jest-dom": "npm:6.5.0" - "@testing-library/user-event": "npm:14.5.2" - "@vitest/expect": "npm:2.0.5" - "@vitest/spy": "npm:2.0.5" - peerDependencies: - storybook: ^8.6.14 - checksum: 10/47f65b441389b497a01cd3c3830cc46eb984836bb3c13848799c6074529f5609b284eb16f5473ac9029c7d857def1f13e998325083c4d09bb48a0b171d4221a3 - languageName: node - linkType: hard - -"@storybook/theming@npm:8.6.14": - version: 8.6.14 - resolution: "@storybook/theming@npm:8.6.14" - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/6936ea3348968fe598ad47421c11a78c6ee2ce62336ea1ce9cb8257e9faa2553d3ac3e443f8a36d35a41b0d60eb169231516649c710582ec68fdead4f23ffc0e + checksum: 10/a5b80438976011498998035b6cffb513246dfd6852b035639212a89f8a9114b16757803ce750feb863393af0b9f47ff170d202c5341b083ee4440663976f2939 languageName: node linkType: hard @@ -19513,22 +19214,6 @@ __metadata: languageName: unknown linkType: soft -"@testing-library/dom@npm:10.4.0": - version: 10.4.0 - resolution: "@testing-library/dom@npm:10.4.0" - dependencies: - "@babel/code-frame": "npm:^7.10.4" - "@babel/runtime": "npm:^7.12.5" - "@types/aria-query": "npm:^5.0.1" - aria-query: "npm:5.3.0" - chalk: "npm:^4.1.0" - dom-accessibility-api: "npm:^0.5.9" - lz-string: "npm:^1.5.0" - pretty-format: "npm:^27.0.2" - checksum: 10/05825ee9a15b88cbdae12c137db7111c34069ed3c7a1bd03b6696cb1b37b29f6f2d2de581ebf03033e7df1ab7ebf08399310293f440a4845d95c02c0a9ecc899 - languageName: node - linkType: hard - "@testing-library/dom@npm:^10.0.0": version: 10.4.1 resolution: "@testing-library/dom@npm:10.4.1" @@ -19545,33 +19230,17 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:6.5.0": - version: 6.5.0 - resolution: "@testing-library/jest-dom@npm:6.5.0" - dependencies: - "@adobe/css-tools": "npm:^4.4.0" - aria-query: "npm:^5.0.0" - chalk: "npm:^3.0.0" - css.escape: "npm:^1.5.1" - dom-accessibility-api: "npm:^0.6.3" - lodash: "npm:^4.17.21" - redent: "npm:^3.0.0" - checksum: 10/3d2080888af5fd7306f57448beb5a23f55d965e265b5e53394fffc112dfb0678d616a5274ff0200c46c7618f293520f86fc8562eecd8bdbc0dbb3294d63ec431 - languageName: node - linkType: hard - -"@testing-library/jest-dom@npm:^6.0.0": - version: 6.6.4 - resolution: "@testing-library/jest-dom@npm:6.6.4" +"@testing-library/jest-dom@npm:^6.0.0, @testing-library/jest-dom@npm:^6.6.3": + version: 6.8.0 + resolution: "@testing-library/jest-dom@npm:6.8.0" dependencies: "@adobe/css-tools": "npm:^4.4.0" aria-query: "npm:^5.0.0" css.escape: "npm:^1.5.1" dom-accessibility-api: "npm:^0.6.3" - lodash: "npm:^4.17.21" picocolors: "npm:^1.1.1" redent: "npm:^3.0.0" - checksum: 10/5e67112c789f884fb75b279c2cddfdd0995a012a7847a03c474e4134f0d213934ee70c97433bca26b45e3a5ffa56faafe6499c8e57841179c4f2bd80eef429cd + checksum: 10/d9bebf1f32e46fdde7e12a2b1ee1f8d113b2fb56e86720f185c32ccae2b76e74c76ecaa69c3aeee98a5fded3417b47032891e7ec9be83d4e6bf888ed8356032f languageName: node linkType: hard @@ -19617,16 +19286,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/user-event@npm:14.5.2": - version: 14.5.2 - resolution: "@testing-library/user-event@npm:14.5.2" - peerDependencies: - "@testing-library/dom": ">=7.21.4" - checksum: 10/49821459d81c6bc435d97128d6386ca24f1e4b3ba8e46cb5a96fe3643efa6e002d88c1b02b7f2ec58da593e805c59b78d7fdf0db565c1f02ba782f63ee984040 - languageName: node - linkType: hard - -"@testing-library/user-event@npm:^14.0.0": +"@testing-library/user-event@npm:^14.0.0, @testing-library/user-event@npm:^14.6.1": version: 14.6.1 resolution: "@testing-library/user-event@npm:14.6.1" peerDependencies: @@ -19857,7 +19517,7 @@ __metadata: languageName: node linkType: hard -"@types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.18.0": +"@types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.20.5": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" dependencies: @@ -19889,7 +19549,7 @@ __metadata: languageName: node linkType: hard -"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.0.6, @types/babel__traverse@npm:^7.18.0": +"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.0.6, @types/babel__traverse@npm:^7.20.7": version: 7.28.0 resolution: "@types/babel__traverse@npm:7.28.0" dependencies: @@ -21328,11 +20988,11 @@ __metadata: linkType: hard "@types/set-cookie-parser@npm:^2.4.0": - version: 2.4.3 - resolution: "@types/set-cookie-parser@npm:2.4.3" + version: 2.4.10 + resolution: "@types/set-cookie-parser@npm:2.4.10" dependencies: "@types/node": "npm:*" - checksum: 10/8c0ded364c5a53598dc58f6c668d6fdbefa3bb78fcb1181202b92f4d8495ca33b4317f54ac0fe42824278e789d730ee5cbd2f7f864466e708589ff4eab2bf457 + checksum: 10/105cc90c7d7deeb344858f720b58bd137356586545ac00d1a448e050bfcc0f385553ff26bc9c674bd8c2e953a458149eadb1945ee3d1eee81e6c0656236ebc0a languageName: node linkType: hard @@ -21516,7 +21176,7 @@ __metadata: languageName: node linkType: hard -"@types/tough-cookie@npm:*, @types/tough-cookie@npm:^4.0.5": +"@types/tough-cookie@npm:*": version: 4.0.5 resolution: "@types/tough-cookie@npm:4.0.5" checksum: 10/01fd82efc8202670865928629697b62fe9bf0c0dcbc5b1c115831caeb073a2c0abb871ff393d7df1ae94ea41e256cb87d2a5a91fd03cdb1b0b4384e08d4ee482 @@ -21572,13 +21232,6 @@ __metadata: languageName: node linkType: hard -"@types/uuid@npm:^9.0.1": - version: 9.0.8 - resolution: "@types/uuid@npm:9.0.8" - checksum: 10/b8c60b7ba8250356b5088302583d1704a4e1a13558d143c549c408bf8920535602ffc12394ede77f8a8083511b023704bc66d1345792714002bfa261b17c5275 - languageName: node - linkType: hard - "@types/vinyl@npm:^2.0.4": version: 2.0.6 resolution: "@types/vinyl@npm:2.0.6" @@ -22241,19 +21894,7 @@ __metadata: languageName: node linkType: hard -"@vitest/expect@npm:2.0.5": - version: 2.0.5 - resolution: "@vitest/expect@npm:2.0.5" - dependencies: - "@vitest/spy": "npm:2.0.5" - "@vitest/utils": "npm:2.0.5" - chai: "npm:^5.1.1" - tinyrainbow: "npm:^1.2.0" - checksum: 10/ca9a218f50254b2259fd16166b2d8c9ccc8ee2cc068905e6b3d6281da10967b1590cc7d34b5fa9d429297f97e740450233745583b4cc12272ff11705faf70a37 - languageName: node - linkType: hard - -"@vitest/expect@npm:>1.6.0": +"@vitest/expect@npm:3.2.4, @vitest/expect@npm:>1.6.0": version: 3.2.4 resolution: "@vitest/expect@npm:3.2.4" dependencies: @@ -22266,21 +21907,22 @@ __metadata: languageName: node linkType: hard -"@vitest/pretty-format@npm:2.0.5": - version: 2.0.5 - resolution: "@vitest/pretty-format@npm:2.0.5" +"@vitest/mocker@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/mocker@npm:3.2.4" dependencies: - tinyrainbow: "npm:^1.2.0" - checksum: 10/70bf452dd0b8525e658795125b3f11110bd6baadfaa38c5bb91ca763bded35ec6dc80e27964ad4e91b91be6544d35e18ea7748c1997693988f975a7283c3e9a0 - languageName: node - linkType: hard - -"@vitest/pretty-format@npm:2.1.9": - version: 2.1.9 - resolution: "@vitest/pretty-format@npm:2.1.9" - dependencies: - tinyrainbow: "npm:^1.2.0" - checksum: 10/557dc637c5825abd62ccb15080e59e04d22121e746d8020a0815d7c0c45132fed81b1ff36b26f5991e57a9f1d36e52aa19712abbfe1d0cbcd14252b449a919dc + "@vitest/spy": "npm:3.2.4" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.17" + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10/5e92431b6ed9fc1679060e4caef3e4623f4750542a5d7cd944774f8217c4d231e273202e8aea00bab33260a5a9222ecb7005d80da0348c3c829bd37d123071a8 languageName: node linkType: hard @@ -22293,15 +21935,6 @@ __metadata: languageName: node linkType: hard -"@vitest/spy@npm:2.0.5": - version: 2.0.5 - resolution: "@vitest/spy@npm:2.0.5" - dependencies: - tinyspy: "npm:^3.0.0" - checksum: 10/ed19f4c3bb4d3853241e8070979615138e24403ce4c137fa48c903b3af2c8b3ada2cc26aca9c1aa323bb314a457a8130a29acbb18dafd4e42737deefb2abf1ca - languageName: node - linkType: hard - "@vitest/spy@npm:3.2.4": version: 3.2.4 resolution: "@vitest/spy@npm:3.2.4" @@ -22311,18 +21944,6 @@ __metadata: languageName: node linkType: hard -"@vitest/utils@npm:2.0.5": - version: 2.0.5 - resolution: "@vitest/utils@npm:2.0.5" - dependencies: - "@vitest/pretty-format": "npm:2.0.5" - estree-walker: "npm:^3.0.3" - loupe: "npm:^3.1.1" - tinyrainbow: "npm:^1.2.0" - checksum: 10/d631d56d29c33bc8de631166b2b6691c470187a345469dfef7048befe6027e1c6ff9552f2ee11c8a247522c325c4a64bfcc73f8f0f0c525da39cb9f190f119f8 - languageName: node - linkType: hard - "@vitest/utils@npm:3.2.4": version: 3.2.4 resolution: "@vitest/utils@npm:3.2.4" @@ -22334,17 +21955,6 @@ __metadata: languageName: node linkType: hard -"@vitest/utils@npm:^2.1.1": - version: 2.1.9 - resolution: "@vitest/utils@npm:2.1.9" - dependencies: - "@vitest/pretty-format": "npm:2.1.9" - loupe: "npm:^3.1.2" - tinyrainbow: "npm:^1.2.0" - checksum: 10/83d62d5703a3210a2f137c25dc4e797a7a1d74d5d2e14ecc33b274c7710304fa8b5099101c98bc8d66cc2bf18a14f88ebf21f0996a99d0ee1439ae23b49f3961 - languageName: node - linkType: hard - "@vue/compiler-core@npm:3.5.17": version: 3.5.17 resolution: "@vue/compiler-core@npm:3.5.17" @@ -22600,9 +22210,9 @@ __metadata: linkType: hard "@xmldom/xmldom@npm:^0.8.3": - version: 0.8.10 - resolution: "@xmldom/xmldom@npm:0.8.10" - checksum: 10/62400bc5e0e75b90650e33a5ceeb8d94829dd11f9b260962b71a784cd014ddccec3e603fe788af9c1e839fa4648d8c521ebd80d8b752878d3a40edabc9ce7ccf + version: 0.8.11 + resolution: "@xmldom/xmldom@npm:0.8.11" + checksum: 10/f6d6ffdf71cf19d9b3c10e978fad40d2f85453bf5b2aa05be8aa0c5ad13f84690c3153316729213cc652d06ec12c605ddb0aa03886f1d73d51b974b4105d31e3 languageName: node linkType: hard @@ -25105,13 +24715,6 @@ __metadata: languageName: node linkType: hard -"browser-assert@npm:^1.2.1": - version: 1.2.1 - resolution: "browser-assert@npm:1.2.1" - checksum: 10/8b2407cd04c1ed592cf892dec35942b7d72635829221e0788c9a16c4d2afa8b7156bc9705b1c4b32c30d88136c576fda3cbcb8f494d6f865264c706ea8798d92 - languageName: node - linkType: hard - "browser-headers@npm:^0.4.1": version: 0.4.1 resolution: "browser-headers@npm:0.4.1" @@ -25653,7 +25256,7 @@ __metadata: languageName: node linkType: hard -"chai@npm:^5.1.1, chai@npm:^5.2.0": +"chai@npm:^5.2.0": version: 5.2.1 resolution: "chai@npm:5.2.1" dependencies: @@ -27995,7 +27598,7 @@ __metadata: languageName: node linkType: hard -"dequal@npm:^2.0.0, dequal@npm:^2.0.2, dequal@npm:^2.0.3": +"dequal@npm:^2.0.0, dequal@npm:^2.0.3": version: 2.0.3 resolution: "dequal@npm:2.0.3" checksum: 10/6ff05a7561f33603df87c45e389c9ac0a95e3c056be3da1a0c4702149e3a7f6fe5ffbb294478687ba51a9e95f3a60e8b6b9005993acd79c292c7d15f71964b6b @@ -29084,18 +28687,6 @@ __metadata: languageName: node linkType: hard -"es-toolkit@npm:^1.22.0": - version: 1.39.9 - resolution: "es-toolkit@npm:1.39.9" - dependenciesMeta: - "@trivago/prettier-plugin-sort-imports@4.3.0": - unplugged: true - prettier-plugin-sort-re-exports@0.0.1: - unplugged: true - checksum: 10/39b2fb9173ae2782a31b7bda896809261078744f5c658e5f4a7d7121a221a9e0c0b816610c17fec02629b92556a86e4834c7d6c956de05b09f418cc42dba3936 - languageName: node - linkType: hard - "es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.46, es5-ext@npm:^0.10.50, es5-ext@npm:^0.10.53, es5-ext@npm:^0.10.61, es5-ext@npm:^0.10.62, es5-ext@npm:~0.10.14, es5-ext@npm:~0.10.2, es5-ext@npm:~0.10.46": version: 0.10.63 resolution: "es5-ext@npm:0.10.63" @@ -29620,16 +29211,15 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-storybook@npm:^0.12.0": - version: 0.12.0 - resolution: "eslint-plugin-storybook@npm:0.12.0" +"eslint-plugin-storybook@npm:^9.1.5": + version: 9.1.5 + resolution: "eslint-plugin-storybook@npm:9.1.5" dependencies: - "@storybook/csf": "npm:^0.1.11" "@typescript-eslint/utils": "npm:^8.8.1" - ts-dedent: "npm:^2.2.0" peerDependencies: eslint: ">=8" - checksum: 10/278ea59565e30b74ee1d57f0a8f704906eaf40973b13999ec2c44872bb90c7505dfb12777b264940e2b480e81ace85c0532af69666e76a783b8ffa898a1d49ad + storybook: ^9.1.5 + checksum: 10/e852f8e667f056b07a47182bc2d6e931469e27087df099da3340cfedd61e5bb6e529e0e7c793d1eab318214d34f0ac9c437f96bf1df92e9b3eada39d2a6d2839 languageName: node linkType: hard @@ -30949,6 +30539,17 @@ __metadata: languageName: node linkType: hard +"find-up@npm:^7.0.0": + version: 7.0.0 + resolution: "find-up@npm:7.0.0" + dependencies: + locate-path: "npm:^7.2.0" + path-exists: "npm:^5.0.0" + unicorn-magic: "npm:^0.1.0" + checksum: 10/7e6b08fbc05a10677e25e74bb0a020054a86b31d1806c5e6a9e32e75472bbf177210bc16e5f97453be8bda7ae2e3d97669dbb2901f8c30b39ce53929cbea6746 + languageName: node + linkType: hard + "find-yarn-workspace-root2@npm:1.2.16": version: 1.2.16 resolution: "find-yarn-workspace-root2@npm:1.2.16" @@ -35234,13 +34835,6 @@ __metadata: languageName: node linkType: hard -"jsdoc-type-pratt-parser@npm:^4.0.0": - version: 4.1.0 - resolution: "jsdoc-type-pratt-parser@npm:4.1.0" - checksum: 10/30d88f95f6cbb4a1aa6d4b0d0ae46eb1096e606235ecaf9bab7a3ed5da860516b5d1cd967182765002f292c627526db918f3e56d34637bcf810e6ef84d403f3f - languageName: node - linkType: hard - "jsdom@npm:^20.0.0": version: 20.0.0 resolution: "jsdom@npm:20.0.0" @@ -36455,6 +36049,15 @@ __metadata: languageName: node linkType: hard +"locate-path@npm:^7.2.0": + version: 7.2.0 + resolution: "locate-path@npm:7.2.0" + dependencies: + p-locate: "npm:^6.0.0" + checksum: 10/1c6d269d4efec555937081be964e8a9b4a136319c79ca1d45ac6382212a8466113c75bd89e44521ca8ecd1c47fb08523b56eee5c0712bc7d14fec5f729deeb42 + languageName: node + linkType: hard + "lodash-es@npm:^4.17.21": version: 4.17.21 resolution: "lodash-es@npm:4.17.21" @@ -36899,7 +36502,7 @@ __metadata: languageName: node linkType: hard -"loupe@npm:^3.1.0, loupe@npm:^3.1.1, loupe@npm:^3.1.2, loupe@npm:^3.1.4": +"loupe@npm:^3.1.0, loupe@npm:^3.1.4": version: 3.2.0 resolution: "loupe@npm:3.2.0" checksum: 10/80d48e35b014c2ba5886e25a02ee4cc9c1f659b0eca9c4fa8f07051cdc689e0507a763fbe05a63abbd3b7d4640774a722c905d4b1681b4b92c3ba8f87d96fea2 @@ -37067,15 +36670,6 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.27.0": - version: 0.27.0 - resolution: "magic-string@npm:0.27.0" - dependencies: - "@jridgewell/sourcemap-codec": "npm:^1.4.13" - checksum: 10/10a18a48d22fb14467d6cb4204aba58d6790ae7ba023835dc7a65e310cf216f042a17fab1155ba43e47117310a9b7c3fd3bb79f40be40f5124d6b1af9e96399b - languageName: node - linkType: hard - "magic-string@npm:^0.30.0, magic-string@npm:^0.30.10, magic-string@npm:^0.30.17, magic-string@npm:^0.30.3": version: 0.30.17 resolution: "magic-string@npm:0.30.17" @@ -37215,13 +36809,6 @@ __metadata: languageName: node linkType: hard -"map-or-similar@npm:^1.5.0": - version: 1.5.0 - resolution: "map-or-similar@npm:1.5.0" - checksum: 10/3cf43bcd0e7af41d7bade5f8b5be6bb9d021cc47e6008ad545d071cf3a709ba782884002f9eec6ccd51f572fc17841e07bf74628e0bc3694c33f4622b03e4b4c - languageName: node - linkType: hard - "markdown-escape@npm:^2.0.0": version: 2.0.0 resolution: "markdown-escape@npm:2.0.0" @@ -37626,15 +37213,6 @@ __metadata: languageName: node linkType: hard -"memoizerific@npm:^1.11.3": - version: 1.11.3 - resolution: "memoizerific@npm:1.11.3" - dependencies: - map-or-similar: "npm:^1.5.0" - checksum: 10/72b6b80699777d000f03db6e15fdabcd4afe77feb45be51fe195cb230c64a368fcfcfbb976375eac3283bd8193d6b1a67ac3081cae07f64fca73f1aa568d59e3 - languageName: node - linkType: hard - "merge-descriptors@npm:1.0.3, merge-descriptors@npm:^1.0.1": version: 1.0.3 resolution: "merge-descriptors@npm:1.0.3" @@ -38729,14 +38307,13 @@ __metadata: linkType: hard "msw@npm:^2.0.0, msw@npm:^2.0.8, msw@npm:^2.7.3": - version: 2.7.3 - resolution: "msw@npm:2.7.3" + version: 2.11.2 + resolution: "msw@npm:2.11.2" dependencies: "@bundled-es-modules/cookie": "npm:^2.0.1" "@bundled-es-modules/statuses": "npm:^1.0.1" - "@bundled-es-modules/tough-cookie": "npm:^0.1.6" "@inquirer/confirm": "npm:^5.0.0" - "@mswjs/interceptors": "npm:^0.37.0" + "@mswjs/interceptors": "npm:^0.39.1" "@open-draft/deferred-promise": "npm:^2.2.0" "@open-draft/until": "npm:^2.1.0" "@types/cookie": "npm:^0.6.0" @@ -38747,7 +38324,9 @@ __metadata: outvariant: "npm:^1.4.3" path-to-regexp: "npm:^6.3.0" picocolors: "npm:^1.1.1" + rettime: "npm:^0.7.0" strict-event-emitter: "npm:^0.5.1" + tough-cookie: "npm:^6.0.0" type-fest: "npm:^4.26.1" yargs: "npm:^17.7.2" peerDependencies: @@ -38757,7 +38336,7 @@ __metadata: optional: true bin: msw: cli/index.js - checksum: 10/f193329a68fc22e477a6f8504aa44a92bd12847f2eeac1dfbd8ec1cc43ff293112ec067de1c7fe312ba02beecb313fb00aeeebf5817432b57af2d796b2dff2fa + checksum: 10/cd31627e79bfdc22d6695b5a001804ee4fc7145a157616589c72bdf95c60f1f12c5abee05730a9683e1a68d613c2e767a235fdadc21d30e398a1996c4b699d29 languageName: node linkType: hard @@ -40258,6 +39837,15 @@ __metadata: languageName: node linkType: hard +"p-limit@npm:^4.0.0": + version: 4.0.0 + resolution: "p-limit@npm:4.0.0" + dependencies: + yocto-queue: "npm:^1.0.0" + checksum: 10/01d9d70695187788f984226e16c903475ec6a947ee7b21948d6f597bed788e3112cc7ec2e171c1d37125057a5f45f3da21d8653e04a3a793589e12e9e80e756b + languageName: node + linkType: hard + "p-locate@npm:^3.0.0": version: 3.0.0 resolution: "p-locate@npm:3.0.0" @@ -40285,6 +39873,15 @@ __metadata: languageName: node linkType: hard +"p-locate@npm:^6.0.0": + version: 6.0.0 + resolution: "p-locate@npm:6.0.0" + dependencies: + p-limit: "npm:^4.0.0" + checksum: 10/2bfe5234efa5e7a4e74b30a5479a193fdd9236f8f6b4d2f3f69e3d286d9a7d7ab0c118a2a50142efcf4e41625def635bd9332d6cbf9cc65d85eb0718c579ab38 + languageName: node + linkType: hard + "p-map@npm:^2.0.0": version: 2.1.0 resolution: "p-map@npm:2.1.0" @@ -40794,6 +40391,13 @@ __metadata: languageName: node linkType: hard +"path-exists@npm:^5.0.0": + version: 5.0.0 + resolution: "path-exists@npm:5.0.0" + checksum: 10/8ca842868cab09423994596eb2c5ec2a971c17d1a3cb36dbf060592c730c725cd524b9067d7d2a1e031fef9ba7bd2ac6dc5ec9fb92aa693265f7be3987045254 + languageName: node + linkType: hard + "path-is-absolute@npm:^1.0.0": version: 1.0.1 resolution: "path-is-absolute@npm:1.0.1" @@ -41288,15 +40892,6 @@ __metadata: languageName: node linkType: hard -"polished@npm:^4.2.2": - version: 4.3.1 - resolution: "polished@npm:4.3.1" - dependencies: - "@babel/runtime": "npm:^7.17.8" - checksum: 10/0902fe2eb16aecde1587a00efee7db8081b1331ac7bcfb6e61214d266388723a84858d732ad9395028e0aecd2bb8d0c39cc03d14b4c24c22329a0e40c38141eb - languageName: node - linkType: hard - "pony-cause@npm:^1.1.1": version: 1.1.1 resolution: "pony-cause@npm:1.1.1" @@ -41930,15 +41525,6 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^3.1.1": - version: 3.6.2 - resolution: "prettier@npm:3.6.2" - bin: - prettier: bin/prettier.cjs - checksum: 10/1213691706bcef1371d16ef72773c8111106c3533b660b1cc8ec158bd109cdf1462804125f87f981f23c4a3dba053b6efafda30ab0114cc5b4a725606bb9ff26 - languageName: node - linkType: hard - "pretty-bytes@npm:^5.1.0, pretty-bytes@npm:^5.3.0": version: 5.6.0 resolution: "pretty-bytes@npm:5.6.0" @@ -42829,21 +42415,21 @@ __metadata: languageName: node linkType: hard -"react-docgen@npm:^7.0.0": - version: 7.1.1 - resolution: "react-docgen@npm:7.1.1" +"react-docgen@npm:^8.0.0": + version: 8.0.1 + resolution: "react-docgen@npm:8.0.1" dependencies: - "@babel/core": "npm:^7.18.9" - "@babel/traverse": "npm:^7.18.9" - "@babel/types": "npm:^7.18.9" - "@types/babel__core": "npm:^7.18.0" - "@types/babel__traverse": "npm:^7.18.0" + "@babel/core": "npm:^7.28.0" + "@babel/traverse": "npm:^7.28.0" + "@babel/types": "npm:^7.28.2" + "@types/babel__core": "npm:^7.20.5" + "@types/babel__traverse": "npm:^7.20.7" "@types/doctrine": "npm:^0.0.9" "@types/resolve": "npm:^1.20.2" doctrine: "npm:^3.0.0" resolve: "npm:^1.22.1" strip-indent: "npm:^4.0.0" - checksum: 10/501e5fa0d00e32ee27559f44462a34e9531018ccb46c51efbe60b98a4c077f43dbe8999da5bb91d2ab45a83a34099436a3b725fdabd3f218dbb4493c0b1c9f95 + checksum: 10/6eb9ec3870c0f527fd05b048a842e2338825d99703312d6f4e84f3ae0e5eda427e6b4c7c07a93b21d8740e2be23e779e0237c4b2783160e285f0121711e2f47c languageName: node linkType: hard @@ -44238,6 +43824,13 @@ __metadata: languageName: node linkType: hard +"rettime@npm:^0.7.0": + version: 0.7.0 + resolution: "rettime@npm:0.7.0" + checksum: 10/a8037f2bb4db77ba7a919be008eb8468cd69b23bfc87f5a324919ce441ab5c4560549642e82367bc0cdac01ae2534b680f3abba87e11a13d1123533f17069442 + languageName: node + linkType: hard + "reusify@npm:^1.0.4": version: 1.0.4 resolution: "reusify@npm:1.0.4" @@ -44530,13 +44123,10 @@ __metadata: "@octokit/rest": "npm:^19.0.3" "@playwright/test": "npm:^1.32.3" "@spotify/eslint-plugin": "npm:^15.0.0" - "@storybook/addon-essentials": "npm:^8.6.12" - "@storybook/addon-interactions": "npm:^8.6.12" - "@storybook/addon-links": "npm:^8.6.12" - "@storybook/addon-storysource": "npm:^8.6.12" - "@storybook/addon-themes": "npm:^8.6.12" - "@storybook/react": "npm:^8.6.12" - "@storybook/react-vite": "npm:^8.6.12" + "@storybook/addon-docs": "npm:^9.1.5" + "@storybook/addon-links": "npm:^9.1.5" + "@storybook/addon-themes": "npm:^9.1.5" + "@storybook/react-vite": "npm:^9.1.5" "@techdocs/cli": "workspace:*" "@types/cacheable-request": "npm:^8.3.6" "@types/global-agent": "npm:^2.1.3" @@ -44563,7 +44153,7 @@ __metadata: shx: "npm:^0.4.0" sloc: "npm:^0.3.1" sort-package-json: "npm:^2.8.0" - storybook: "npm:^8.6.12" + storybook: "npm:^9.1.5" typedoc: "npm:^0.28.0" typescript: "npm:~5.7.0" vite: "npm:^7.1.2" @@ -45998,21 +45588,30 @@ __metadata: languageName: node linkType: hard -"storybook@npm:^8.6.12": - version: 8.6.14 - resolution: "storybook@npm:8.6.14" +"storybook@npm:^9.1.5": + version: 9.1.5 + resolution: "storybook@npm:9.1.5" dependencies: - "@storybook/core": "npm:8.6.14" + "@storybook/global": "npm:^5.0.0" + "@testing-library/jest-dom": "npm:^6.6.3" + "@testing-library/user-event": "npm:^14.6.1" + "@vitest/expect": "npm:3.2.4" + "@vitest/mocker": "npm:3.2.4" + "@vitest/spy": "npm:3.2.4" + better-opn: "npm:^3.0.2" + esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0" + esbuild-register: "npm:^3.5.0" + recast: "npm:^0.23.5" + semver: "npm:^7.6.2" + ws: "npm:^8.18.0" peerDependencies: prettier: ^2 || ^3 peerDependenciesMeta: prettier: optional: true bin: - getstorybook: ./bin/index.cjs - sb: ./bin/index.cjs storybook: ./bin/index.cjs - checksum: 10/104932fe29ebf49bef24c90285741cec964d1c36b3f0b38da1dace31ac664be457f2a510e41ba69fe8aa0d90c20e3446fec27d1ced41e5eefb3a9ca713d99e79 + checksum: 10/bf205e9daacbb3570484480a8c3ad0ef36e5cbbecde50126a579678b79194ececd598823e5747ee8a77f6a71ff82a4994f7540ed4d15adae30d66f13928f118c languageName: node linkType: hard @@ -46139,9 +45738,9 @@ __metadata: linkType: hard "strict-event-emitter@npm:^0.4.3": - version: 0.4.3 - resolution: "strict-event-emitter@npm:0.4.3" - checksum: 10/ee335aba8a43bc6749d3337d78b9f6dbe8358d34b6eb38e7075dadf7f1cae621d96588663dda22dce17e6e7c7e281ddac79ca47421da8250bb4bf53b979508a5 + version: 0.4.6 + resolution: "strict-event-emitter@npm:0.4.6" + checksum: 10/abdbf59b6c45b599cc2f227fa473765d1510d155ebd22533e8ecb06110dfacb2ff07aece7fd528dde2b4f9e379d60f2687eee8af3fa2877c3ed88ee5b7ed2707 languageName: node linkType: hard @@ -47217,7 +46816,7 @@ __metadata: languageName: node linkType: hard -"tiny-invariant@npm:^1.0.6, tiny-invariant@npm:^1.3.1, tiny-invariant@npm:^1.3.3": +"tiny-invariant@npm:^1.0.6, tiny-invariant@npm:^1.3.3": version: 1.3.3 resolution: "tiny-invariant@npm:1.3.3" checksum: 10/5e185c8cc2266967984ce3b352a4e57cb89dad5a8abb0dea21468a6ecaa67cd5bb47a3b7a85d08041008644af4f667fb8b6575ba38ba5fb00b3b5068306e59fe @@ -47248,13 +46847,6 @@ __metadata: languageName: node linkType: hard -"tinyrainbow@npm:^1.2.0": - version: 1.2.0 - resolution: "tinyrainbow@npm:1.2.0" - checksum: 10/2924444db6804355e5ba2b6e586c7f77329d93abdd7257a069a0f4530dff9f16de484e80479094e3f39273462541b003a65ee3a6afc2d12555aa745132deba5d - languageName: node - linkType: hard - "tinyrainbow@npm:^2.0.0": version: 2.0.0 resolution: "tinyrainbow@npm:2.0.0" @@ -47262,13 +46854,6 @@ __metadata: languageName: node linkType: hard -"tinyspy@npm:^3.0.0": - version: 3.0.2 - resolution: "tinyspy@npm:3.0.2" - checksum: 10/5db671b2ff5cd309de650c8c4761ca945459d7204afb1776db9a04fb4efa28a75f08517a8620c01ee32a577748802231ad92f7d5b194dc003ee7f987a2a06337 - languageName: node - linkType: hard - "tinyspy@npm:^4.0.3": version: 4.0.3 resolution: "tinyspy@npm:4.0.3" @@ -47283,6 +46868,13 @@ __metadata: languageName: node linkType: hard +"tldts-core@npm:^7.0.14": + version: 7.0.14 + resolution: "tldts-core@npm:7.0.14" + checksum: 10/753b573ea972b9da2deb04df2d2fb4631e33b898cb36506bb4ae0dde272d155f92d19aba3011c296544d1548408ec93289e29ad7d57b9f0bc8de339f7b2ddc4b + languageName: node + linkType: hard + "tldts@npm:^6.1.32": version: 6.1.51 resolution: "tldts@npm:6.1.51" @@ -47294,6 +46886,17 @@ __metadata: languageName: node linkType: hard +"tldts@npm:^7.0.5": + version: 7.0.14 + resolution: "tldts@npm:7.0.14" + dependencies: + tldts-core: "npm:^7.0.14" + bin: + tldts: bin/cli.js + checksum: 10/fbee0768cc35446465c4d2e3c166a7a66b89b033f7b3fc8bfd7e1125eb691d243601ff8efae3f581606db13afd74c172d7fea6b7ce69d1a6acd3d0a1789a3c91 + languageName: node + linkType: hard + "tmp-promise@npm:^3.0.2": version: 3.0.3 resolution: "tmp-promise@npm:3.0.3" @@ -47405,7 +47008,7 @@ __metadata: languageName: node linkType: hard -"tough-cookie@npm:^4.0.0, tough-cookie@npm:^4.1.4": +"tough-cookie@npm:^4.0.0": version: 4.1.4 resolution: "tough-cookie@npm:4.1.4" dependencies: @@ -47426,6 +47029,15 @@ __metadata: languageName: node linkType: hard +"tough-cookie@npm:^6.0.0": + version: 6.0.0 + resolution: "tough-cookie@npm:6.0.0" + dependencies: + tldts: "npm:^7.0.5" + checksum: 10/1b0592241655912eb972e1c284ccf975af154576b8e9912cad4ed7b4b408a60ccfdad1bc53eef10d376f6a5ef9d84e2f8ea0b46c92263d52de855247ff100e27 + languageName: node + linkType: hard + "tr46@npm:^3.0.0": version: 3.0.0 resolution: "tr46@npm:3.0.0" @@ -47577,7 +47189,7 @@ __metadata: languageName: node linkType: hard -"ts-dedent@npm:^2.0.0, ts-dedent@npm:^2.2.0": +"ts-dedent@npm:^2.0.0": version: 2.2.0 resolution: "ts-dedent@npm:2.2.0" checksum: 10/93ed8f7878b6d5ed3c08d99b740010eede6bccfe64bce61c5a4da06a2c17d6ddbb80a8c49c2d15251de7594a4f93ffa21dd10e7be75ef66a4dc9951b4a94e2af @@ -49746,7 +49358,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:*, ws@npm:^8.12.0, ws@npm:^8.13.0, ws@npm:^8.17.1, ws@npm:^8.18.0, ws@npm:^8.2.3, ws@npm:^8.8.0": +"ws@npm:*, ws@npm:^8.12.0, ws@npm:^8.13.0, ws@npm:^8.17.1, ws@npm:^8.18.0, ws@npm:^8.8.0": version: 8.18.3 resolution: "ws@npm:8.18.3" peerDependencies: @@ -50077,6 +49689,13 @@ __metadata: languageName: node linkType: hard +"yocto-queue@npm:^1.0.0": + version: 1.2.1 + resolution: "yocto-queue@npm:1.2.1" + checksum: 10/0843d6c2c0558e5c06e98edf9c17942f25c769e21b519303a5c2adefd5b738c9b2054204dc856ac0cd9d134b1bc27d928ce84fd23c9e2423b7e013d5a6f50577 + languageName: node + linkType: hard + "yoctocolors-cjs@npm:^2.1.2": version: 2.1.2 resolution: "yoctocolors-cjs@npm:2.1.2" From bf5c4c5937a2fe67d922332928df32e940fbb618 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sun, 14 Sep 2025 15:14:20 +0100 Subject: [PATCH 177/233] Fis ast-types Signed-off-by: Charles de Dreuille --- .../patches/ast-types-npm-0.16.1-43c4ac4b0d.patch | 15 +++++++++++++++ package.json | 5 ++++- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 .yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch diff --git a/.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch b/.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch new file mode 100644 index 0000000000..6625880cf0 --- /dev/null +++ b/.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch @@ -0,0 +1,15 @@ +diff --git a/main.d.ts b/main.d.ts +index 6b9a8d05d61821a7e7dc831a52a9f7b505bfee42..1010bb5352d975a171a4019768ff6f6f2967c301 100644 +--- a/main.d.ts ++++ b/main.d.ts +@@ -1,7 +1,7 @@ +-import { ASTNode, Type, AnyType, Field } from "./types"; +-import { NodePath } from "./node-path"; ++import { ASTNode, type Type, AnyType, Field } from "./types"; ++import { type NodePath } from "./node-path"; + import { namedTypes } from "./gen/namedTypes"; +-import { builders } from "./gen/builders"; ++import { type builders } from "./gen/builders"; + import { Visitor } from "./gen/visitor"; + declare const astNodesAreEquivalent: { + (a: any, b: any, problemPath?: any): boolean; diff --git a/package.json b/package.json index a96c13a76a..fd6cf6f0c5 100644 --- a/package.json +++ b/package.json @@ -105,11 +105,14 @@ "@types/react-dom": "^18.0.0", "@yarnpkg/plugin-npm@npm:^3.1.0": "patch:@yarnpkg/plugin-npm@npm%3A3.1.0#~/.yarn/patches/@yarnpkg-plugin-npm-npm-3.1.0-6533d0f5a1.patch", "ast-types@0.14.2": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch", + "ast-types@0.16.1": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", "ast-types@^0.14.1": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch", + "ast-types@^0.16.0": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", "csstype@npm:^3.0.2": "3.0.9", "csstype@npm:^3.1.2": "3.0.9", "csstype@npm:^3.1.3": "3.0.9", - "jest-haste-map@^29.7.0": "patch:jest-haste-map@npm%3A29.7.0#./.yarn/patches/jest-haste-map-npm-29.7.0-e3be419eff.patch" + "jest-haste-map@^29.7.0": "patch:jest-haste-map@npm%3A29.7.0#./.yarn/patches/jest-haste-map-npm-29.7.0-e3be419eff.patch", + "recast@npm:0.23.9>ast-types": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch" }, "dependencies": { "@backstage/errors": "workspace:^", From 7d7049693e7ab25f8e6105e70180c81b85f07721 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sun, 14 Sep 2025 20:26:46 +0100 Subject: [PATCH 178/233] Add Storybook patch Signed-off-by: Charles de Dreuille --- ...storybook-react-npm-9.1.5-2331f18b6b.patch | 35 +++++++++++++++++++ package.json | 3 +- yarn.lock | 18 ++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 .yarn/patches/@storybook-react-npm-9.1.5-2331f18b6b.patch diff --git a/.yarn/patches/@storybook-react-npm-9.1.5-2331f18b6b.patch b/.yarn/patches/@storybook-react-npm-9.1.5-2331f18b6b.patch new file mode 100644 index 0000000000..b8fb44001f --- /dev/null +++ b/.yarn/patches/@storybook-react-npm-9.1.5-2331f18b6b.patch @@ -0,0 +1,35 @@ +diff --git a/dist/preview.d.ts b/dist/preview.d.ts +index 73525b5fe07240d27733b59362cf2076cd28a6ed..0420e6b170a65b6d0db64fcfcfa07b68cd851942 100644 +--- a/dist/preview.d.ts ++++ b/dist/preview.d.ts +@@ -174,7 +174,7 @@ type UnionToIntersection = ( + declare function __definePreview[]>(input: { + addons: Addons; + } & ProjectAnnotations>): ReactPreview>; +-interface ReactPreview extends Preview { ++interface ReactPreview extends Omit, 'meta'> { + meta, TMetaArgs extends Partial>(meta: { + render?: ArgsStoryFn; + component?: ComponentType; +@@ -187,16 +187,13 @@ interface ReactPreview extends Preview { + }>; + } + type DecoratorsArgs = UnionToIntersection ? TArgs : unknown>; +-interface ReactMeta> extends Meta { ++interface ReactMeta extends Omit>, 'story'> { + story ReactTypes['storyResult']) | (StoryAnnotations & { + render: () => ReactTypes['storyResult']; +- })>(story?: TInput): ReactStory ReactTypes['storyResult'] ? { +- render: TInput; +- } : TInput>; +- story, SetOptional>>>(story?: TInput): ReactStory; ++ })>(story?: TInput): ReactStory; ++ story(story?: TInput): ReactStory; + } +-interface ReactStory> extends Story { ++interface ReactStory { + Component: ComponentType>; + } +- + export { ReactPreview, ReactStory, __definePreview }; +\ No newline at end of file diff --git a/package.json b/package.json index fd6cf6f0c5..63ddacee4a 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,8 @@ "csstype@npm:^3.1.2": "3.0.9", "csstype@npm:^3.1.3": "3.0.9", "jest-haste-map@^29.7.0": "patch:jest-haste-map@npm%3A29.7.0#./.yarn/patches/jest-haste-map-npm-29.7.0-e3be419eff.patch", - "recast@npm:0.23.9>ast-types": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch" + "recast@npm:0.23.9>ast-types": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", + "@storybook/react@npm:9.1.5": "patch:@storybook/react@npm%3A9.1.5#~/.yarn/patches/@storybook-react-npm-9.1.5-2331f18b6b.patch" }, "dependencies": { "@backstage/errors": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 3041ed63d5..6fd2ecb8cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18456,6 +18456,24 @@ __metadata: languageName: node linkType: hard +"@storybook/react@patch:@storybook/react@npm%3A9.1.5#~/.yarn/patches/@storybook-react-npm-9.1.5-2331f18b6b.patch": + version: 9.1.5 + resolution: "@storybook/react@patch:@storybook/react@npm%3A9.1.5#~/.yarn/patches/@storybook-react-npm-9.1.5-2331f18b6b.patch::version=9.1.5&hash=8c1145" + dependencies: + "@storybook/global": "npm:^5.0.0" + "@storybook/react-dom-shim": "npm:9.1.5" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.1.5 + typescript: ">= 4.9.x" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/17edd1c58208ca26ed805656fadda3b0797a995afc0d64005ce50b4383dc1b3bbfead2c9c4523f1450ee7b82570b0302acadedb17ea3d8aeacfb1b66f7f436d1 + languageName: node + linkType: hard + "@swagger-api/apidom-ast@npm:^1.0.0-beta.11": version: 1.0.0-beta.11 resolution: "@swagger-api/apidom-ast@npm:1.0.0-beta.11" From 2181f45d4937f39fb8e4173b47f099cf9ff9960d Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sun, 14 Sep 2025 20:52:15 +0100 Subject: [PATCH 179/233] Add more ast-types patches Signed-off-by: Charles de Dreuille --- .../ast-types-npm-0.16.1-596f974e68.patch | 15 ++++++++++++++ package.json | 4 ++++ yarn.lock | 20 +++++++++---------- 3 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 .yarn/patches/ast-types-npm-0.16.1-596f974e68.patch diff --git a/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch b/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch new file mode 100644 index 0000000000..996a23c205 --- /dev/null +++ b/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch @@ -0,0 +1,15 @@ +diff --git a/lib/main.d.ts b/lib/main.d.ts +index 9b54fef0086ea0c99368d8a95f2476e5dcd6d9c1..5c3853901e522c445cdb23b07538aad8a15bafb0 100644 +--- a/lib/main.d.ts ++++ b/lib/main.d.ts +@@ -1,7 +1,7 @@ +-import { ASTNode, Type, AnyType, Field } from "./types"; +-import { NodePath } from "./node-path"; ++import { ASTNode, type Type, AnyType, Field } from "./types"; ++import { type NodePath } from "./node-path"; + import { namedTypes } from "./gen/namedTypes"; +-import { builders } from "./gen/builders"; ++import { type builders } from "./gen/builders"; + import { Visitor } from "./gen/visitor"; + declare const astNodesAreEquivalent: { + (a: any, b: any, problemPath?: any): boolean; diff --git a/package.json b/package.json index 63ddacee4a..7a50acea1b 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,10 @@ "ast-types@0.16.1": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", "ast-types@^0.14.1": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch", "ast-types@^0.16.0": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", + "ast-types@npm:^0.14.1": "patch:ast-types@npm%3A0.16.1#~/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch", + "ast-types@npm:0.14.2": "patch:ast-types@npm%3A0.16.1#~/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch", + "ast-types@npm:^0.16.1": "patch:ast-types@npm%3A0.16.1#~/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch", + "ast-types@npm:^0.13.4": "patch:ast-types@npm%3A0.16.1#~/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch", "csstype@npm:^3.0.2": "3.0.9", "csstype@npm:^3.1.2": "3.0.9", "csstype@npm:^3.1.3": "3.0.9", diff --git a/yarn.lock b/yarn.lock index 6fd2ecb8cf..27a29997b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23888,16 +23888,7 @@ __metadata: languageName: node linkType: hard -"ast-types@npm:^0.13.4": - version: 0.13.4 - resolution: "ast-types@npm:0.13.4" - dependencies: - tslib: "npm:^2.0.1" - checksum: 10/c55b375b9aaf44713d8c0f77a08215ab6d44f368b13e44f2141c421022af3c62b615a30c8ea629457f0cbaec409c713401c0188a124552c8fe4a5ad6b17ff3c3 - languageName: node - linkType: hard - -"ast-types@npm:^0.16.1": +"ast-types@npm:0.16.1": version: 0.16.1 resolution: "ast-types@npm:0.16.1" dependencies: @@ -23915,6 +23906,15 @@ __metadata: languageName: node linkType: hard +"ast-types@patch:ast-types@npm%3A0.16.1#~/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch": + version: 0.16.1 + resolution: "ast-types@patch:ast-types@npm%3A0.16.1#~/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch::version=0.16.1&hash=538845" + dependencies: + tslib: "npm:^2.0.1" + checksum: 10/15c639f8c04116cc3ef0941569549e7cc389b47901ca8b851ad87d3461d9e1dbf85da25a84c2e135864efdcc1bc833b90be216868b7dc7ac4613afcaa98939e0 + languageName: node + linkType: hard + "astral-regex@npm:^2.0.0": version: 2.0.0 resolution: "astral-regex@npm:2.0.0" From ce0ac2c77b571fa6ae21bb0fff6d9605b7ec68bc Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sun, 14 Sep 2025 22:25:06 +0100 Subject: [PATCH 180/233] Update main.ts Signed-off-by: Charles de Dreuille --- .storybook/main.ts | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/.storybook/main.ts b/.storybook/main.ts index 74d84b55ab..1b9e4e2bde 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -42,7 +42,7 @@ const config: StorybookConfig = { name: getAbsolutePath('@storybook/react-vite'), options: {}, }, - viteFinal: async config => { + viteFinal: async (config, { configType }) => { // Add Node.js polyfills for browser compatibility // // When upgrading from Storybook 8 to 9 with the react-vite framework, @@ -61,12 +61,24 @@ const config: StorybookConfig = { // // Without these, Backstage components that rely on Node.js APIs will fail // to load in Storybook's browser environment. - config.define = { - ...config.define, - global: 'globalThis', - 'process.env': '{}', - process: '{ env: {}, browser: true }', - }; + // Different configurations for development vs production + if (configType === 'DEVELOPMENT') { + // Development: Include process polyfill to prevent runtime errors + config.define = { + ...config.define, + global: 'globalThis', + 'process.env': {}, + process: '({ env: {}, browser: true })', + }; + } else if (configType === 'PRODUCTION') { + // Production: Minimal define to avoid esbuild errors + config.define = { + ...config.define, + global: 'globalThis', + 'process.env': {}, + // No process polyfill in production build + }; + } config.resolve = { ...config.resolve, From 289cdc7d59fcc7fc0046f7e2fbc00a99d98dface Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 15 Sep 2025 10:37:18 +0100 Subject: [PATCH 181/233] Cleanup yarn patches Signed-off-by: Charles de Dreuille --- .../patches/ast-types-npm-0.16.1-43c4ac4b0d.patch | 8 ++++---- .../patches/ast-types-npm-0.16.1-596f974e68.patch | 15 --------------- package.json | 8 ++------ yarn.lock | 13 +++++++++++-- 4 files changed, 17 insertions(+), 27 deletions(-) delete mode 100644 .yarn/patches/ast-types-npm-0.16.1-596f974e68.patch diff --git a/.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch b/.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch index 6625880cf0..996a23c205 100644 --- a/.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch +++ b/.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch @@ -1,7 +1,7 @@ -diff --git a/main.d.ts b/main.d.ts -index 6b9a8d05d61821a7e7dc831a52a9f7b505bfee42..1010bb5352d975a171a4019768ff6f6f2967c301 100644 ---- a/main.d.ts -+++ b/main.d.ts +diff --git a/lib/main.d.ts b/lib/main.d.ts +index 9b54fef0086ea0c99368d8a95f2476e5dcd6d9c1..5c3853901e522c445cdb23b07538aad8a15bafb0 100644 +--- a/lib/main.d.ts ++++ b/lib/main.d.ts @@ -1,7 +1,7 @@ -import { ASTNode, Type, AnyType, Field } from "./types"; -import { NodePath } from "./node-path"; diff --git a/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch b/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch deleted file mode 100644 index 996a23c205..0000000000 --- a/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/lib/main.d.ts b/lib/main.d.ts -index 9b54fef0086ea0c99368d8a95f2476e5dcd6d9c1..5c3853901e522c445cdb23b07538aad8a15bafb0 100644 ---- a/lib/main.d.ts -+++ b/lib/main.d.ts -@@ -1,7 +1,7 @@ --import { ASTNode, Type, AnyType, Field } from "./types"; --import { NodePath } from "./node-path"; -+import { ASTNode, type Type, AnyType, Field } from "./types"; -+import { type NodePath } from "./node-path"; - import { namedTypes } from "./gen/namedTypes"; --import { builders } from "./gen/builders"; -+import { type builders } from "./gen/builders"; - import { Visitor } from "./gen/visitor"; - declare const astNodesAreEquivalent: { - (a: any, b: any, problemPath?: any): boolean; diff --git a/package.json b/package.json index 7a50acea1b..531d1422c3 100644 --- a/package.json +++ b/package.json @@ -105,13 +105,9 @@ "@types/react-dom": "^18.0.0", "@yarnpkg/plugin-npm@npm:^3.1.0": "patch:@yarnpkg/plugin-npm@npm%3A3.1.0#~/.yarn/patches/@yarnpkg-plugin-npm-npm-3.1.0-6533d0f5a1.patch", "ast-types@0.14.2": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch", - "ast-types@0.16.1": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", "ast-types@^0.14.1": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch", - "ast-types@^0.16.0": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", - "ast-types@npm:^0.14.1": "patch:ast-types@npm%3A0.16.1#~/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch", - "ast-types@npm:0.14.2": "patch:ast-types@npm%3A0.16.1#~/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch", - "ast-types@npm:^0.16.1": "patch:ast-types@npm%3A0.16.1#~/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch", - "ast-types@npm:^0.13.4": "patch:ast-types@npm%3A0.16.1#~/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch", + "ast-types@npm:0.14.2": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", + "ast-types@npm:^0.16.1": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", "csstype@npm:^3.0.2": "3.0.9", "csstype@npm:^3.1.2": "3.0.9", "csstype@npm:^3.1.3": "3.0.9", diff --git a/yarn.lock b/yarn.lock index 27a29997b3..9152562f09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23897,6 +23897,15 @@ __metadata: languageName: node linkType: hard +"ast-types@npm:^0.13.4": + version: 0.13.4 + resolution: "ast-types@npm:0.13.4" + dependencies: + tslib: "npm:^2.0.1" + checksum: 10/c55b375b9aaf44713d8c0f77a08215ab6d44f368b13e44f2141c421022af3c62b615a30c8ea629457f0cbaec409c713401c0188a124552c8fe4a5ad6b17ff3c3 + languageName: node + linkType: hard + "ast-types@patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch::locator=root%40workspace%3A.": version: 0.14.2 resolution: "ast-types@patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch::version=0.14.2&hash=f42322&locator=root%40workspace%3A." @@ -23906,9 +23915,9 @@ __metadata: languageName: node linkType: hard -"ast-types@patch:ast-types@npm%3A0.16.1#~/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch": +"ast-types@patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch::locator=root%40workspace%3A.": version: 0.16.1 - resolution: "ast-types@patch:ast-types@npm%3A0.16.1#~/.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch::version=0.16.1&hash=538845" + resolution: "ast-types@patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch::version=0.16.1&hash=538845&locator=root%40workspace%3A." dependencies: tslib: "npm:^2.0.1" checksum: 10/15c639f8c04116cc3ef0941569549e7cc389b47901ca8b851ad87d3461d9e1dbf85da25a84c2e135864efdcc1bc833b90be216868b7dc7ac4613afcaa98939e0 From bdcc589842ecdaa29bfe35c0160f10f56c9fe1dc Mon Sep 17 00:00:00 2001 From: Bohdan Liashenko Date: Mon, 15 Sep 2025 11:43:17 +0200 Subject: [PATCH 182/233] Use contenthash name param for webpack caching Signed-off-by: Bohdan Liashenko --- packages/cli/src/modules/build/lib/bundler/config.ts | 4 ++-- packages/cli/src/modules/build/lib/bundler/optimization.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/modules/build/lib/bundler/config.ts b/packages/cli/src/modules/build/lib/bundler/config.ts index bf78d04b4a..9031ce09d2 100644 --- a/packages/cli/src/modules/build/lib/bundler/config.ts +++ b/packages/cli/src/modules/build/lib/bundler/config.ts @@ -430,10 +430,10 @@ export async function createConfig( path: paths.targetDist, publicPath: options.moduleFederation?.mode === 'remote' ? 'auto' : `${publicPath}/`, - filename: isDev ? '[name].js' : 'static/[name].[fullhash:8].js', + filename: isDev ? '[name].js' : 'static/[name].[contenthash:8].js', chunkFilename: isDev ? '[name].chunk.js' - : 'static/[name].[chunkhash:8].chunk.js', + : 'static/[name].[contenthash:8].chunk.js', ...(isDev ? { devtoolModuleFilenameTemplate: (info: any) => diff --git a/packages/cli/src/modules/build/lib/bundler/optimization.ts b/packages/cli/src/modules/build/lib/bundler/optimization.ts index 5d763ea4ac..63319ebec6 100644 --- a/packages/cli/src/modules/build/lib/bundler/optimization.ts +++ b/packages/cli/src/modules/build/lib/bundler/optimization.ts @@ -72,7 +72,7 @@ export const optimization = ( }, filename: isDev ? 'module-[name].js' - : 'static/module-[name].[chunkhash:8].js', + : 'static/module-[name].[contenthash:8].js', priority: 10, minSize: 100000, minChunks: 1, From e6f45dc34ca844547dd8f5b617ed70c4ca12719d Mon Sep 17 00:00:00 2001 From: Bohdan Liashenko Date: Mon, 15 Sep 2025 11:51:20 +0200 Subject: [PATCH 183/233] Add changeset log Signed-off-by: Bohdan Liashenko --- .changeset/all-bats-shine.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/all-bats-shine.md diff --git a/.changeset/all-bats-shine.md b/.changeset/all-bats-shine.md new file mode 100644 index 0000000000..7d9af288c3 --- /dev/null +++ b/.changeset/all-bats-shine.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Change webpack caching config param to `contenthash` for cli bundler. This fixes an issue with webpack sometimes generating same hash name output files even though the content of a file was different. See more at [https://webpack.js.org/guides/caching/](https://webpack.js.org/guides/caching/). From 0efcc977872dae7e2b4242af3d00792ea0e3bc5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 15 Sep 2025 12:07:50 +0200 Subject: [PATCH 184/233] run yarn generate on all packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/large-otters-invent.md | 9 + .../openapi/generated/apis/Api.client.ts | 451 +----------------- .../schema/openapi/generated/apis/index.ts | 2 +- .../src/schema/openapi/generated/index.ts | 2 +- .../AnalyzeLocationEntityField.model.ts | 2 +- .../AnalyzeLocationExistingEntity.model.ts | 2 +- .../AnalyzeLocationGenerateEntity.model.ts | 2 +- .../models/AnalyzeLocationRequest.model.ts | 2 +- .../models/AnalyzeLocationResponse.model.ts | 2 +- .../models/CreateLocation201Response.model.ts | 2 +- .../models/CreateLocationRequest.model.ts | 2 +- .../models/EntitiesBatchResponse.model.ts | 2 +- .../models/EntitiesQueryResponse.model.ts | 2 +- .../EntitiesQueryResponsePageInfo.model.ts | 2 +- .../openapi/generated/models/Entity.model.ts | 2 +- .../models/EntityAncestryResponse.model.ts | 2 +- .../EntityAncestryResponseItemsInner.model.ts | 2 +- .../generated/models/EntityFacet.model.ts | 2 +- .../models/EntityFacetsResponse.model.ts | 2 +- .../generated/models/EntityLink.model.ts | 2 +- .../generated/models/EntityMeta.model.ts | 2 +- .../generated/models/EntityRelation.model.ts | 2 +- .../generated/models/ErrorError.model.ts | 2 +- .../generated/models/ErrorRequest.model.ts | 2 +- .../generated/models/ErrorResponse.model.ts | 2 +- .../models/GetEntitiesByRefsRequest.model.ts | 2 +- .../GetLocations200ResponseInner.model.ts | 2 +- .../generated/models/Location.model.ts | 2 +- .../generated/models/LocationInput.model.ts | 2 +- .../generated/models/LocationSpec.model.ts | 2 +- .../generated/models/ModelError.model.ts | 2 +- .../generated/models/NullableEntity.model.ts | 2 +- .../models/RecursivePartialEntity.model.ts | 2 +- .../RecursivePartialEntityMeta.model.ts | 2 +- .../RecursivePartialEntityMetaAllOf.model.ts | 2 +- .../RecursivePartialEntityRelation.model.ts | 2 +- .../models/RefreshEntityRequest.model.ts | 2 +- .../models/ValidateEntity400Response.model.ts | 2 +- ...idateEntity400ResponseErrorsInner.model.ts | 2 +- .../models/ValidateEntityRequest.model.ts | 2 +- .../schema/openapi/generated/models/index.ts | 2 +- .../src/schema/openapi/generated/pluginId.ts | 2 +- .../src/schema/openapi/index.ts | 2 +- .../openapi/generated/apis/Api.server.ts | 2 +- .../schema/openapi/generated/apis/index.ts | 2 +- .../src/schema/openapi/generated/index.ts | 2 +- .../AnalyzeLocationEntityField.model.ts | 2 +- .../AnalyzeLocationExistingEntity.model.ts | 2 +- .../AnalyzeLocationGenerateEntity.model.ts | 2 +- .../models/AnalyzeLocationRequest.model.ts | 2 +- .../models/AnalyzeLocationResponse.model.ts | 2 +- .../models/CreateLocation201Response.model.ts | 2 +- .../models/CreateLocationRequest.model.ts | 2 +- .../models/EntitiesBatchResponse.model.ts | 2 +- .../models/EntitiesQueryResponse.model.ts | 2 +- .../EntitiesQueryResponsePageInfo.model.ts | 2 +- .../openapi/generated/models/Entity.model.ts | 2 +- .../models/EntityAncestryResponse.model.ts | 2 +- .../EntityAncestryResponseItemsInner.model.ts | 2 +- .../generated/models/EntityFacet.model.ts | 2 +- .../models/EntityFacetsResponse.model.ts | 2 +- .../generated/models/EntityLink.model.ts | 2 +- .../generated/models/EntityMeta.model.ts | 2 +- .../generated/models/EntityRelation.model.ts | 2 +- .../generated/models/ErrorError.model.ts | 2 +- .../generated/models/ErrorRequest.model.ts | 2 +- .../generated/models/ErrorResponse.model.ts | 2 +- .../models/GetEntitiesByRefsRequest.model.ts | 2 +- .../GetLocations200ResponseInner.model.ts | 2 +- .../generated/models/Location.model.ts | 2 +- .../generated/models/LocationInput.model.ts | 2 +- .../generated/models/LocationSpec.model.ts | 2 +- .../generated/models/ModelError.model.ts | 2 +- .../generated/models/NullableEntity.model.ts | 2 +- .../models/RecursivePartialEntity.model.ts | 2 +- .../RecursivePartialEntityMeta.model.ts | 2 +- .../RecursivePartialEntityMetaAllOf.model.ts | 2 +- .../RecursivePartialEntityRelation.model.ts | 2 +- .../models/RefreshEntityRequest.model.ts | 2 +- .../models/ValidateEntity400Response.model.ts | 2 +- ...idateEntity400ResponseErrorsInner.model.ts | 2 +- .../models/ValidateEntityRequest.model.ts | 2 +- .../schema/openapi/generated/models/index.ts | 2 +- .../src/schema/openapi/generated/router.ts | 4 +- .../src/schema/openapi/index.ts | 2 +- .../openapi/generated/apis/Api.server.ts | 2 +- .../schema/openapi/generated/apis/index.ts | 2 +- .../src/schema/openapi/generated/index.ts | 2 +- .../generated/models/ErrorError.model.ts | 2 +- .../generated/models/ErrorRequest.model.ts | 2 +- .../generated/models/ErrorResponse.model.ts | 2 +- .../openapi/generated/models/Event.model.ts | 2 +- .../GetSubscriptionEvents200Response.model.ts | 2 +- .../generated/models/ModelError.model.ts | 2 +- .../models/PostEventRequest.model.ts | 2 +- .../models/PutSubscriptionRequest.model.ts | 2 +- .../schema/openapi/generated/models/index.ts | 2 +- .../src/schema/openapi/generated/router.ts | 2 +- .../src/schema/openapi/index.ts | 2 +- .../openapi/generated/apis/Api.client.ts | 2 +- .../schema/openapi/generated/apis/index.ts | 2 +- .../src/schema/openapi/generated/index.ts | 2 +- .../generated/models/ErrorError.model.ts | 2 +- .../generated/models/ErrorRequest.model.ts | 2 +- .../generated/models/ErrorResponse.model.ts | 2 +- .../openapi/generated/models/Event.model.ts | 2 +- .../GetSubscriptionEvents200Response.model.ts | 2 +- .../generated/models/ModelError.model.ts | 2 +- .../models/PostEventRequest.model.ts | 2 +- .../models/PutSubscriptionRequest.model.ts | 2 +- .../schema/openapi/generated/models/index.ts | 2 +- .../src/schema/openapi/generated/pluginId.ts | 2 +- .../events-node/src/schema/openapi/index.ts | 2 +- .../openapi/generated/apis/Api.server.ts | 2 +- .../schema/openapi/generated/apis/index.ts | 2 +- .../src/schema/openapi/generated/index.ts | 2 +- .../generated/models/ErrorError.model.ts | 2 +- .../generated/models/ErrorRequest.model.ts | 2 +- .../generated/models/ErrorResponse.model.ts | 2 +- .../generated/models/ModelError.model.ts | 2 +- .../models/Query200Response.model.ts | 2 +- .../Query200ResponseResultsInner.model.ts | 2 +- ...ry200ResponseResultsInnerDocument.model.ts | 2 +- .../schema/openapi/generated/models/index.ts | 2 +- .../src/schema/openapi/index.ts | 2 +- 125 files changed, 159 insertions(+), 549 deletions(-) create mode 100644 .changeset/large-otters-invent.md diff --git a/.changeset/large-otters-invent.md b/.changeset/large-otters-invent.md new file mode 100644 index 0000000000..2efaf853a3 --- /dev/null +++ b/.changeset/large-otters-invent.md @@ -0,0 +1,9 @@ +--- +'@backstage/catalog-client': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-events-backend': patch +'@backstage/plugin-search-backend': patch +'@backstage/plugin-events-node': patch +--- + +Updated generated schemas diff --git a/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts b/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts index be1fa78117..a2a751ae2f 100644 --- a/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts +++ b/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts @@ -236,115 +236,14 @@ export class DefaultApiClient { } /** - * Get all entities matching a given filter. - * @param fields - By default the full entities are returned, but you can pass in a `fields` query -parameter which selects what parts of the entity data to retain. This makes the -response smaller and faster to transfer, and may allow the catalog to perform -more efficient queries. - -The query parameter value is a comma separated list of simplified JSON paths -like above. Each path corresponds to the key of either a value, or of a subtree -root that you want to keep in the output. The rest is pruned away. For example, -specifying `?fields=metadata.name,metadata.annotations,spec` retains only the -`name` and `annotations` fields of the `metadata` of each entity (it'll be an -object with at most two keys), keeps the entire `spec` unchanged, and cuts out -all other roots such as `relations`. - -Some more real world usable examples: - -- Return only enough data to form the full ref of each entity: - - `/entities/by-query?fields=kind,metadata.namespace,metadata.name` - - * @param limit - Number of records to return in the response. - * @param filter - You can pass in one or more filter sets that get matched against each entity. -Each filter set is a number of conditions that all have to match for the -condition to be true (conditions effectively have an AND between them). At least -one filter set has to be true for the entity to be part of the result set -(filter sets effectively have an OR between them). - -Example: - -```text -/entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type - - Return entities that match - - Filter set 1: - Condition 1: kind = user - AND - Condition 2: metadata.namespace = default - - OR - - Filter set 2: - Condition 1: kind = group - AND - Condition 2: spec.type exists -``` - -Each condition is either on the form `<key>`, or on the form `<key>=<value>`. -The first form asserts on the existence of a certain key (with any value), and -the second asserts that the key exists and has a certain value. All checks are -always case _insensitive_. - -In all cases, the key is a simplified JSON path in a given piece of entity data. -Each part of the path is a key of an object, and the traversal also descends -through arrays. There are two special forms: - -- Array items that are simple value types (such as strings) match on a key-value - pair where the key is the item as a string, and the value is the string `true` -- Relations can be matched on a `relations.<type>=<targetRef>` form - -Let's look at a simplified example to illustrate the concept: - -```json -{ - "a": { - "b": ["c", { "d": 1 }], - "e": 7 - } -} -``` - -This would match any one of the following conditions: - -- `a` -- `a.b` -- `a.b.c` -- `a.b.c=true` -- `a.b.d` -- `a.b.d=1` -- `a.e` -- `a.e=7` - -Some more real world usable examples: - -- Return all orphaned entities: - - `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - -- Return all users and groups: - - `/entities/by-query?filter=kind=user&filter=kind=group` - -- Return all service components: - - `/entities/by-query?filter=kind=component,spec.type=service` - -- Return all entities with the `java` tag: - - `/entities/by-query?filter=metadata.tags.java` - -- Return all users who are members of the `ops` group (note that the full - [reference](references.md) of the group is used): - - `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` - - * @param offset - Number of records to skip in the query page. - * @param after - Pointer to the previous page of results. - * @param order - - */ + * Get all entities matching a given filter. + * @param fields - By default the full entities are returned, but you can pass in a `fields` query parameter which selects what parts of the entity data to retain. This makes the response smaller and faster to transfer, and may allow the catalog to perform more efficient queries. The query parameter value is a comma separated list of simplified JSON paths like above. Each path corresponds to the key of either a value, or of a subtree root that you want to keep in the output. The rest is pruned away. For example, specifying `?fields=metadata.name,metadata.annotations,spec` retains only the `name` and `annotations` fields of the `metadata` of each entity (it'll be an object with at most two keys), keeps the entire `spec` unchanged, and cuts out all other roots such as `relations`. Some more real world usable examples: - Return only enough data to form the full ref of each entity: `/entities/by-query?fields=kind,metadata.namespace,metadata.name` + * @param limit - Number of records to return in the response. + * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let's look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` + * @param offset - Number of records to skip in the query page. + * @param after - Pointer to the previous page of results. + * @param order - + */ public async getEntities( // @ts-ignore request: GetEntities, @@ -368,148 +267,16 @@ Some more real world usable examples: } /** - * Search for entities by a given query. - * @param fields - By default the full entities are returned, but you can pass in a `fields` query -parameter which selects what parts of the entity data to retain. This makes the -response smaller and faster to transfer, and may allow the catalog to perform -more efficient queries. - -The query parameter value is a comma separated list of simplified JSON paths -like above. Each path corresponds to the key of either a value, or of a subtree -root that you want to keep in the output. The rest is pruned away. For example, -specifying `?fields=metadata.name,metadata.annotations,spec` retains only the -`name` and `annotations` fields of the `metadata` of each entity (it'll be an -object with at most two keys), keeps the entire `spec` unchanged, and cuts out -all other roots such as `relations`. - -Some more real world usable examples: - -- Return only enough data to form the full ref of each entity: - - `/entities/by-query?fields=kind,metadata.namespace,metadata.name` - - * @param limit - Number of records to return in the response. - * @param offset - Number of records to skip in the query page. - * @param orderField - By default the entities are returned ordered by their internal uid. You can -customize the `orderField` query parameters to affect that ordering. - -For example, to return entities by their name: - -`/entities/by-query?orderField=metadata.name,asc` - -Each parameter can be followed by `asc` for ascending lexicographical order or -`desc` for descending (reverse) lexicographical order. - - * @param cursor - You may pass the `cursor` query parameters to perform cursor based pagination -through the set of entities. The value of `cursor` will be returned in the response, under the `pageInfo` property: - -```json - "pageInfo": { - "nextCursor": "a-cursor", - "prevCursor": "another-cursor" - } -``` - -If `nextCursor` exists, it can be used to retrieve the next batch of entities. Following the same approach, -if `prevCursor` exists, it can be used to retrieve the previous batch of entities. - -- [`filter`](#filtering), for selecting only a subset of all entities -- [`fields`](#field-selection), for selecting only parts of the full data - structure of each entity -- `limit` for limiting the number of entities returned (20 is the default) -- [`orderField`](#ordering), for deciding the order of the entities -- `fullTextFilter` - **NOTE**: [`filter`, `orderField`, `fullTextFilter`] and `cursor` are mutually exclusive. This means that, - it isn't possible to change any of [`filter`, `orderField`, `fullTextFilter`] when passing `cursor` as query parameters, - as changing any of these properties will affect pagination. If any of `filter`, `orderField`, `fullTextFilter` is specified together with `cursor`, only the latter is taken into consideration. - - * @param filter - You can pass in one or more filter sets that get matched against each entity. -Each filter set is a number of conditions that all have to match for the -condition to be true (conditions effectively have an AND between them). At least -one filter set has to be true for the entity to be part of the result set -(filter sets effectively have an OR between them). - -Example: - -```text -/entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type - - Return entities that match - - Filter set 1: - Condition 1: kind = user - AND - Condition 2: metadata.namespace = default - - OR - - Filter set 2: - Condition 1: kind = group - AND - Condition 2: spec.type exists -``` - -Each condition is either on the form `<key>`, or on the form `<key>=<value>`. -The first form asserts on the existence of a certain key (with any value), and -the second asserts that the key exists and has a certain value. All checks are -always case _insensitive_. - -In all cases, the key is a simplified JSON path in a given piece of entity data. -Each part of the path is a key of an object, and the traversal also descends -through arrays. There are two special forms: - -- Array items that are simple value types (such as strings) match on a key-value - pair where the key is the item as a string, and the value is the string `true` -- Relations can be matched on a `relations.<type>=<targetRef>` form - -Let's look at a simplified example to illustrate the concept: - -```json -{ - "a": { - "b": ["c", { "d": 1 }], - "e": 7 - } -} -``` - -This would match any one of the following conditions: - -- `a` -- `a.b` -- `a.b.c` -- `a.b.c=true` -- `a.b.d` -- `a.b.d=1` -- `a.e` -- `a.e=7` - -Some more real world usable examples: - -- Return all orphaned entities: - - `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - -- Return all users and groups: - - `/entities/by-query?filter=kind=user&filter=kind=group` - -- Return all service components: - - `/entities/by-query?filter=kind=component,spec.type=service` - -- Return all entities with the `java` tag: - - `/entities/by-query?filter=metadata.tags.java` - -- Return all users who are members of the `ops` group (note that the full - [reference](references.md) of the group is used): - - `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` - - * @param fullTextFilterTerm - Text search term. - * @param fullTextFilterFields - A comma separated list of fields to sort returned results by. - */ + * Search for entities by a given query. + * @param fields - By default the full entities are returned, but you can pass in a `fields` query parameter which selects what parts of the entity data to retain. This makes the response smaller and faster to transfer, and may allow the catalog to perform more efficient queries. The query parameter value is a comma separated list of simplified JSON paths like above. Each path corresponds to the key of either a value, or of a subtree root that you want to keep in the output. The rest is pruned away. For example, specifying `?fields=metadata.name,metadata.annotations,spec` retains only the `name` and `annotations` fields of the `metadata` of each entity (it'll be an object with at most two keys), keeps the entire `spec` unchanged, and cuts out all other roots such as `relations`. Some more real world usable examples: - Return only enough data to form the full ref of each entity: `/entities/by-query?fields=kind,metadata.namespace,metadata.name` + * @param limit - Number of records to return in the response. + * @param offset - Number of records to skip in the query page. + * @param orderField - By default the entities are returned ordered by their internal uid. You can customize the `orderField` query parameters to affect that ordering. For example, to return entities by their name: `/entities/by-query?orderField=metadata.name,asc` Each parameter can be followed by `asc` for ascending lexicographical order or `desc` for descending (reverse) lexicographical order. + * @param cursor - You may pass the `cursor` query parameters to perform cursor based pagination through the set of entities. The value of `cursor` will be returned in the response, under the `pageInfo` property: ```json \"pageInfo\": { \"nextCursor\": \"a-cursor\", \"prevCursor\": \"another-cursor\" } ``` If `nextCursor` exists, it can be used to retrieve the next batch of entities. Following the same approach, if `prevCursor` exists, it can be used to retrieve the previous batch of entities. - [`filter`](#filtering), for selecting only a subset of all entities - [`fields`](#field-selection), for selecting only parts of the full data structure of each entity - `limit` for limiting the number of entities returned (20 is the default) - [`orderField`](#ordering), for deciding the order of the entities - `fullTextFilter` **NOTE**: [`filter`, `orderField`, `fullTextFilter`] and `cursor` are mutually exclusive. This means that, it isn't possible to change any of [`filter`, `orderField`, `fullTextFilter`] when passing `cursor` as query parameters, as changing any of these properties will affect pagination. If any of `filter`, `orderField`, `fullTextFilter` is specified together with `cursor`, only the latter is taken into consideration. + * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let's look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` + * @param fullTextFilterTerm - Text search term. + * @param fullTextFilterFields - A comma separated list of fields to sort returned results by. + */ public async getEntitiesByQuery( // @ts-ignore request: GetEntitiesByQuery, @@ -533,93 +300,10 @@ Some more real world usable examples: } /** - * Get a batch set of entities given an array of entityRefs. - * @param filter - You can pass in one or more filter sets that get matched against each entity. -Each filter set is a number of conditions that all have to match for the -condition to be true (conditions effectively have an AND between them). At least -one filter set has to be true for the entity to be part of the result set -(filter sets effectively have an OR between them). - -Example: - -```text -/entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type - - Return entities that match - - Filter set 1: - Condition 1: kind = user - AND - Condition 2: metadata.namespace = default - - OR - - Filter set 2: - Condition 1: kind = group - AND - Condition 2: spec.type exists -``` - -Each condition is either on the form `<key>`, or on the form `<key>=<value>`. -The first form asserts on the existence of a certain key (with any value), and -the second asserts that the key exists and has a certain value. All checks are -always case _insensitive_. - -In all cases, the key is a simplified JSON path in a given piece of entity data. -Each part of the path is a key of an object, and the traversal also descends -through arrays. There are two special forms: - -- Array items that are simple value types (such as strings) match on a key-value - pair where the key is the item as a string, and the value is the string `true` -- Relations can be matched on a `relations.<type>=<targetRef>` form - -Let's look at a simplified example to illustrate the concept: - -```json -{ - "a": { - "b": ["c", { "d": 1 }], - "e": 7 - } -} -``` - -This would match any one of the following conditions: - -- `a` -- `a.b` -- `a.b.c` -- `a.b.c=true` -- `a.b.d` -- `a.b.d=1` -- `a.e` -- `a.e=7` - -Some more real world usable examples: - -- Return all orphaned entities: - - `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - -- Return all users and groups: - - `/entities/by-query?filter=kind=user&filter=kind=group` - -- Return all service components: - - `/entities/by-query?filter=kind=component,spec.type=service` - -- Return all entities with the `java` tag: - - `/entities/by-query?filter=metadata.tags.java` - -- Return all users who are members of the `ops` group (note that the full - [reference](references.md) of the group is used): - - `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` - - * @param getEntitiesByRefsRequest - - */ + * Get a batch set of entities given an array of entityRefs. + * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let's look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` + * @param getEntitiesByRefsRequest - + */ public async getEntitiesByRefs( // @ts-ignore request: GetEntitiesByRefs, @@ -730,93 +414,10 @@ Some more real world usable examples: } /** - * Get all entity facets that match the given filters. - * @param facet - - * @param filter - You can pass in one or more filter sets that get matched against each entity. -Each filter set is a number of conditions that all have to match for the -condition to be true (conditions effectively have an AND between them). At least -one filter set has to be true for the entity to be part of the result set -(filter sets effectively have an OR between them). - -Example: - -```text -/entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type - - Return entities that match - - Filter set 1: - Condition 1: kind = user - AND - Condition 2: metadata.namespace = default - - OR - - Filter set 2: - Condition 1: kind = group - AND - Condition 2: spec.type exists -``` - -Each condition is either on the form `<key>`, or on the form `<key>=<value>`. -The first form asserts on the existence of a certain key (with any value), and -the second asserts that the key exists and has a certain value. All checks are -always case _insensitive_. - -In all cases, the key is a simplified JSON path in a given piece of entity data. -Each part of the path is a key of an object, and the traversal also descends -through arrays. There are two special forms: - -- Array items that are simple value types (such as strings) match on a key-value - pair where the key is the item as a string, and the value is the string `true` -- Relations can be matched on a `relations.<type>=<targetRef>` form - -Let's look at a simplified example to illustrate the concept: - -```json -{ - "a": { - "b": ["c", { "d": 1 }], - "e": 7 - } -} -``` - -This would match any one of the following conditions: - -- `a` -- `a.b` -- `a.b.c` -- `a.b.c=true` -- `a.b.d` -- `a.b.d=1` -- `a.e` -- `a.e=7` - -Some more real world usable examples: - -- Return all orphaned entities: - - `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - -- Return all users and groups: - - `/entities/by-query?filter=kind=user&filter=kind=group` - -- Return all service components: - - `/entities/by-query?filter=kind=component,spec.type=service` - -- Return all entities with the `java` tag: - - `/entities/by-query?filter=metadata.tags.java` - -- Return all users who are members of the `ops` group (note that the full - [reference](references.md) of the group is used): - - `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` - - */ + * Get all entity facets that match the given filters. + * @param facet - + * @param filter - You can pass in one or more filter sets that get matched against each entity. Each filter set is a number of conditions that all have to match for the condition to be true (conditions effectively have an AND between them). At least one filter set has to be true for the entity to be part of the result set (filter sets effectively have an OR between them). Example: ```text /entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type Return entities that match Filter set 1: Condition 1: kind = user AND Condition 2: metadata.namespace = default OR Filter set 2: Condition 1: kind = group AND Condition 2: spec.type exists ``` Each condition is either on the form `<key>`, or on the form `<key>=<value>`. The first form asserts on the existence of a certain key (with any value), and the second asserts that the key exists and has a certain value. All checks are always case _insensitive_. In all cases, the key is a simplified JSON path in a given piece of entity data. Each part of the path is a key of an object, and the traversal also descends through arrays. There are two special forms: - Array items that are simple value types (such as strings) match on a key-value pair where the key is the item as a string, and the value is the string `true` - Relations can be matched on a `relations.<type>=<targetRef>` form Let's look at a simplified example to illustrate the concept: ```json { \"a\": { \"b\": [\"c\", { \"d\": 1 }], \"e\": 7 } } ``` This would match any one of the following conditions: - `a` - `a.b` - `a.b.c` - `a.b.c=true` - `a.b.d` - `a.b.d=1` - `a.e` - `a.e=7` Some more real world usable examples: - Return all orphaned entities: `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true` - Return all users and groups: `/entities/by-query?filter=kind=user&filter=kind=group` - Return all service components: `/entities/by-query?filter=kind=component,spec.type=service` - Return all entities with the `java` tag: `/entities/by-query?filter=metadata.tags.java` - Return all users who are members of the `ops` group (note that the full [reference](references.md) of the group is used): `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops` + */ public async getEntityFacets( // @ts-ignore request: GetEntityFacets, diff --git a/packages/catalog-client/src/schema/openapi/generated/apis/index.ts b/packages/catalog-client/src/schema/openapi/generated/apis/index.ts index af52f9db46..fc7c83b736 100644 --- a/packages/catalog-client/src/schema/openapi/generated/apis/index.ts +++ b/packages/catalog-client/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/index.ts b/packages/catalog-client/src/schema/openapi/generated/index.ts index bb399e97a0..dc3055033d 100644 --- a/packages/catalog-client/src/schema/openapi/generated/index.ts +++ b/packages/catalog-client/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationEntityField.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationEntityField.model.ts index ad2b5aecd1..65d2a2db65 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationEntityField.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationEntityField.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts index fb01ae8e8e..e7563a5c41 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts index e760d9cf94..dca7b943cc 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts index c1085a2a7b..b91f979678 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts index 66fc46bc0f..40d67804b7 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/CreateLocation201Response.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/CreateLocation201Response.model.ts index 27e557639a..75575079c4 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/CreateLocation201Response.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/CreateLocation201Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/CreateLocationRequest.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/CreateLocationRequest.model.ts index 5351ac289b..183aedc1f8 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/CreateLocationRequest.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/CreateLocationRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts index 1a962367d1..7b02ad53d9 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts index 11c42a6aee..ea67ecb2e5 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponsePageInfo.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponsePageInfo.model.ts index 07b4137c77..8e0fab6f68 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponsePageInfo.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntitiesQueryResponsePageInfo.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/Entity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/Entity.model.ts index f386f3dd17..fed0b96399 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/Entity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/Entity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts index a94f784f05..e48164ea7c 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts index 40594a1515..b82248b5e2 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityFacet.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityFacet.model.ts index f0f89f6eae..36551c0b66 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityFacet.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityFacet.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts index e013f0b5a8..43b10e0965 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityLink.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityLink.model.ts index 53d7d1f9ff..3c406bb785 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityLink.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityLink.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityMeta.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityMeta.model.ts index cc8dd555a6..ea2dedac5c 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityMeta.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityMeta.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/EntityRelation.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/EntityRelation.model.ts index fe98a84481..79313ca658 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/EntityRelation.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/EntityRelation.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ErrorError.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ErrorError.model.ts index fe5811628d..e0265e95d7 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ErrorRequest.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ErrorRequest.model.ts index d44dcb66d9..3eb5e15740 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ErrorResponse.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ErrorResponse.model.ts index 91c120483d..edbcc32df7 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts index 9160f0823b..2308831357 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts index a9f1af1c53..d5f1f1260d 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/Location.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/Location.model.ts index d7e4a22de4..9c6e8fbc96 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/Location.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/Location.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/LocationInput.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/LocationInput.model.ts index 9cdc4c2c13..f86296a14b 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/LocationInput.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/LocationInput.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/LocationSpec.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/LocationSpec.model.ts index 1d56d8f3b0..9286b27d01 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/LocationSpec.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/LocationSpec.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ModelError.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ModelError.model.ts index 5526d703e6..958fde7d0b 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ModelError.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/NullableEntity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/NullableEntity.model.ts index aa6135ded4..fccc37650e 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/NullableEntity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/NullableEntity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts index b3913ccd85..345adaedea 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts index df5ce3811b..da5d7dce1c 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts index 6598f8df96..fc2437efc5 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityRelation.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityRelation.model.ts index 9c7dafb534..6c70398610 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityRelation.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/RecursivePartialEntityRelation.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/RefreshEntityRequest.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/RefreshEntityRequest.model.ts index 617f35288b..b26b7f1c45 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/RefreshEntityRequest.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/RefreshEntityRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts index fac9525afa..532b6fa9e3 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts index 8718d7938d..81cf98faac 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntityRequest.model.ts b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntityRequest.model.ts index c3be04818d..c411697d94 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntityRequest.model.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/ValidateEntityRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/models/index.ts b/packages/catalog-client/src/schema/openapi/generated/models/index.ts index d6c81becb5..0e0d46df01 100644 --- a/packages/catalog-client/src/schema/openapi/generated/models/index.ts +++ b/packages/catalog-client/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/generated/pluginId.ts b/packages/catalog-client/src/schema/openapi/generated/pluginId.ts index b45379f0b0..62afe18e6e 100644 --- a/packages/catalog-client/src/schema/openapi/generated/pluginId.ts +++ b/packages/catalog-client/src/schema/openapi/generated/pluginId.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/catalog-client/src/schema/openapi/index.ts b/packages/catalog-client/src/schema/openapi/index.ts index db98243cbf..196aad553a 100644 --- a/packages/catalog-client/src/schema/openapi/index.ts +++ b/packages/catalog-client/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/apis/Api.server.ts b/plugins/catalog-backend/src/schema/openapi/generated/apis/Api.server.ts index b758377e5f..c10518a08e 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/apis/Api.server.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/apis/Api.server.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/apis/index.ts b/plugins/catalog-backend/src/schema/openapi/generated/apis/index.ts index a3cdbbebd2..8d81cbaf39 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/apis/index.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/index.ts b/plugins/catalog-backend/src/schema/openapi/generated/index.ts index 69c39313c6..dec4b8804e 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/index.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationEntityField.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationEntityField.model.ts index ad2b5aecd1..65d2a2db65 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationEntityField.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationEntityField.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts index fb01ae8e8e..e7563a5c41 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationExistingEntity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts index e760d9cf94..dca7b943cc 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationGenerateEntity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts index c1085a2a7b..b91f979678 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts index 66fc46bc0f..40d67804b7 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/AnalyzeLocationResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocation201Response.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocation201Response.model.ts index 27e557639a..75575079c4 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocation201Response.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocation201Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocationRequest.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocationRequest.model.ts index 5351ac289b..183aedc1f8 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocationRequest.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/CreateLocationRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts index 1a962367d1..7b02ad53d9 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesBatchResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts index 11c42a6aee..ea67ecb2e5 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponsePageInfo.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponsePageInfo.model.ts index 07b4137c77..8e0fab6f68 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponsePageInfo.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntitiesQueryResponsePageInfo.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/Entity.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/Entity.model.ts index f386f3dd17..fed0b96399 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/Entity.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/Entity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts index a94f784f05..e48164ea7c 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts index 40594a1515..b82248b5e2 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityAncestryResponseItemsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacet.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacet.model.ts index f0f89f6eae..36551c0b66 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacet.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacet.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts index e013f0b5a8..43b10e0965 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityFacetsResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityLink.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityLink.model.ts index 53d7d1f9ff..3c406bb785 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityLink.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityLink.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityMeta.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityMeta.model.ts index cc8dd555a6..ea2dedac5c 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityMeta.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityMeta.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityRelation.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityRelation.model.ts index fe98a84481..79313ca658 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/EntityRelation.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/EntityRelation.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorError.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorError.model.ts index fe5811628d..e0265e95d7 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts index d44dcb66d9..3eb5e15740 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts index 91c120483d..edbcc32df7 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts index 9160f0823b..2308831357 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/GetEntitiesByRefsRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts index a9f1af1c53..d5f1f1260d 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/GetLocations200ResponseInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/Location.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/Location.model.ts index d7e4a22de4..9c6e8fbc96 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/Location.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/Location.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/LocationInput.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/LocationInput.model.ts index 9cdc4c2c13..f86296a14b 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/LocationInput.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/LocationInput.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/LocationSpec.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/LocationSpec.model.ts index 1d56d8f3b0..9286b27d01 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/LocationSpec.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/LocationSpec.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/ModelError.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/ModelError.model.ts index 5526d703e6..958fde7d0b 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/ModelError.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/NullableEntity.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/NullableEntity.model.ts index aa6135ded4..fccc37650e 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/NullableEntity.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/NullableEntity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts index b3913ccd85..345adaedea 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntity.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts index df5ce3811b..da5d7dce1c 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMeta.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts index 6598f8df96..fc2437efc5 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityMetaAllOf.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityRelation.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityRelation.model.ts index 9c7dafb534..6c70398610 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityRelation.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/RecursivePartialEntityRelation.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/RefreshEntityRequest.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/RefreshEntityRequest.model.ts index 617f35288b..b26b7f1c45 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/RefreshEntityRequest.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/RefreshEntityRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts index fac9525afa..532b6fa9e3 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts index 8718d7938d..81cf98faac 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntity400ResponseErrorsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntityRequest.model.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntityRequest.model.ts index c3be04818d..c411697d94 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntityRequest.model.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/ValidateEntityRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/models/index.ts b/plugins/catalog-backend/src/schema/openapi/generated/models/index.ts index d6c81becb5..0e0d46df01 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/models/index.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/catalog-backend/src/schema/openapi/generated/router.ts b/plugins/catalog-backend/src/schema/openapi/generated/router.ts index d32ef0a4de..fddec71822 100644 --- a/plugins/catalog-backend/src/schema/openapi/generated/router.ts +++ b/plugins/catalog-backend/src/schema/openapi/generated/router.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ export const spec = { title: 'catalog', version: '1', description: - 'The API surface consists of a few distinct groups of functionality. Each has a\ndedicated section below.\n\n> **Note:** This page only describes some of the most commonly used parts of the\n> API, and is a work in progress.\n\nAll of the URL paths in this article are assumed to be on top of some base URL\npointing at your catalog installation. For example, if the path given in a\nsection below is `/entities`, and the catalog is located at\n`http://localhost:7007/api/catalog` during local development, the full URL would\nbe `http://localhost:7007/api/catalog/entities`. The actual URL may vary from\none organization to the other, especially in production, but is commonly your\n`backend.baseUrl` in your app config, plus `/api/catalog` at the end.\n\nSome or all of the endpoints may accept or require an `Authorization` header\nwith a `Bearer` token, which should then be the Backstage token returned by the\n[`identity API`](https://backstage.io/docs/reference/core-plugin-api.identityapiref).\n', + 'The API surface consists of a few distinct groups of functionality. Each has a\ndedicated section below.\n\n:::note Note \n This page only describes some of the most commonly used parts of the API, and is a work in progress.\n:::\n\nAll of the URL paths in this article are assumed to be on top of some base URL\npointing at your catalog installation. For example, if the path given in a\nsection below is `/entities`, and the catalog is located at\n`http://localhost:7007/api/catalog` during local development, the full URL would\nbe `http://localhost:7007/api/catalog/entities`. The actual URL may vary from\none organization to the other, especially in production, but is commonly your\n`backend.baseUrl` in your app config, plus `/api/catalog` at the end.\n\nSome or all of the endpoints may accept or require an `Authorization` header\nwith a `Bearer` token, which should then be the Backstage token returned by the\n[`identity API`](https://backstage.io/docs/reference/core-plugin-api.identityapiref).\n', license: { name: 'Apache-2.0', url: 'http://www.apache.org/licenses/LICENSE-2.0.html', diff --git a/plugins/catalog-backend/src/schema/openapi/index.ts b/plugins/catalog-backend/src/schema/openapi/index.ts index db98243cbf..196aad553a 100644 --- a/plugins/catalog-backend/src/schema/openapi/index.ts +++ b/plugins/catalog-backend/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/apis/Api.server.ts b/plugins/events-backend/src/schema/openapi/generated/apis/Api.server.ts index 4a1efdfe67..0b116fe012 100644 --- a/plugins/events-backend/src/schema/openapi/generated/apis/Api.server.ts +++ b/plugins/events-backend/src/schema/openapi/generated/apis/Api.server.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/apis/index.ts b/plugins/events-backend/src/schema/openapi/generated/apis/index.ts index a3cdbbebd2..8d81cbaf39 100644 --- a/plugins/events-backend/src/schema/openapi/generated/apis/index.ts +++ b/plugins/events-backend/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/index.ts b/plugins/events-backend/src/schema/openapi/generated/index.ts index 69c39313c6..dec4b8804e 100644 --- a/plugins/events-backend/src/schema/openapi/generated/index.ts +++ b/plugins/events-backend/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/ErrorError.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/ErrorError.model.ts index 0a51c67fef..809cd3fa80 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts index d44dcb66d9..3eb5e15740 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts index 91c120483d..edbcc32df7 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/Event.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/Event.model.ts index edd88b0738..02e52a81d9 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/Event.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/Event.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts index 3ca3282f47..ff15eef3ce 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/ModelError.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/ModelError.model.ts index 3e6af947ec..3914d73531 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/ModelError.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/PostEventRequest.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/PostEventRequest.model.ts index cfaf66ce4f..6bbb2974b5 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/PostEventRequest.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/PostEventRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts b/plugins/events-backend/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts index a1dc298789..786f0a0644 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/models/index.ts b/plugins/events-backend/src/schema/openapi/generated/models/index.ts index 5044f65fff..6baa8ef449 100644 --- a/plugins/events-backend/src/schema/openapi/generated/models/index.ts +++ b/plugins/events-backend/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/generated/router.ts b/plugins/events-backend/src/schema/openapi/generated/router.ts index d2cedd2166..f1b9de2099 100644 --- a/plugins/events-backend/src/schema/openapi/generated/router.ts +++ b/plugins/events-backend/src/schema/openapi/generated/router.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-backend/src/schema/openapi/index.ts b/plugins/events-backend/src/schema/openapi/index.ts index db98243cbf..196aad553a 100644 --- a/plugins/events-backend/src/schema/openapi/index.ts +++ b/plugins/events-backend/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/apis/Api.client.ts b/plugins/events-node/src/schema/openapi/generated/apis/Api.client.ts index c77ddc1d67..71e3767af2 100644 --- a/plugins/events-node/src/schema/openapi/generated/apis/Api.client.ts +++ b/plugins/events-node/src/schema/openapi/generated/apis/Api.client.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/apis/index.ts b/plugins/events-node/src/schema/openapi/generated/apis/index.ts index af52f9db46..fc7c83b736 100644 --- a/plugins/events-node/src/schema/openapi/generated/apis/index.ts +++ b/plugins/events-node/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/index.ts b/plugins/events-node/src/schema/openapi/generated/index.ts index bb399e97a0..dc3055033d 100644 --- a/plugins/events-node/src/schema/openapi/generated/index.ts +++ b/plugins/events-node/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/ErrorError.model.ts b/plugins/events-node/src/schema/openapi/generated/models/ErrorError.model.ts index 0a51c67fef..809cd3fa80 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/ErrorRequest.model.ts b/plugins/events-node/src/schema/openapi/generated/models/ErrorRequest.model.ts index d44dcb66d9..3eb5e15740 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/ErrorResponse.model.ts b/plugins/events-node/src/schema/openapi/generated/models/ErrorResponse.model.ts index 91c120483d..edbcc32df7 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/Event.model.ts b/plugins/events-node/src/schema/openapi/generated/models/Event.model.ts index edd88b0738..02e52a81d9 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/Event.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/Event.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts b/plugins/events-node/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts index 3ca3282f47..ff15eef3ce 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/GetSubscriptionEvents200Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/ModelError.model.ts b/plugins/events-node/src/schema/openapi/generated/models/ModelError.model.ts index 3e6af947ec..3914d73531 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/ModelError.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/PostEventRequest.model.ts b/plugins/events-node/src/schema/openapi/generated/models/PostEventRequest.model.ts index cfaf66ce4f..6bbb2974b5 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/PostEventRequest.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/PostEventRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts b/plugins/events-node/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts index a1dc298789..786f0a0644 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/PutSubscriptionRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/models/index.ts b/plugins/events-node/src/schema/openapi/generated/models/index.ts index 5044f65fff..6baa8ef449 100644 --- a/plugins/events-node/src/schema/openapi/generated/models/index.ts +++ b/plugins/events-node/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/generated/pluginId.ts b/plugins/events-node/src/schema/openapi/generated/pluginId.ts index 36b5433cc3..b5fc50cfc0 100644 --- a/plugins/events-node/src/schema/openapi/generated/pluginId.ts +++ b/plugins/events-node/src/schema/openapi/generated/pluginId.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/events-node/src/schema/openapi/index.ts b/plugins/events-node/src/schema/openapi/index.ts index db98243cbf..196aad553a 100644 --- a/plugins/events-node/src/schema/openapi/index.ts +++ b/plugins/events-node/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/apis/Api.server.ts b/plugins/search-backend/src/schema/openapi/generated/apis/Api.server.ts index d23648d8b5..26652de2cd 100644 --- a/plugins/search-backend/src/schema/openapi/generated/apis/Api.server.ts +++ b/plugins/search-backend/src/schema/openapi/generated/apis/Api.server.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/apis/index.ts b/plugins/search-backend/src/schema/openapi/generated/apis/index.ts index a3cdbbebd2..8d81cbaf39 100644 --- a/plugins/search-backend/src/schema/openapi/generated/apis/index.ts +++ b/plugins/search-backend/src/schema/openapi/generated/apis/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/index.ts b/plugins/search-backend/src/schema/openapi/generated/index.ts index 69c39313c6..dec4b8804e 100644 --- a/plugins/search-backend/src/schema/openapi/generated/index.ts +++ b/plugins/search-backend/src/schema/openapi/generated/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/ErrorError.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/ErrorError.model.ts index 0a51c67fef..809cd3fa80 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/ErrorError.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/ErrorError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts index d44dcb66d9..3eb5e15740 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/ErrorRequest.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts index 91c120483d..edbcc32df7 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/ErrorResponse.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/ModelError.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/ModelError.model.ts index 3e6af947ec..3914d73531 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/ModelError.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/ModelError.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/Query200Response.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/Query200Response.model.ts index 8ddb2a0b63..190ab8dc59 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/Query200Response.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/Query200Response.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInner.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInner.model.ts index ec635c8e5b..7a1c9c155a 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInner.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInner.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInnerDocument.model.ts b/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInnerDocument.model.ts index 9e82274aa7..004ce76587 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInnerDocument.model.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/Query200ResponseResultsInnerDocument.model.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/generated/models/index.ts b/plugins/search-backend/src/schema/openapi/generated/models/index.ts index d38087bf8c..fc9f1408f7 100644 --- a/plugins/search-backend/src/schema/openapi/generated/models/index.ts +++ b/plugins/search-backend/src/schema/openapi/generated/models/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugins/search-backend/src/schema/openapi/index.ts b/plugins/search-backend/src/schema/openapi/index.ts index db98243cbf..196aad553a 100644 --- a/plugins/search-backend/src/schema/openapi/index.ts +++ b/plugins/search-backend/src/schema/openapi/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 259a3c5b2a11917c910130fbf3d2526ade98daa5 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 15 Sep 2025 11:12:03 +0100 Subject: [PATCH 185/233] Fixes ast-types Signed-off-by: Charles de Dreuille --- package.json | 1 + yarn.lock | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/package.json b/package.json index 531d1422c3..b8f36ed234 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "@yarnpkg/plugin-npm@npm:^3.1.0": "patch:@yarnpkg/plugin-npm@npm%3A3.1.0#~/.yarn/patches/@yarnpkg-plugin-npm-npm-3.1.0-6533d0f5a1.patch", "ast-types@0.14.2": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch", "ast-types@^0.14.1": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch", + "ast-types@npm:^0.13.4": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", "ast-types@npm:0.14.2": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", "ast-types@npm:^0.16.1": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", "csstype@npm:^3.0.2": "3.0.9", diff --git a/yarn.lock b/yarn.lock index 9152562f09..3a5aa9ac30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23897,15 +23897,6 @@ __metadata: languageName: node linkType: hard -"ast-types@npm:^0.13.4": - version: 0.13.4 - resolution: "ast-types@npm:0.13.4" - dependencies: - tslib: "npm:^2.0.1" - checksum: 10/c55b375b9aaf44713d8c0f77a08215ab6d44f368b13e44f2141c421022af3c62b615a30c8ea629457f0cbaec409c713401c0188a124552c8fe4a5ad6b17ff3c3 - languageName: node - linkType: hard - "ast-types@patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch::locator=root%40workspace%3A.": version: 0.14.2 resolution: "ast-types@patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch::version=0.14.2&hash=f42322&locator=root%40workspace%3A." From 4a77f7bd01167faeecfcc1000adcefa5e6d18449 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Sep 2025 12:16:19 +0200 Subject: [PATCH 186/233] Update .changeset/all-bats-shine.md Signed-off-by: Patrik Oldsberg --- .changeset/all-bats-shine.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/all-bats-shine.md b/.changeset/all-bats-shine.md index 7d9af288c3..68d86c8d15 100644 --- a/.changeset/all-bats-shine.md +++ b/.changeset/all-bats-shine.md @@ -2,4 +2,4 @@ '@backstage/cli': patch --- -Change webpack caching config param to `contenthash` for cli bundler. This fixes an issue with webpack sometimes generating same hash name output files even though the content of a file was different. See more at [https://webpack.js.org/guides/caching/](https://webpack.js.org/guides/caching/). +Updated the WebPack configuration to use `contenthash`. This fixes an issue were builds would sometimes generate output files with the same name but different content across builds, leading to breakages when loading the frontend app. From 0ffa4c7955aaf8d96bf869ab5564220ce976a883 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Sep 2025 12:09:46 +0200 Subject: [PATCH 187/233] ui: fix matchMedia fallback Signed-off-by: Patrik Oldsberg --- .changeset/shy-chicken-smash.md | 5 +++++ .github/vale/config/vocabularies/Backstage/accept.txt | 1 + packages/ui/src/hooks/useMediaQuery.ts | 6 +++++- 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 .changeset/shy-chicken-smash.md diff --git a/.changeset/shy-chicken-smash.md b/.changeset/shy-chicken-smash.md new file mode 100644 index 0000000000..7524a50580 --- /dev/null +++ b/.changeset/shy-chicken-smash.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Removed the need to mock `window.matchMedia` in tests, falling back to default breakpoint values instead. diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index 516eec516f..b0225be547 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -38,6 +38,7 @@ Bitrise Blackbox bool boolean +breakpoint Brex broadcasted bugfixes diff --git a/packages/ui/src/hooks/useMediaQuery.ts b/packages/ui/src/hooks/useMediaQuery.ts index 9e2cc9ef0b..659ea4a418 100644 --- a/packages/ui/src/hooks/useMediaQuery.ts +++ b/packages/ui/src/hooks/useMediaQuery.ts @@ -22,7 +22,8 @@ type UseMediaQueryOptions = { initializeWithValue?: boolean; }; -const IS_SERVER = typeof window === 'undefined'; +const IS_SERVER = + typeof window === 'undefined' || typeof window.matchMedia === 'undefined'; export function useMediaQuery( query: string, @@ -51,6 +52,9 @@ export function useMediaQuery( } useIsomorphicLayoutEffect(() => { + if (IS_SERVER) { + return; + } const matchMedia = window.matchMedia(query); // Triggered at the first client-side load and if query changes From 15575563f7a38907d6c07a36e05ed97150a77431 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 5 Sep 2025 19:10:55 +0200 Subject: [PATCH 188/233] catalog-backend: add support for type in catalog.rules.allow Signed-off-by: Vincenzo Scamporlino --- .../src/ingestion/CatalogRules.test.ts | 38 ++++++++++++++++++- .../src/ingestion/CatalogRules.ts | 29 +++++++++++--- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts index 19c231884c..67d1e86248 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts @@ -27,8 +27,14 @@ const entity = { kind: 'Group', } as Entity, component: { - kind: 'component', + kind: 'Component', } as Entity, + componentWithType: { + kind: 'Component', + spec: { + type: 'service', + }, + } as unknown as Entity, location: { kind: 'Location', } as Entity, @@ -164,6 +170,9 @@ describe('DefaultCatalogRulesEnforcer', () => { expect(enforcer.isAllowed(entity.user, location.x)).toBe(false); expect(enforcer.isAllowed(entity.group, location.y)).toBe(false); expect(enforcer.isAllowed(entity.component, location.z)).toBe(true); + expect(enforcer.isAllowed(entity.componentWithType, location.z)).toBe( + true, + ); expect(enforcer.isAllowed(entity.location, location.z)).toBe(true); }); @@ -268,6 +277,33 @@ describe('DefaultCatalogRulesEnforcer', () => { }); }); + it('should only allow components with a specific type', () => { + const enforcer = DefaultCatalogRulesEnforcer.fromConfig( + new ConfigReader({ + catalog: { + rules: [ + { + allow: [{ kind: 'Component', type: 'service' }], + locations: [{ type: 'url', pattern: 'https://github.com/b/**' }], + }, + ], + }, + }), + ); + + expect(enforcer.isAllowed(entity.component, location.w)).toBe(false); + expect(enforcer.isAllowed(entity.component, location.y)).toBe(false); + expect(enforcer.isAllowed(entity.component, location.z)).toBe(false); + + expect(enforcer.isAllowed(entity.componentWithType, location.w)).toBe(true); + expect(enforcer.isAllowed(entity.componentWithType, location.y)).toBe( + false, + ); + expect(enforcer.isAllowed(entity.componentWithType, location.z)).toBe( + false, + ); + }); + it('should allow locations with a hidden folder', () => { const enforcer = DefaultCatalogRulesEnforcer.fromConfig( new ConfigReader({ diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index 8fb237df30..ddb6398bf6 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -26,9 +26,7 @@ import { minimatch } from 'minimatch'; * An undefined list of matchers means match all, an empty list of matchers means match none. */ export type CatalogRule = { - allow: Array<{ - kind: string; - }>; + allow: CatalogRuleAllow[]; locations?: Array<{ exact?: string; type: string; @@ -36,6 +34,11 @@ export type CatalogRule = { }>; }; +type CatalogRuleAllow = { + kind: string; + type?: string; +}; + /** * Decides whether an entity from a given location is allowed to enter the * catalog, according to some rule set. @@ -78,6 +81,9 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { * catalog: * rules: * - allow: [Component, API] + * - allow: + * - kind: Resource + * type: database * - allow: [Template] * locations: * - type: url @@ -105,7 +111,14 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { const globalRules = config .getConfigArray('catalog.rules') .map(ruleConf => ({ - allow: ruleConf.getStringArray('allow').map(kind => ({ kind })), + allow: ruleConf + .get>('allow') + .map(kindOrObject => { + if (typeof kindOrObject === 'string') { + return { kind: kindOrObject }; + } + return kindOrObject; + }), locations: ruleConf .getOptionalConfigArray('locations') ?.map(locationConfig => { @@ -199,13 +212,17 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { return false; } - private matchEntity(entity: Entity, matchers?: { kind: string }[]): boolean { + private matchEntity(entity: Entity, matchers?: CatalogRuleAllow[]): boolean { if (!matchers) { return true; } for (const matcher of matchers) { - if (entity?.kind?.toLowerCase() !== matcher.kind.toLowerCase()) { + if (entity.kind?.toLowerCase() !== matcher.kind.toLowerCase()) { + continue; + } + + if (matcher.type && matcher.type !== entity.spec?.type) { continue; } From 36694fb7749c2b8f2db8a918a907688f27fded66 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 5 Sep 2025 19:12:33 +0200 Subject: [PATCH 189/233] catalog-backend: allow type to catalog.rules.allow Signed-off-by: Vincenzo Scamporlino --- plugins/catalog-backend/config.d.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 6329bcb111..d346f01e37 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -38,8 +38,11 @@ export interface Config { * Allow entities of these particular kinds. * * E.g. ["Component", "API", "Template", "Location"] + * + * You can also specify the type of the entity by using an object with `kind` and optional `type` properties. + * E.g. [{ kind: "Component", type: "service" }] */ - allow: Array; + allow: Array; /** * Limit this rule to a specific location * From 9b40a55ded2b3a5f5819d57d573d9e0993c34c49 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 5 Sep 2025 19:12:59 +0200 Subject: [PATCH 190/233] catalog-backend: type changeset Signed-off-by: Vincenzo Scamporlino --- .changeset/rotten-hairs-attack.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .changeset/rotten-hairs-attack.md diff --git a/.changeset/rotten-hairs-attack.md b/.changeset/rotten-hairs-attack.md new file mode 100644 index 0000000000..1b89ad46e7 --- /dev/null +++ b/.changeset/rotten-hairs-attack.md @@ -0,0 +1,25 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Add support for specifying an entity `type` in `catalog.rules.allow` rules within the catalog configuration. + +For example, this enables allowing all `Template` entities with the type `website`: + +```diff + catalog: + rules: + - allow: + - Component + - API + - Resource + - System + - Domain + - Location ++ - allow: ++ - kind: Template ++ type: website + locations: + - type: url + pattern: https://github.com/org/*\/blob/master/*.yaml +``` From 77980e3f80b0e0001ddf78331ba9abe509000e81 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 10 Sep 2025 09:37:31 +0200 Subject: [PATCH 191/233] catalog-backend: rename type to spec.type Signed-off-by: Vincenzo Scamporlino --- .changeset/rotten-hairs-attack.md | 4 ++-- plugins/catalog-backend/config.d.ts | 2 +- plugins/catalog-backend/src/ingestion/CatalogRules.test.ts | 2 +- plugins/catalog-backend/src/ingestion/CatalogRules.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.changeset/rotten-hairs-attack.md b/.changeset/rotten-hairs-attack.md index 1b89ad46e7..7754441f78 100644 --- a/.changeset/rotten-hairs-attack.md +++ b/.changeset/rotten-hairs-attack.md @@ -2,7 +2,7 @@ '@backstage/plugin-catalog-backend': minor --- -Add support for specifying an entity `type` in `catalog.rules.allow` rules within the catalog configuration. +Add support for specifying an entity `spec.type` in `catalog.rules.allow` rules within the catalog configuration. For example, this enables allowing all `Template` entities with the type `website`: @@ -18,7 +18,7 @@ For example, this enables allowing all `Template` entities with the type `websit - Location + - allow: + - kind: Template -+ type: website ++ spec.type: website locations: - type: url pattern: https://github.com/org/*\/blob/master/*.yaml diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index d346f01e37..4b349405cc 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -42,7 +42,7 @@ export interface Config { * You can also specify the type of the entity by using an object with `kind` and optional `type` properties. * E.g. [{ kind: "Component", type: "service" }] */ - allow: Array; + allow: Array; /** * Limit this rule to a specific location * diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts index 67d1e86248..a28ceb0d48 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts @@ -283,7 +283,7 @@ describe('DefaultCatalogRulesEnforcer', () => { catalog: { rules: [ { - allow: [{ kind: 'Component', type: 'service' }], + allow: [{ kind: 'Component', 'spec.type': 'service' }], locations: [{ type: 'url', pattern: 'https://github.com/b/**' }], }, ], diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index ddb6398bf6..b348f5b752 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -36,7 +36,7 @@ export type CatalogRule = { type CatalogRuleAllow = { kind: string; - type?: string; + 'spec.type'?: string; }; /** @@ -222,7 +222,7 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { continue; } - if (matcher.type && matcher.type !== entity.spec?.type) { + if (matcher['spec.type'] && matcher['spec.type'] !== entity.spec?.type) { continue; } From 3c3466dd51cecba1b650f33f2610a9fe8fe2032f Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 11 Sep 2025 20:33:46 +0200 Subject: [PATCH 192/233] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Vincenzo Scamporlino --- .changeset/rotten-hairs-attack.md | 2 +- plugins/catalog-backend/config.d.ts | 4 ++-- plugins/catalog-backend/src/ingestion/CatalogRules.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/rotten-hairs-attack.md b/.changeset/rotten-hairs-attack.md index 7754441f78..aa6dabc78c 100644 --- a/.changeset/rotten-hairs-attack.md +++ b/.changeset/rotten-hairs-attack.md @@ -2,7 +2,7 @@ '@backstage/plugin-catalog-backend': minor --- -Add support for specifying an entity `spec.type` in `catalog.rules.allow` rules within the catalog configuration. +Add support for specifying an entity `spec.type` in `catalog.rules` and `catalog.locations.rules` within the catalog configuration. For example, this enables allowing all `Template` entities with the type `website`: diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 4b349405cc..a506476dbf 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -39,8 +39,8 @@ export interface Config { * * E.g. ["Component", "API", "Template", "Location"] * - * You can also specify the type of the entity by using an object with `kind` and optional `type` properties. - * E.g. [{ kind: "Component", type: "service" }] + * You can also specify the type of the entity by using an object with `kind` and optional `spec.type` properties. + * E.g. [{ kind: "Component", 'spec.type': "service" }] */ allow: Array; /** diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index b348f5b752..ea4e520177 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -83,7 +83,7 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { * - allow: [Component, API] * - allow: * - kind: Resource - * type: database + * 'spec.type': database * - allow: [Template] * locations: * - type: url From 0364b9f107cc1be37e4342d555483b35b5eddbfa Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 11 Sep 2025 20:41:09 +0200 Subject: [PATCH 193/233] catalog-backend: improve validation Signed-off-by: Vincenzo Scamporlino --- .../src/ingestion/CatalogRules.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index ea4e520177..f6026a84cd 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -19,6 +19,7 @@ import { Entity } from '@backstage/catalog-model'; import path from 'path'; import { LocationSpec } from '@backstage/plugin-catalog-common'; import { minimatch } from 'minimatch'; +import { z } from 'zod'; /** * Rules to apply to catalog entities. @@ -47,6 +48,16 @@ export type CatalogRulesEnforcer = { isAllowed(entity: Entity, location: LocationSpec): boolean; }; +const allowRuleParser = z.array( + z + .object({ + kind: z.string(), + 'spec.type': z.string().optional(), + }) + .or(z.string()) + .transform(val => (typeof val === 'string' ? { kind: val } : val)), +); + /** * Implements the default catalog rule set, consuming the config keys * `catalog.rules` and `catalog.locations.[].rules`. @@ -111,14 +122,7 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { const globalRules = config .getConfigArray('catalog.rules') .map(ruleConf => ({ - allow: ruleConf - .get>('allow') - .map(kindOrObject => { - if (typeof kindOrObject === 'string') { - return { kind: kindOrObject }; - } - return kindOrObject; - }), + allow: allowRuleParser.parse(ruleConf.get('allow')), locations: ruleConf .getOptionalConfigArray('locations') ?.map(locationConfig => { From 62bb3b75d076389678c1538b5bf2a55fbbd7ccf2 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Mon, 15 Sep 2025 11:50:34 +0200 Subject: [PATCH 194/233] catalog-backend: make type comparison lowercase Signed-off-by: Vincenzo Scamporlino --- .../src/ingestion/CatalogRules.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index f6026a84cd..15e620a443 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -222,12 +222,23 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { } for (const matcher of matchers) { - if (entity.kind?.toLowerCase() !== matcher.kind.toLowerCase()) { + if ( + entity.kind?.toLocaleLowerCase('en-US') !== + matcher.kind.toLocaleLowerCase('en-US') + ) { continue; } - if (matcher['spec.type'] && matcher['spec.type'] !== entity.spec?.type) { - continue; + if (matcher['spec.type']) { + if (typeof entity.spec?.type !== 'string') { + continue; + } + if ( + matcher['spec.type'].toLocaleLowerCase('en-US') !== + entity.spec.type.toLocaleLowerCase('en-US') + ) { + continue; + } } return true; From 7e7ed57de26e89c410787e523086e0948280dfeb Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Tue, 5 Aug 2025 13:24:21 +0300 Subject: [PATCH 195/233] feat(notifications): extension point to modify user resolving this change adds a new extension point method that can be used to pass a custom function that can modify how individual user entity references are resolved inside the notifications backend. Signed-off-by: Hellgren Heikki --- .changeset/twenty-shoes-tickle.md | 9 ++ plugins/notifications-backend/src/plugin.ts | 18 ++++ .../src/service/router.test.ts | 85 +++++++++++++++++++ .../src/service/router.ts | 30 +++++-- plugins/notifications-node/report.api.md | 10 +++ plugins/notifications-node/src/extensions.ts | 16 ++++ 6 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 .changeset/twenty-shoes-tickle.md diff --git a/.changeset/twenty-shoes-tickle.md b/.changeset/twenty-shoes-tickle.md new file mode 100644 index 0000000000..d13d93adde --- /dev/null +++ b/.changeset/twenty-shoes-tickle.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-notifications-backend': patch +'@backstage/plugin-notifications-node': patch +--- + +A new extension point method was added that can be used to modify how the users receiving notifications +are resolved. The function passed to the extension point should only return complete user entity references +based on the notification target references and the excluded entity references. Note that the input is a list +of entity references that can be any entity kind, not just user entities. diff --git a/plugins/notifications-backend/src/plugin.ts b/plugins/notifications-backend/src/plugin.ts index 5f430318b5..49b3924a68 100644 --- a/plugins/notifications-backend/src/plugin.ts +++ b/plugins/notifications-backend/src/plugin.ts @@ -22,6 +22,7 @@ import { createRouter } from './service/router'; import { signalsServiceRef } from '@backstage/plugin-signals-node'; import { NotificationProcessor, + NotificationRecipientResolver, notificationsProcessingExtensionPoint, NotificationsProcessingExtensionPoint, } from '@backstage/plugin-notifications-node'; @@ -33,6 +34,7 @@ class NotificationsProcessingExtensionPointImpl implements NotificationsProcessingExtensionPoint { #processors = new Array(); + #recipientResolver: NotificationRecipientResolver | undefined = undefined; addProcessor( ...processors: Array> @@ -43,6 +45,21 @@ class NotificationsProcessingExtensionPointImpl get processors() { return this.#processors; } + + setNotificationRecipientResolver( + resolver: NotificationRecipientResolver, + ): void { + if (this.#recipientResolver) { + throw new Error( + 'Notification recipient resolver is already set. You can only set it once.', + ); + } + this.#recipientResolver = resolver; + } + + get recipientResolver() { + return this.#recipientResolver; + } } /** @@ -98,6 +115,7 @@ export const notificationsPlugin = createBackendPlugin({ catalog, signals, processors: processingExtensions.processors, + recipientResolver: processingExtensions.recipientResolver, }), ); httpRouter.addAuthPolicy({ diff --git a/plugins/notifications-backend/src/service/router.test.ts b/plugins/notifications-backend/src/service/router.test.ts index 980762edbc..c66331c278 100644 --- a/plugins/notifications-backend/src/service/router.test.ts +++ b/plugins/notifications-backend/src/service/router.test.ts @@ -501,6 +501,91 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { }); }); + describe('POST /notifications with custom receiver resolver', () => { + const httpAuth = mockServices.httpAuth({ + defaultCredentials: mockCredentials.service(), + }); + + const recipientResolver = jest.fn(); + + beforeAll(async () => { + const router = await createRouter({ + logger: mockServices.logger.mock(), + store, + signals: signalService, + userInfo, + config, + httpAuth, + auth, + catalog, + recipientResolver, + }); + app = express().use(router).use(mockErrorHandler()); + }); + + beforeEach(async () => { + jest.resetAllMocks(); + const client = await database.getClient(); + await client('notification').del(); + await client('broadcast').del(); + await client('user_settings').del(); + }); + + const sendNotification = async (data: NotificationSendOptions) => + request(app) + .post('/notifications') + .send(data) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + it('should use custom recipient resolver', async () => { + recipientResolver.mockResolvedValue(['user:default/mock']); + const response = await sendNotification({ + recipients: { + type: 'entity', + entityRef: ['system:default/mock'], + }, + payload: { + title: 'test notification', + }, + }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual([ + { + created: expect.any(String), + id: expect.any(String), + origin: 'external:test-service', + payload: { + severity: 'normal', + title: 'test notification', + }, + user: 'user:default/mock', + }, + ]); + + const client = await database.getClient(); + const notifications = await client('notification') + .where('user', 'user:default/mock') + .select(); + expect(notifications).toHaveLength(1); + }); + + it('should return error if recipient resolver returns something other than an array of user entity refs', async () => { + recipientResolver.mockResolvedValue(['system:default/mock']); + const response = await sendNotification({ + recipients: { + type: 'entity', + entityRef: ['system:default/mock'], + }, + payload: { + title: 'test notification', + }, + }); + expect(response.status).toEqual(400); + }); + }); + describe('GET /', () => { const httpAuth = mockServices.httpAuth({ defaultCredentials: mockCredentials.user(), diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index 5805092096..51faf66279 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -26,6 +26,7 @@ import { v4 as uuid } from 'uuid'; import { CatalogService } from '@backstage/plugin-catalog-node'; import { NotificationProcessor, + NotificationRecipientResolver, NotificationSendOptions, } from '@backstage/plugin-notifications-node'; import { InputError, NotFoundError } from '@backstage/errors'; @@ -52,6 +53,7 @@ import { getUsersForEntityRef } from './getUsersForEntityRef'; import { Config, readDurationFromConfig } from '@backstage/config'; import { durationToMilliseconds } from '@backstage/types'; import pThrottle from 'p-throttle'; +import { parseEntityRef } from '@backstage/catalog-model'; /** @internal */ export interface RouterOptions { @@ -64,6 +66,7 @@ export interface RouterOptions { signals?: SignalsService; catalog: CatalogService; processors?: NotificationProcessor[]; + recipientResolver?: NotificationRecipientResolver; } /** @internal */ @@ -80,6 +83,7 @@ export async function createRouter( catalog, processors = [], signals, + recipientResolver, } = options; const WEB_NOTIFICATION_CHANNEL = 'Web'; @@ -639,6 +643,17 @@ export async function createRouter( opts: NotificationSendOptions, origin: string, ): Promise => { + if ( + users.find(u => { + const compound = parseEntityRef(u); + return compound.kind.toLocaleLowerCase('en-US') !== 'user'; + }) + ) { + throw new InputError( + 'Invalid user entity reference provided in recipients', + ); + } + const { scope } = opts.payload; const uniqueUsers = [...new Set(users)]; const throttled = throttle((user: string) => @@ -702,11 +717,16 @@ export async function createRouter( const entityRef = recipients.entityRef; try { - users = await getUsersForEntityRef( - entityRef, - recipients.excludeEntityRef ?? [], - { auth, catalog }, - ); + users = recipientResolver + ? await recipientResolver( + entityRef, + recipients.excludeEntityRef ?? [], + ) + : await getUsersForEntityRef( + entityRef, + recipients.excludeEntityRef ?? [], + { auth, catalog }, + ); } catch (e) { throw new InputError('Failed to resolve notification receivers', e); } diff --git a/plugins/notifications-node/report.api.md b/plugins/notifications-node/report.api.md index 995b98bc21..6b424e0fa5 100644 --- a/plugins/notifications-node/report.api.md +++ b/plugins/notifications-node/report.api.md @@ -41,6 +41,12 @@ export interface NotificationProcessor { // @public @deprecated (undocumented) export type NotificationProcessorFilters = NotificationProcessorFilters_2; +// @public +export type NotificationRecipientResolver = ( + entityRef: string | string[] | null, + excludeEntityRefs: string | string[], +) => Promise; + // @public (undocumented) export type NotificationRecipients = | { @@ -83,6 +89,10 @@ export interface NotificationsProcessingExtensionPoint { addProcessor( ...processors: Array> ): void; + // (undocumented) + setNotificationRecipientResolver( + resolver: NotificationRecipientResolver, + ): void; } // @public (undocumented) diff --git a/plugins/notifications-node/src/extensions.ts b/plugins/notifications-node/src/extensions.ts index 03ce47fea6..1e525a8d2b 100644 --- a/plugins/notifications-node/src/extensions.ts +++ b/plugins/notifications-node/src/extensions.ts @@ -100,6 +100,19 @@ export interface NotificationProcessor { getNotificationFilters?(): NotificationProcessorFilters; } +/** + * NotificationRecipientResolver is a function that resolves the individual users to receive the notification + * based on the entity reference(s) and the excluded entity reference(s). + * + * The function should return a list of user entity references that should receive the notification. + * + * @public + */ +export type NotificationRecipientResolver = ( + entityRef: string | string[] | null, + excludeEntityRefs: string | string[], +) => Promise; + /** * @public */ @@ -107,6 +120,9 @@ export interface NotificationsProcessingExtensionPoint { addProcessor( ...processors: Array> ): void; + setNotificationRecipientResolver( + resolver: NotificationRecipientResolver, + ): void; } /** From 4632b6b4da40415b6f45724b09aceff13174d7f8 Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Tue, 9 Sep 2025 22:41:45 +0300 Subject: [PATCH 196/233] feat(catalog): change extension point to inteface ignore other than user entity references instead error Signed-off-by: Hellgren Heikki --- .changeset/twenty-shoes-tickle.md | 9 +- ...aultNotificationRecipientResolver.test.ts} | 111 ++++++++--- .../DefaultNotificationRecipientResolver.ts | 177 ++++++++++++++++++ .../src/service/getUsersForEntityRef.ts | 159 ---------------- .../src/service/router.test.ts | 23 ++- .../src/service/router.ts | 69 ++++--- plugins/notifications-node/report.api.md | 13 +- plugins/notifications-node/src/extensions.ts | 19 +- 8 files changed, 335 insertions(+), 245 deletions(-) rename plugins/notifications-backend/src/service/{getUsersForEntityRef.test.ts => DefaultNotificationRecipientResolver.test.ts} (58%) create mode 100644 plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.ts delete mode 100644 plugins/notifications-backend/src/service/getUsersForEntityRef.ts diff --git a/.changeset/twenty-shoes-tickle.md b/.changeset/twenty-shoes-tickle.md index d13d93adde..da067d1ec9 100644 --- a/.changeset/twenty-shoes-tickle.md +++ b/.changeset/twenty-shoes-tickle.md @@ -3,7 +3,10 @@ '@backstage/plugin-notifications-node': patch --- -A new extension point method was added that can be used to modify how the users receiving notifications -are resolved. The function passed to the extension point should only return complete user entity references -based on the notification target references and the excluded entity references. Note that the input is a list +A new extension point was added that can be used to modify how the users receiving notifications +are resolved. The interface passed to the extension point should only return complete user entity references +based on the notification target references and the excluded entity references. Note that the inputs are lists of entity references that can be any entity kind, not just user entities. + +Using this extension point will override the default behavior of resolving users with the +`DefaultNotificationRecipientResolver`. diff --git a/plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts b/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.test.ts similarity index 58% rename from plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts rename to plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.test.ts index 838ded5c18..cd17db3680 100644 --- a/plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts +++ b/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.test.ts @@ -15,33 +15,28 @@ */ import { mockServices } from '@backstage/backend-test-utils'; -import { getUsersForEntityRef } from './getUsersForEntityRef'; import { RELATION_HAS_MEMBER, RELATION_OWNED_BY, RELATION_PARENT_OF, } from '@backstage/catalog-model'; import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; +import { DefaultNotificationRecipientResolver } from './DefaultNotificationRecipientResolver.ts'; describe('getUsersForEntityRef', () => { - it('should return empty array if entityRef is null', async () => { - await expect( - getUsersForEntityRef(null, [], { - auth: mockServices.auth(), - catalog: catalogServiceMock(), - }), - ).resolves.toEqual([]); - }); - it('should resolve users without calling catalog', async () => { const catalog = catalogServiceMock(); jest.spyOn(catalog, 'getEntitiesByRefs'); + const resolver = new DefaultNotificationRecipientResolver( + mockServices.auth(), + catalog, + ); await expect( - getUsersForEntityRef(['user:foo', 'user:ignored'], ['user:ignored'], { - auth: mockServices.auth(), - catalog, + resolver.resolveNotificationRecipients({ + entityRefs: ['user:foo', 'user:ignored'], + excludeEntityRefs: ['user:ignored'], }), - ).resolves.toEqual(['user:foo']); + ).resolves.toEqual({ userEntityRefs: ['user:foo'] }); expect(catalog.getEntitiesByRefs).not.toHaveBeenCalled(); }); @@ -85,16 +80,18 @@ describe('getUsersForEntityRef', () => { ], }); + const resolver = new DefaultNotificationRecipientResolver( + mockServices.auth(), + catalog, + ); await expect( - getUsersForEntityRef( - 'group:default/parent_group', - ['user:default/ignored'], - { - auth: mockServices.auth(), - catalog, - }, - ), - ).resolves.toEqual(['user:default/foo', 'user:default/bar']); + resolver.resolveNotificationRecipients({ + entityRefs: ['group:default/parent_group'], + excludeEntityRefs: ['user:default/ignored'], + }), + ).resolves.toEqual({ + userEntityRefs: ['user:default/foo', 'user:default/bar'], + }); }); it('should resolve user owner of entity from entity ref', async () => { @@ -116,12 +113,16 @@ describe('getUsersForEntityRef', () => { ], }); + const resolver = new DefaultNotificationRecipientResolver( + mockServices.auth(), + catalog, + ); await expect( - getUsersForEntityRef('component:default/test_component', [], { - auth: mockServices.auth(), - catalog, + resolver.resolveNotificationRecipients({ + entityRefs: ['component:default/test_component'], + excludeEntityRefs: [], }), - ).resolves.toEqual(['user:default/foo']); + ).resolves.toEqual({ userEntityRefs: ['user:default/foo'] }); }); it('should resolve group owner of entity from entity ref', async () => { @@ -156,11 +157,59 @@ describe('getUsersForEntityRef', () => { ], }); + const resolver = new DefaultNotificationRecipientResolver( + mockServices.auth(), + catalog, + ); await expect( - getUsersForEntityRef('component:default/test_component', [], { - auth: mockServices.auth(), - catalog, + resolver.resolveNotificationRecipients({ + entityRefs: ['component:default/test_component'], + excludeEntityRefs: [], }), - ).resolves.toEqual(['user:default/foo']); + ).resolves.toEqual({ userEntityRefs: ['user:default/foo'] }); + }); + + it('should filter excluded refs after resolving', async () => { + const catalog = catalogServiceMock({ + entities: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test_component', + }, + relations: [ + { + type: RELATION_OWNED_BY, + targetRef: 'group:default/owner_group', + }, + ], + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'owner_group', + }, + relations: [ + { + type: RELATION_HAS_MEMBER, + targetRef: 'user:default/foo', + }, + ], + }, + ], + }); + + const resolver = new DefaultNotificationRecipientResolver( + mockServices.auth(), + catalog, + ); + await expect( + resolver.resolveNotificationRecipients({ + entityRefs: ['component:default/test_component'], + excludeEntityRefs: ['user:default/foo'], + }), + ).resolves.toEqual({ userEntityRefs: [] }); }); }); diff --git a/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.ts b/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.ts new file mode 100644 index 0000000000..2401ec7c1f --- /dev/null +++ b/plugins/notifications-backend/src/service/DefaultNotificationRecipientResolver.ts @@ -0,0 +1,177 @@ +/* + * 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 { + Entity, + isGroupEntity, + isUserEntity, + parseEntityRef, + RELATION_HAS_MEMBER, + RELATION_OWNED_BY, + RELATION_PARENT_OF, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { AuthService } from '@backstage/backend-plugin-api'; +import { CatalogService } from '@backstage/plugin-catalog-node'; +import { NotificationRecipientResolver } from '@backstage/plugin-notifications-node'; + +const isUserEntityRef = (ref: string) => + parseEntityRef(ref).kind.toLocaleLowerCase() === 'user'; + +// Partitions array of entity references to two arrays; user entity refs and other entity refs +const partitionEntityRefs = (refs: string[]): string[][] => { + const ret = [[], []] as string[][]; + for (const ref of refs) { + if (isUserEntityRef(ref)) { + ret[0].push(ref); + } else { + ret[1].push(ref); + } + } + return ret; +}; + +export class DefaultNotificationRecipientResolver + implements NotificationRecipientResolver +{ + constructor( + private readonly auth: AuthService, + private readonly catalog: CatalogService, + ) {} + + async resolveNotificationRecipients(options: { + entityRefs: string[]; + excludeEntityRefs?: string[]; + }): Promise<{ userEntityRefs: string[] }> { + const { entityRefs, excludeEntityRefs = [] } = options; + + const [userEntityRefs, otherEntityRefs] = partitionEntityRefs(entityRefs); + const users: string[] = userEntityRefs.filter( + ref => !excludeEntityRefs.includes(ref), + ); + const filtered = otherEntityRefs.filter( + ref => !excludeEntityRefs.includes(ref), + ); + + const fields = ['kind', 'metadata.name', 'metadata.namespace', 'relations']; + let entities: Array = []; + if (filtered.length > 0) { + const fetchedEntities = await this.catalog.getEntitiesByRefs( + { + entityRefs: filtered, + fields, + }, + { credentials: await this.auth.getOwnServiceCredentials() }, + ); + entities = fetchedEntities.items; + } + + const cachedEntityRefs = new Map(); + + const mapEntity = async (entity: Entity | undefined): Promise => { + if (!entity) { + return []; + } + + const currentEntityRef = stringifyEntityRef(entity); + if (excludeEntityRefs.includes(currentEntityRef)) { + return []; + } + + if (cachedEntityRefs.has(currentEntityRef)) { + return cachedEntityRefs.get(currentEntityRef)!; + } + + if (isUserEntity(entity)) { + return [currentEntityRef]; + } + + if (isGroupEntity(entity)) { + if (!entity.relations?.length) { + return []; + } + + const groupUsers = entity.relations + .filter( + relation => + relation.type === RELATION_HAS_MEMBER && + isUserEntityRef(relation.targetRef), + ) + .map(r => r.targetRef); + + const childGroupRefs = entity.relations + .filter(relation => relation.type === RELATION_PARENT_OF) + .map(r => r.targetRef); + + let childGroupUsers: string[][] = []; + if (childGroupRefs.length > 0) { + const childGroups = await this.catalog.getEntitiesByRefs( + { + entityRefs: childGroupRefs, + fields, + }, + { credentials: await this.auth.getOwnServiceCredentials() }, + ); + childGroupUsers = await Promise.all(childGroups.items.map(mapEntity)); + } + + const ret = [ + ...new Set([...groupUsers, ...childGroupUsers.flat(2)]), + ].filter(ref => !excludeEntityRefs.includes(ref)); + cachedEntityRefs.set(currentEntityRef, ret); + return ret; + } + + if (entity.relations?.length) { + const ownerRef = entity.relations.find( + relation => relation.type === RELATION_OWNED_BY, + )?.targetRef; + + if (!ownerRef) { + return []; + } + + if (isUserEntityRef(ownerRef)) { + if (excludeEntityRefs.includes(ownerRef)) { + return []; + } + return [ownerRef]; + } + + const owner = await this.catalog.getEntityByRef(ownerRef, { + credentials: await this.auth.getOwnServiceCredentials(), + }); + const ret = await mapEntity(owner); + cachedEntityRefs.set(currentEntityRef, ret); + return ret; + } + + return []; + }; + + for (const entity of entities) { + const u = await mapEntity(entity); + users.push(...u); + } + + return { + userEntityRefs: [...new Set(users)] + .filter(Boolean) + // Need to filter again after resolving users + .filter(ref => !excludeEntityRefs.includes(ref)), + }; + } +} diff --git a/plugins/notifications-backend/src/service/getUsersForEntityRef.ts b/plugins/notifications-backend/src/service/getUsersForEntityRef.ts deleted file mode 100644 index 2fa1412992..0000000000 --- a/plugins/notifications-backend/src/service/getUsersForEntityRef.ts +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - Entity, - isGroupEntity, - isUserEntity, - parseEntityRef, - RELATION_HAS_MEMBER, - RELATION_OWNED_BY, - RELATION_PARENT_OF, - stringifyEntityRef, -} from '@backstage/catalog-model'; -import { AuthService } from '@backstage/backend-plugin-api'; -import { CatalogService } from '@backstage/plugin-catalog-node'; - -const isUserEntityRef = (ref: string) => - parseEntityRef(ref).kind.toLocaleLowerCase() === 'user'; - -// Partitions array of entity references to two arrays; user entity refs and other entity refs -const partitionEntityRefs = (refs: string[]): string[][] => - refs.reduce( - ([userEntityRefs, otherEntityRefs]: string[][], ref: string) => { - return isUserEntityRef(ref) - ? [[...userEntityRefs, ref], otherEntityRefs] - : [userEntityRefs, [...otherEntityRefs, ref]]; - }, - [[], []], - ); - -export const getUsersForEntityRef = async ( - entityRef: string | string[] | null, - excludeEntityRefs: string | string[], - options: { - auth: AuthService; - catalog: CatalogService; - }, -): Promise => { - const { auth, catalog } = options; - - if (entityRef === null) { - return []; - } - - const excluded = Array.isArray(excludeEntityRefs) - ? excludeEntityRefs - : [excludeEntityRefs]; - - const refsArr = Array.isArray(entityRef) ? entityRef : [entityRef]; - const [userEntityRefs, otherEntityRefs] = partitionEntityRefs(refsArr); - const users: string[] = userEntityRefs.filter(ref => !excluded.includes(ref)); - const entityRefs = otherEntityRefs.filter(ref => !excluded.includes(ref)); - - const fields = ['kind', 'metadata.name', 'metadata.namespace', 'relations']; - let entities: Array = []; - if (entityRefs.length > 0) { - const fetchedEntities = await catalog.getEntitiesByRefs( - { - entityRefs, - fields, - }, - { credentials: await auth.getOwnServiceCredentials() }, - ); - entities = fetchedEntities.items; - } - - const mapEntity = async (entity: Entity | undefined): Promise => { - if (!entity) { - return []; - } - - const currentEntityRef = stringifyEntityRef(entity); - if (excluded.includes(currentEntityRef)) { - return []; - } - - if (isUserEntity(entity)) { - return [currentEntityRef]; - } - - if (isGroupEntity(entity)) { - if (!entity.relations?.length) { - return []; - } - - const groupUsers = entity.relations - .filter( - relation => - relation.type === RELATION_HAS_MEMBER && - isUserEntityRef(relation.targetRef), - ) - .map(r => r.targetRef); - - const childGroupRefs = entity.relations - .filter(relation => relation.type === RELATION_PARENT_OF) - .map(r => r.targetRef); - - let childGroupUsers: string[][] = []; - if (childGroupRefs.length > 0) { - const childGroups = await catalog.getEntitiesByRefs( - { - entityRefs: childGroupRefs, - fields, - }, - { credentials: await auth.getOwnServiceCredentials() }, - ); - childGroupUsers = await Promise.all(childGroups.items.map(mapEntity)); - } - - return [...groupUsers, ...childGroupUsers.flat(2)].filter( - ref => !excluded.includes(ref), - ); - } - - if (entity.relations?.length) { - const ownerRef = entity.relations.find( - relation => relation.type === RELATION_OWNED_BY, - )?.targetRef; - - if (!ownerRef) { - return []; - } - - if (isUserEntityRef(ownerRef)) { - if (excluded.includes(ownerRef)) { - return []; - } - return [ownerRef]; - } - - const owner = await catalog.getEntityByRef(ownerRef, { - credentials: await auth.getOwnServiceCredentials(), - }); - return mapEntity(owner); - } - - return []; - }; - - for (const entity of entities) { - const u = await mapEntity(entity); - users.push(...u); - } - - return [...new Set(users)].filter(Boolean); -}; diff --git a/plugins/notifications-backend/src/service/router.test.ts b/plugins/notifications-backend/src/service/router.test.ts index c66331c278..08aa021014 100644 --- a/plugins/notifications-backend/src/service/router.test.ts +++ b/plugins/notifications-backend/src/service/router.test.ts @@ -25,7 +25,10 @@ import { TestDatabaseId, TestDatabases, } from '@backstage/backend-test-utils'; -import { NotificationSendOptions } from '@backstage/plugin-notifications-node'; +import { + NotificationRecipientResolver, + NotificationSendOptions, +} from '@backstage/plugin-notifications-node'; import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; import { DatabaseService } from '@backstage/backend-plugin-api'; import { v4 as uuid } from 'uuid'; @@ -506,7 +509,10 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { defaultCredentials: mockCredentials.service(), }); - const recipientResolver = jest.fn(); + const resolveFn = jest.fn(); + const recipientResolver: NotificationRecipientResolver = { + resolveNotificationRecipients: resolveFn, + }; beforeAll(async () => { const router = await createRouter({ @@ -539,7 +545,9 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { .set('Accept', 'application/json'); it('should use custom recipient resolver', async () => { - recipientResolver.mockResolvedValue(['user:default/mock']); + resolveFn.mockResolvedValue({ + userEntityRefs: ['user:default/mock'], + }); const response = await sendNotification({ recipients: { type: 'entity', @@ -571,8 +579,10 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { expect(notifications).toHaveLength(1); }); - it('should return error if recipient resolver returns something other than an array of user entity refs', async () => { - recipientResolver.mockResolvedValue(['system:default/mock']); + it('should ignore if recipient resolver returns something other than an array of user entity refs', async () => { + resolveFn.mockResolvedValue({ + userEntityRefs: ['system:default/mock'], + }); const response = await sendNotification({ recipients: { type: 'entity', @@ -582,7 +592,8 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { title: 'test notification', }, }); - expect(response.status).toEqual(400); + expect(response.status).toEqual(200); + expect(response.body).toEqual([]); }); }); diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index 51faf66279..027d6a1732 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -49,11 +49,11 @@ import { OriginSetting, } from '@backstage/plugin-notifications-common'; import { parseEntityOrderFieldParams } from './parseEntityOrderFieldParams'; -import { getUsersForEntityRef } from './getUsersForEntityRef'; import { Config, readDurationFromConfig } from '@backstage/config'; import { durationToMilliseconds } from '@backstage/types'; import pThrottle from 'p-throttle'; import { parseEntityRef } from '@backstage/catalog-model'; +import { DefaultNotificationRecipientResolver } from './DefaultNotificationRecipientResolver.ts'; /** @internal */ export interface RouterOptions { @@ -104,6 +104,10 @@ export async function createRouter( const defaultNotificationSettings: NotificationSettings | undefined = config.getOptional('notifications.defaultSettings'); + const usedRecipientResolver = + recipientResolver ?? + new DefaultNotificationRecipientResolver(auth, catalog); + const getUser = async (req: Request) => { const credentials = await httpAuth.credentials(req, { allow: ['user'] }); const info = await userInfo.getUserInfo(credentials); @@ -637,25 +641,25 @@ export async function createRouter( return ret; }; + const filterNonUserEntityRefs = (refs: string[]): string[] => { + return refs.filter(ref => { + try { + const parsed = parseEntityRef(ref); + return parsed.kind.toLowerCase() === 'user'; + } catch { + return false; + } + }); + }; + const sendUserNotifications = async ( baseNotification: Omit, users: string[], opts: NotificationSendOptions, origin: string, ): Promise => { - if ( - users.find(u => { - const compound = parseEntityRef(u); - return compound.kind.toLocaleLowerCase('en-US') !== 'user'; - }) - ) { - throw new InputError( - 'Invalid user entity reference provided in recipients', - ); - } - const { scope } = opts.payload; - const uniqueUsers = [...new Set(users)]; + const uniqueUsers = [...new Set(filterNonUserEntityRefs(users))]; const throttled = throttle((user: string) => sendUserNotification(baseNotification, user, opts, origin, scope), ); @@ -676,7 +680,6 @@ export async function createRouter( const { recipients, payload } = opts; const { title, link } = payload; const notifications: Notification[] = []; - let users = []; if (!recipients || !title) { const missing = [ @@ -714,30 +717,26 @@ export async function createRouter( ); notifications.push(broadcast); } else if (recipients.type === 'entity') { - const entityRef = recipients.entityRef; - + const entityRefs = [recipients.entityRef].flat(); + const excludedEntityRefs = recipients.excludeEntityRef + ? [recipients.excludeEntityRef].flat() + : undefined; try { - users = recipientResolver - ? await recipientResolver( - entityRef, - recipients.excludeEntityRef ?? [], - ) - : await getUsersForEntityRef( - entityRef, - recipients.excludeEntityRef ?? [], - { auth, catalog }, - ); + const { userEntityRefs } = + await usedRecipientResolver.resolveNotificationRecipients({ + entityRefs, + excludedEntityRefs, + }); + const userNotifications = await sendUserNotifications( + baseNotification, + userEntityRefs, + opts, + origin, + ); + notifications.push(...userNotifications); } catch (e) { - throw new InputError('Failed to resolve notification receivers', e); + throw new InputError('Failed to send user notifications', e); } - - const userNotifications = await sendUserNotifications( - baseNotification, - users, - opts, - origin, - ); - notifications.push(...userNotifications); } else { throw new InputError( `Invalid recipients type, please use either 'broadcast' or 'entity'`, diff --git a/plugins/notifications-node/report.api.md b/plugins/notifications-node/report.api.md index 6b424e0fa5..ced4fac94e 100644 --- a/plugins/notifications-node/report.api.md +++ b/plugins/notifications-node/report.api.md @@ -42,10 +42,15 @@ export interface NotificationProcessor { export type NotificationProcessorFilters = NotificationProcessorFilters_2; // @public -export type NotificationRecipientResolver = ( - entityRef: string | string[] | null, - excludeEntityRefs: string | string[], -) => Promise; +export interface NotificationRecipientResolver { + // (undocumented) + resolveNotificationRecipients(options: { + entityRefs: string[]; + excludedEntityRefs?: string[]; + }): Promise<{ + userEntityRefs: string[]; + }>; +} // @public (undocumented) export type NotificationRecipients = diff --git a/plugins/notifications-node/src/extensions.ts b/plugins/notifications-node/src/extensions.ts index 1e525a8d2b..98338de221 100644 --- a/plugins/notifications-node/src/extensions.ts +++ b/plugins/notifications-node/src/extensions.ts @@ -101,17 +101,22 @@ export interface NotificationProcessor { } /** - * NotificationRecipientResolver is a function that resolves the individual users to receive the notification - * based on the entity reference(s) and the excluded entity reference(s). + * NotificationRecipientResolver interface is used to resolve the individual + * users to receive the notification. * - * The function should return a list of user entity references that should receive the notification. + * The `resolveNotificationRecipients` is used to resolve notifications sent for + * entity references, and it should return object with a list of user + * entity references that should receive the notification. In case the function + * returns other than user entity references, those are ignored. * * @public */ -export type NotificationRecipientResolver = ( - entityRef: string | string[] | null, - excludeEntityRefs: string | string[], -) => Promise; +export interface NotificationRecipientResolver { + resolveNotificationRecipients(options: { + entityRefs: string[]; + excludedEntityRefs?: string[]; + }): Promise<{ userEntityRefs: string[] }>; +} /** * @public From aee107ba37e3abc50c61123901811841f9f6d629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C3=96st=C3=B6r?= Date: Wed, 10 Sep 2025 17:38:03 +0100 Subject: [PATCH 197/233] feat(scaffolder): add auto_init option for repository creation and examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adam Östör --- .changeset/brown-pens-shave.md | 18 +++ .../report.api.md | 1 + .../actions/githubRepoCreate.examples.test.ts | 101 ++++++++++++++ .../src/actions/githubRepoCreate.examples.ts | 15 ++ .../src/actions/githubRepoCreate.test.ts | 128 ++++++++++++++++++ .../src/actions/githubRepoCreate.ts | 3 + .../src/actions/helpers.ts | 3 + .../src/actions/inputProperties.ts | 9 ++ 8 files changed, 278 insertions(+) create mode 100644 .changeset/brown-pens-shave.md diff --git a/.changeset/brown-pens-shave.md b/.changeset/brown-pens-shave.md new file mode 100644 index 0000000000..33cf7cfeca --- /dev/null +++ b/.changeset/brown-pens-shave.md @@ -0,0 +1,18 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Add `auto_init` option to 'github:repo:create' action to create repository with an initial commit containing a README.md file + +This initial commit is created by GitHub itself and the commit is signed, so the repository will not be empty after creation. + +```diff + - action: github:repo:create + id: init-new-repo + input: + repoUrl: 'github.com?repo=repo&owner=owner' + description: This is the description + visibility: private ++ autoInit: true + +``` diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index 8f1bc23785..ed109eb9e3 100644 --- a/plugins/scaffolder-backend-module-github/report.api.md +++ b/plugins/scaffolder-backend-module-github/report.api.md @@ -291,6 +291,7 @@ export function createGithubRepoCreateAction(options: { requiredLinearHistory?: boolean | undefined; customProperties?: Record | undefined; subscribe?: boolean | undefined; + autoInit?: boolean | undefined; }, { remoteUrl: string; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts index da4837adb7..c37e9c222b 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts @@ -136,6 +136,7 @@ describe('github:repo:create examples', () => { has_wiki: undefined, homepage: undefined, visibility: 'private', + auto_init: undefined, }); }); @@ -169,6 +170,7 @@ describe('github:repo:create examples', () => { has_wiki: undefined, homepage: undefined, visibility: 'private', + auto_init: undefined, }); }); @@ -203,6 +205,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: false, // disable wiki homepage: undefined, + auto_init: undefined, }); }); @@ -246,6 +249,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: 'https://example.com', + auto_init: undefined, }); }); @@ -289,6 +293,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -332,6 +337,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -375,6 +381,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -418,6 +425,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -461,6 +469,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -504,6 +513,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -547,6 +557,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -590,6 +601,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -633,6 +645,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -676,6 +689,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -719,6 +733,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -762,6 +777,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -805,6 +821,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -848,6 +865,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -891,6 +909,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -934,6 +953,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -977,6 +997,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: 'https://project-xyz.com', + auto_init: undefined, }); }); @@ -1020,6 +1041,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1063,6 +1085,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1106,6 +1129,7 @@ describe('github:repo:create examples', () => { has_projects: true, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1149,6 +1173,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1192,6 +1217,7 @@ describe('github:repo:create examples', () => { has_projects: false, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1235,6 +1261,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1278,6 +1305,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1321,6 +1349,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1364,6 +1393,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1407,6 +1437,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1450,6 +1481,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1493,6 +1525,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1536,6 +1569,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1579,6 +1613,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: 'https://internal.example.com', + auto_init: undefined, }); }); @@ -1622,6 +1657,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1665,6 +1701,7 @@ describe('github:repo:create examples', () => { has_projects: true, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1708,6 +1745,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1751,6 +1789,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1794,6 +1833,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1837,6 +1877,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: 'https://webapp.example.com', + auto_init: undefined, }); }); @@ -1880,6 +1921,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1923,6 +1965,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -1966,6 +2009,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2009,6 +2053,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2052,6 +2097,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2095,6 +2141,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: false, homepage: undefined, + auto_init: undefined, }); }); @@ -2138,6 +2185,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2181,6 +2229,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2224,6 +2273,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2267,6 +2317,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2310,6 +2361,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: 'https://example.com', + auto_init: undefined, }); }); @@ -2353,6 +2405,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2396,6 +2449,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2439,6 +2493,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2482,6 +2537,7 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); }); @@ -2525,6 +2581,51 @@ describe('github:repo:create examples', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, + }); + }); + + it(`Should ${examples[58].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[58].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_update_branch: false, + custom_properties: undefined, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + auto_init: true, }); }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts index ce39e7510d..41c6a15fba 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts @@ -1006,4 +1006,19 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Create a repository with an initial commit.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with an initial (signed) commit containing a README', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + autoInit: true, + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts index b90f0c4e5a..adad5237da 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.test.ts @@ -157,6 +157,7 @@ describe('github:repo:create', () => { has_wiki: undefined, homepage: undefined, visibility: 'private', + auto_init: undefined, }); await action.handler({ @@ -185,6 +186,7 @@ describe('github:repo:create', () => { has_wiki: undefined, homepage: undefined, visibility: 'public', + auto_init: undefined, }); await action.handler({ @@ -212,6 +214,7 @@ describe('github:repo:create', () => { has_projects: undefined, has_wiki: undefined, visibility: 'private', + auto_init: undefined, }); await action.handler({ @@ -242,6 +245,7 @@ describe('github:repo:create', () => { has_projects: undefined, has_issues: undefined, homepage: 'https://example.com', + auto_init: undefined, }); await action.handler({ @@ -271,6 +275,7 @@ describe('github:repo:create', () => { has_wiki: undefined, has_projects: undefined, has_issues: undefined, + auto_init: undefined, }); await action.handler({ @@ -303,6 +308,67 @@ describe('github:repo:create', () => { has_wiki: undefined, homepage: 'https://example.com', visibility: 'private', + auto_init: undefined, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + autoInit: true, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + description: 'description', + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + allow_update_branch: false, + custom_properties: undefined, + has_issues: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + visibility: 'private', + auto_init: true, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + autoInit: false, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + description: 'description', + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + allow_update_branch: false, + custom_properties: undefined, + has_issues: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + visibility: 'private', + auto_init: false, }); }); @@ -334,6 +400,7 @@ describe('github:repo:create', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); await action.handler({ @@ -361,6 +428,7 @@ describe('github:repo:create', () => { has_projects: undefined, has_wiki: undefined, homepage: undefined, + auto_init: undefined, }); await action.handler({ @@ -388,6 +456,7 @@ describe('github:repo:create', () => { has_issues: undefined, has_projects: undefined, has_wiki: undefined, + auto_init: undefined, }); await action.handler({ @@ -417,6 +486,7 @@ describe('github:repo:create', () => { has_projects: undefined, has_issues: undefined, homepage: 'https://example.com', + auto_init: undefined, }); await action.handler({ @@ -446,6 +516,7 @@ describe('github:repo:create', () => { has_projects: undefined, has_issues: undefined, homepage: 'https://example.com', + auto_init: undefined, }); // Custom properties on user repos should be ignored @@ -478,6 +549,63 @@ describe('github:repo:create', () => { has_projects: undefined, has_wiki: undefined, homepage: 'https://example.com', + auto_init: undefined, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + autoInit: true, + }, + }); + expect( + mockOctokit.rest.repos.createForAuthenticatedUser, + ).toHaveBeenCalledWith({ + description: 'description', + name: 'repo', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_message: 'COMMIT_MESSAGES', + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + allow_update_branch: false, + has_wiki: undefined, + has_projects: undefined, + has_issues: undefined, + homepage: undefined, + auto_init: true, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + autoInit: false, + }, + }); + expect( + mockOctokit.rest.repos.createForAuthenticatedUser, + ).toHaveBeenCalledWith({ + description: 'description', + name: 'repo', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_message: 'COMMIT_MESSAGES', + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + allow_update_branch: false, + has_wiki: undefined, + has_projects: undefined, + has_issues: undefined, + homepage: undefined, + auto_init: false, }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts index a85010797d..952bf514d4 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts @@ -81,6 +81,7 @@ export function createGithubRepoCreateAction(options: { requiredLinearHistory: inputProps.requiredLinearHistory, customProperties: inputProps.customProperties, subscribe: inputProps.subscribe, + autoInit: inputProps.autoInit, }, output: { remoteUrl: outputProps.remoteUrl, @@ -113,6 +114,7 @@ export function createGithubRepoCreateAction(options: { customProperties, subscribe, token: providedToken, + autoInit = undefined, } = ctx.input; const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); @@ -164,6 +166,7 @@ export function createGithubRepoCreateAction(options: { customProperties, subscribe, ctx.logger, + autoInit, ); return newRepo.clone_url; }, diff --git a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts index 6509d7fd29..a3d0caed33 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts @@ -78,6 +78,7 @@ export async function createGithubRepoWithCollaboratorsAndTopics( customProperties: { [key: string]: string | string[] } | undefined, subscribe: boolean | undefined, logger: LoggerService, + autoInit?: boolean | undefined, ) { // eslint-disable-next-line testing-library/no-await-sync-queries const user = await client.rest.users.getByUsername({ @@ -109,6 +110,7 @@ export async function createGithubRepoWithCollaboratorsAndTopics( has_projects: hasProjects, has_wiki: hasWiki, has_issues: hasIssues, + auto_init: autoInit, // Custom properties only available on org repos custom_properties: customProperties, }) @@ -128,6 +130,7 @@ export async function createGithubRepoWithCollaboratorsAndTopics( has_projects: hasProjects, has_wiki: hasWiki, has_issues: hasIssues, + auto_init: autoInit, }); let newRepo; diff --git a/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts b/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts index bfa232c45d..8a901b0e99 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts @@ -413,6 +413,14 @@ const branch = (z: typeof zod) => }) .optional(); +const autoInit = (z: typeof zod) => + z + .boolean({ + description: `Create an initial commit with empty README. Default is 'false'`, + }) + .default(false) + .optional(); + export { repoUrl, description, @@ -458,4 +466,5 @@ export { bypassPullRequestAllowances, branch, blockCreations, + autoInit, }; From c89e954bf401ac8097ec496a41ac6036ea3f03ad Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Sep 2025 14:29:34 +0200 Subject: [PATCH 198/233] error collection: review fixes Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/src/wiring/createErrorCollector.ts | 2 +- packages/frontend-defaults/src/maybeCreateErrorPage.tsx | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/frontend-app-api/src/wiring/createErrorCollector.ts b/packages/frontend-app-api/src/wiring/createErrorCollector.ts index 633febd945..93540cec63 100644 --- a/packages/frontend-app-api/src/wiring/createErrorCollector.ts +++ b/packages/frontend-app-api/src/wiring/createErrorCollector.ts @@ -89,7 +89,7 @@ export interface ErrorCollector { AppErrorTypes[TCode]['context'], keyof TContext > extends infer IContext extends {} - ? [{}] extends [IContext] + ? {} extends IContext ? { code: TCode; message: string; diff --git a/packages/frontend-defaults/src/maybeCreateErrorPage.tsx b/packages/frontend-defaults/src/maybeCreateErrorPage.tsx index c4bce8cf73..40da64e987 100644 --- a/packages/frontend-defaults/src/maybeCreateErrorPage.tsx +++ b/packages/frontend-defaults/src/maybeCreateErrorPage.tsx @@ -16,8 +16,6 @@ import { FrontendPluginInfo } from '@backstage/frontend-plugin-api'; import { JSX, useEffect, useState } from 'react'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { AppError, AppErrorTypes } from '@backstage/frontend-app-api'; const DEFAULT_WARNING_CODES: Array = [ From ed1801dae570bde4e0dd688cbdfc3275b3ae0307 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Mon, 15 Sep 2025 16:28:17 +0200 Subject: [PATCH 199/233] Add auto_init option to github:repo:create action Tweak changeset Signed-off-by: Ben Lambert --- .changeset/brown-pens-shave.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/brown-pens-shave.md b/.changeset/brown-pens-shave.md index 33cf7cfeca..d5810838ff 100644 --- a/.changeset/brown-pens-shave.md +++ b/.changeset/brown-pens-shave.md @@ -2,7 +2,7 @@ '@backstage/plugin-scaffolder-backend-module-github': patch --- -Add `auto_init` option to 'github:repo:create' action to create repository with an initial commit containing a README.md file +Add `auto_init` option to `github:repo:create` action to create repository with an initial commit containing a README.md file This initial commit is created by GitHub itself and the commit is signed, so the repository will not be empty after creation. From d89815ce9f26375af32c516af8661c4b3c8ef631 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 15 Sep 2025 16:55:32 +0200 Subject: [PATCH 200/233] frontend-app-api: add collection of route binding errors Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/report.api.md | 15 +++ .../src/routing/collectRouteIds.test.ts | 65 ++++++++- .../src/routing/collectRouteIds.ts | 20 ++- .../src/routing/resolveRouteBindings.test.ts | 127 ++++++++++++------ .../src/routing/resolveRouteBindings.ts | 38 ++++-- .../src/wiring/createErrorCollector.ts | 10 ++ .../src/wiring/createSpecializedApp.tsx | 4 +- .../src/maybeCreateErrorPage.tsx | 8 +- 8 files changed, 220 insertions(+), 67 deletions(-) diff --git a/packages/frontend-app-api/report.api.md b/packages/frontend-app-api/report.api.md index fca32ed38c..bd0709813b 100644 --- a/packages/frontend-app-api/report.api.md +++ b/packages/frontend-app-api/report.api.md @@ -109,6 +109,21 @@ export type AppErrorTypes = { node: AppNode; }; }; + ROUTE_DUPLICATE: { + context: { + routeId: string; + }; + }; + ROUTE_BINDING_INVALID_VALUE: { + context: { + routeId: string; + }; + }; + ROUTE_NOT_FOUND: { + context: { + routeId: string; + }; + }; }; // @public diff --git a/packages/frontend-app-api/src/routing/collectRouteIds.test.ts b/packages/frontend-app-api/src/routing/collectRouteIds.test.ts index 395886f982..b1bab3fbbe 100644 --- a/packages/frontend-app-api/src/routing/collectRouteIds.test.ts +++ b/packages/frontend-app-api/src/routing/collectRouteIds.test.ts @@ -20,6 +20,18 @@ import { createFrontendPlugin, } from '@backstage/frontend-plugin-api'; import { collectRouteIds } from './collectRouteIds'; +import { createErrorCollector } from '../wiring/createErrorCollector'; + +const collector = createErrorCollector(); + +afterEach(() => { + const errors = collector.collectErrors(); + if (errors) { + throw new Error( + `Unexpected errors: ${errors.map(e => e.message).join(', ')}`, + ); + } +}); describe('collectRouteIds', () => { it('should assign IDs to routes', () => { @@ -33,13 +45,16 @@ describe('collectRouteIds', () => { /^ExternalRouteRef\{created at '.*collectRouteIds\.test\.ts.*'\}$/, ); - const collected = collectRouteIds([ - createFrontendPlugin({ - pluginId: 'test', - routes: { ref }, - externalRoutes: { extRef }, - }), - ]); + const collected = collectRouteIds( + [ + createFrontendPlugin({ + pluginId: 'test', + routes: { ref }, + externalRoutes: { extRef }, + }), + ], + collector, + ); expect(Object.fromEntries(collected.routes)).toEqual({ 'test.ref': ref, }); @@ -50,4 +65,40 @@ describe('collectRouteIds', () => { expect(String(ref)).toBe('RouteRef{test.ref}'); expect(String(extRef)).toBe('ExternalRouteRef{test.extRef}'); }); + + it('should report duplicate route IDs', () => { + const ref = createRouteRef(); + const extRef = createExternalRouteRef(); + + collectRouteIds( + [ + createFrontendPlugin({ + pluginId: 'test', + routes: { 'mid.ref': ref }, + externalRoutes: { 'mid.extRef': extRef }, + }), + createFrontendPlugin({ + pluginId: 'test.mid', + routes: { ref }, + externalRoutes: { extRef }, + }), + ], + collector, + ); + + expect(collector.collectErrors()).toEqual([ + { + code: 'ROUTE_DUPLICATE', + message: + "Duplicate route id 'test.mid.ref' encountered while collecting routes", + context: { routeId: 'test.mid.ref' }, + }, + { + code: 'ROUTE_DUPLICATE', + message: + "Duplicate external route id 'test.mid.extRef' encountered while collecting routes", + context: { routeId: 'test.mid.extRef' }, + }, + ]); + }); }); diff --git a/packages/frontend-app-api/src/routing/collectRouteIds.ts b/packages/frontend-app-api/src/routing/collectRouteIds.ts index 8f189bda81..79afb07928 100644 --- a/packages/frontend-app-api/src/routing/collectRouteIds.ts +++ b/packages/frontend-app-api/src/routing/collectRouteIds.ts @@ -30,6 +30,7 @@ import { toInternalExternalRouteRef } from '../../../frontend-plugin-api/src/rou // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalSubRouteRef } from '../../../frontend-plugin-api/src/routing/SubRouteRef'; import { OpaqueFrontendPlugin } from '@internal/frontend'; +import { ErrorCollector } from '../wiring/createErrorCollector'; /** @internal */ export interface RouteRefsById { @@ -38,7 +39,10 @@ export interface RouteRefsById { } /** @internal */ -export function collectRouteIds(features: FrontendFeature[]): RouteRefsById { +export function collectRouteIds( + features: FrontendFeature[], + collector: ErrorCollector, +): RouteRefsById { const routesById = new Map(); const externalRoutesById = new Map(); @@ -50,7 +54,12 @@ export function collectRouteIds(features: FrontendFeature[]): RouteRefsById { for (const [name, ref] of Object.entries(feature.routes)) { const refId = `${feature.id}.${name}`; if (routesById.has(refId)) { - throw new Error(`Unexpected duplicate route '${refId}'`); + collector.report({ + code: 'ROUTE_DUPLICATE', + message: `Duplicate route id '${refId}' encountered while collecting routes`, + context: { routeId: refId }, + }); + continue; } if (isRouteRef(ref)) { @@ -65,7 +74,12 @@ export function collectRouteIds(features: FrontendFeature[]): RouteRefsById { for (const [name, ref] of Object.entries(feature.externalRoutes)) { const refId = `${feature.id}.${name}`; if (externalRoutesById.has(refId)) { - throw new Error(`Unexpected duplicate external route '${refId}'`); + collector.report({ + code: 'ROUTE_DUPLICATE', + message: `Duplicate external route id '${refId}' encountered while collecting routes`, + context: { routeId: refId }, + }); + continue; } const internalRef = toInternalExternalRouteRef(ref); diff --git a/packages/frontend-app-api/src/routing/resolveRouteBindings.test.ts b/packages/frontend-app-api/src/routing/resolveRouteBindings.test.ts index 6618bef152..003980e617 100644 --- a/packages/frontend-app-api/src/routing/resolveRouteBindings.test.ts +++ b/packages/frontend-app-api/src/routing/resolveRouteBindings.test.ts @@ -20,6 +20,18 @@ import { } from '@backstage/frontend-plugin-api'; import { resolveRouteBindings } from './resolveRouteBindings'; import { ConfigReader } from '@backstage/config'; +import { createErrorCollector } from '../wiring/createErrorCollector'; + +const collector = createErrorCollector(); + +afterEach(() => { + const errors = collector.collectErrors(); + if (errors) { + throw new Error( + `Unexpected errors: ${errors.map(e => e.message).join(', ')}`, + ); + } +}); const emptyIds = { routes: new Map(), externalRoutes: new Map() }; @@ -33,23 +45,30 @@ describe('resolveRouteBindings', () => { }, new ConfigReader({}), emptyIds, + collector, ); expect(result.get(external.myRoute)).toBe(ref); }); - it('throws on unknown keys', () => { + it('reports error on unknown keys', () => { const external = { myRoute: createExternalRouteRef() }; const ref = createRouteRef(); - expect(() => - resolveRouteBindings( - ({ bind }) => { - bind(external, { someOtherRoute: ref } as any); - }, - new ConfigReader({}), - emptyIds, - ), - ).toThrow('Key someOtherRoute is not an existing external route'); + resolveRouteBindings( + ({ bind }) => { + bind(external, { someOtherRoute: ref } as any); + }, + new ConfigReader({}), + emptyIds, + collector, + ); + expect(collector.collectErrors()).toEqual([ + { + code: 'ROUTE_NOT_FOUND', + message: 'Key someOtherRoute is not an existing external route', + context: { routeId: 'someOtherRoute' }, + }, + ]); }); it('reads bindings from config', () => { @@ -64,6 +83,7 @@ describe('resolveRouteBindings', () => { routes: new Map([['myTarget', myTarget]]), externalRoutes: new Map([['mySource', mySource]]), }, + collector, ); expect(result.get(mySource)).toBe(myTarget); @@ -85,6 +105,7 @@ describe('resolveRouteBindings', () => { routes: new Map([['myTarget', myTarget]]), externalRoutes: new Map([['mySource', mySource]]), }, + collector, ).get(mySource), ).toBe(undefined); @@ -100,57 +121,74 @@ describe('resolveRouteBindings', () => { routes: new Map([['myTarget', myTarget]]), externalRoutes: new Map([['mySource', mySource]]), }, + collector, ).get(mySource), ).toBe(myTarget); }); - it('throws on invalid config', () => { + it('reports errors on invalid config', () => { expect(() => resolveRouteBindings( () => {}, new ConfigReader({ app: { routes: { bindings: 'derp' } } }), emptyIds, + collector, ), ).toThrow( "Invalid type in config for key 'app.routes.bindings' in 'mock-config', got string, wanted object", ); - expect(() => - resolveRouteBindings( - () => {}, - new ConfigReader({ app: { routes: { bindings: { mySource: true } } } }), - emptyIds, - ), - ).toThrow( - "Invalid config at app.routes.bindings['mySource'], value must be a non-empty string", + resolveRouteBindings( + () => {}, + new ConfigReader({ app: { routes: { bindings: { mySource: true } } } }), + emptyIds, + collector, ); + expect(collector.collectErrors()).toEqual([ + { + code: 'ROUTE_BINDING_INVALID_VALUE', + message: + "Invalid config at app.routes.bindings['mySource'], value must be a non-empty string or false", + context: { routeId: 'mySource' }, + }, + ]); - expect(() => - resolveRouteBindings( - () => {}, - new ConfigReader({ - app: { routes: { bindings: { mySource: 'myTarget' } } }, - }), - emptyIds, - ), - ).toThrow( - "Invalid config at app.routes.bindings, 'mySource' is not a valid external route", + resolveRouteBindings( + () => {}, + new ConfigReader({ + app: { routes: { bindings: { mySource: 'myTarget' } } }, + }), + emptyIds, + collector, ); + expect(collector.collectErrors()).toEqual([ + { + code: 'ROUTE_NOT_FOUND', + message: + "Invalid config at app.routes.bindings, 'mySource' is not a valid external route", + context: { routeId: 'mySource' }, + }, + ]); - expect(() => - resolveRouteBindings( - () => {}, - new ConfigReader({ - app: { routes: { bindings: { mySource: 'myTarget' } } }, - }), - { - ...emptyIds, - externalRoutes: new Map([['mySource', createExternalRouteRef()]]), - }, - ), - ).toThrow( - "Invalid config at app.routes.bindings['mySource'], 'myTarget' is not a valid route", + resolveRouteBindings( + () => {}, + new ConfigReader({ + app: { routes: { bindings: { mySource: 'myTarget' } } }, + }), + { + ...emptyIds, + externalRoutes: new Map([['mySource', createExternalRouteRef()]]), + }, + collector, ); + expect(collector.collectErrors()).toEqual([ + { + code: 'ROUTE_NOT_FOUND', + message: + "Invalid config at app.routes.bindings['mySource'], 'myTarget' is not a valid route", + context: { routeId: 'myTarget' }, + }, + ]); }); it('can have default targets, but at the lowest priority', () => { @@ -170,6 +208,7 @@ describe('resolveRouteBindings', () => { () => {}, new ConfigReader({}), routesById, + collector, ); expect(result.get(source)).toBe(target1); @@ -181,6 +220,7 @@ describe('resolveRouteBindings', () => { app: { routes: { bindings: { source: 'target2' } } }, }), routesById, + collector, ); expect(result.get(source)).toBe(target2); @@ -192,6 +232,7 @@ describe('resolveRouteBindings', () => { }, new ConfigReader({}), routesById, + collector, ); expect(result.get(source)).toBe(target2); @@ -210,6 +251,7 @@ describe('resolveRouteBindings', () => { () => {}, new ConfigReader({}), routesById, + collector, ); expect(result.get(source)).toBe(target1); @@ -219,6 +261,7 @@ describe('resolveRouteBindings', () => { () => {}, new ConfigReader({ app: { routes: { bindings: { source: false } } } }), routesById, + collector, ); expect(result.get(source)).toBe(undefined); diff --git a/packages/frontend-app-api/src/routing/resolveRouteBindings.ts b/packages/frontend-app-api/src/routing/resolveRouteBindings.ts index 76d620c20c..f9836d7107 100644 --- a/packages/frontend-app-api/src/routing/resolveRouteBindings.ts +++ b/packages/frontend-app-api/src/routing/resolveRouteBindings.ts @@ -20,6 +20,7 @@ import { ExternalRouteRef, } from '@backstage/frontend-plugin-api'; import { RouteRefsById } from './collectRouteIds'; +import { ErrorCollector } from '../wiring/createErrorCollector'; import { Config } from '@backstage/config'; import { JsonObject } from '@backstage/types'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports @@ -80,6 +81,7 @@ export function resolveRouteBindings( bindRoutes: ((context: { bind: CreateAppRouteBinder }) => void) | undefined, config: Config, routesById: RouteRefsById, + collector: ErrorCollector, ): Map { const result = new Map(); const disabledExternalRefs = new Set(); @@ -93,7 +95,12 @@ export function resolveRouteBindings( for (const [key, value] of Object.entries(targetRoutes)) { const externalRoute = externalRoutes[key]; if (!externalRoute) { - throw new Error(`Key ${key} is not an existing external route`); + collector.report({ + code: 'ROUTE_NOT_FOUND', + message: `Key ${key} is not an existing external route`, + context: { routeId: String(key) }, + }); + continue; } if (value) { result.set(externalRoute, value); @@ -112,16 +119,22 @@ export function resolveRouteBindings( if (bindings) { for (const [externalRefId, targetRefId] of Object.entries(bindings)) { if (!isValidTargetRefId(targetRefId)) { - throw new Error( - `Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string or false`, - ); + collector.report({ + code: 'ROUTE_BINDING_INVALID_VALUE', + message: `Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string or false`, + context: { routeId: externalRefId }, + }); + continue; } const externalRef = routesById.externalRoutes.get(externalRefId); if (!externalRef) { - throw new Error( - `Invalid config at app.routes.bindings, '${externalRefId}' is not a valid external route`, - ); + collector.report({ + code: 'ROUTE_NOT_FOUND', + message: `Invalid config at app.routes.bindings, '${externalRefId}' is not a valid external route`, + context: { routeId: externalRefId }, + }); + continue; } // Skip if binding was already defined in code @@ -134,9 +147,14 @@ export function resolveRouteBindings( } else { const targetRef = routesById.routes.get(targetRefId); if (!targetRef) { - throw new Error( - `Invalid config at app.routes.bindings['${externalRefId}'], '${targetRefId}' is not a valid route`, - ); + collector.report({ + code: 'ROUTE_NOT_FOUND', + message: `Invalid config at app.routes.bindings['${externalRefId}'], '${String( + targetRefId, + )}' is not a valid route`, + context: { routeId: String(targetRefId) }, + }); + continue; } result.set(externalRef, targetRef); diff --git a/packages/frontend-app-api/src/wiring/createErrorCollector.ts b/packages/frontend-app-api/src/wiring/createErrorCollector.ts index 93540cec63..c20e92d0f0 100644 --- a/packages/frontend-app-api/src/wiring/createErrorCollector.ts +++ b/packages/frontend-app-api/src/wiring/createErrorCollector.ts @@ -66,6 +66,16 @@ export type AppErrorTypes = { API_EXTENSION_INVALID: { context: { node: AppNode }; }; + // routing + ROUTE_DUPLICATE: { + context: { routeId: string }; + }; + ROUTE_BINDING_INVALID_VALUE: { + context: { routeId: string }; + }; + ROUTE_NOT_FOUND: { + context: { routeId: string }; + }; }; /** diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx index a9b7e6864a..6c6d75415b 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx @@ -316,9 +316,9 @@ export function createSpecializedApp(options?: CreateSpecializedAppOptions): { const appBasePath = getBasePath(config); const appTreeApi = new AppTreeApiProxy(tree, appBasePath); - const routeRefsById = collectRouteIds(features); + const routeRefsById = collectRouteIds(features, collector); const routeResolutionApi = new RouteResolutionApiProxy( - resolveRouteBindings(options?.bindRoutes, config, routeRefsById), + resolveRouteBindings(options?.bindRoutes, config, routeRefsById, collector), appBasePath, ); diff --git a/packages/frontend-defaults/src/maybeCreateErrorPage.tsx b/packages/frontend-defaults/src/maybeCreateErrorPage.tsx index 40da64e987..6bad97f666 100644 --- a/packages/frontend-defaults/src/maybeCreateErrorPage.tsx +++ b/packages/frontend-defaults/src/maybeCreateErrorPage.tsx @@ -28,9 +28,10 @@ const DEFAULT_WARNING_CODES: Array = [ function AppErrorItem(props: { error: AppError }): JSX.Element { const { context } = props.error; - const extensionId = - 'extensionId' in context ? context.extensionId : context.node.spec.id; const node = 'node' in context ? context.node : undefined; + const extensionId = + 'extensionId' in context ? context.extensionId : node?.spec.id; + const routeId = 'routeId' in context ? context.routeId : undefined; const plugin = 'plugin' in context ? context.plugin : node?.spec.plugin; const pluginId = plugin?.id ?? 'N/A'; @@ -46,7 +47,8 @@ function AppErrorItem(props: { error: AppError }): JSX.Element {
{props.error.code}: {props.error.message}
-        
extensionId: {extensionId}
+ {extensionId &&
extensionId: {extensionId}
} + {routeId &&
routeId: {routeId}
} {pluginId &&
pluginId: {pluginId}
} {info && (
From cde70cabfde8ba7cc6c9f0f3c37648441033867d Mon Sep 17 00:00:00 2001 From: Matheus Almeida <38531751+matheusjv11@users.noreply.github.com> Date: Mon, 15 Sep 2025 12:16:24 -0300 Subject: [PATCH 201/233] module-elasticsearch: include custom document id to batch upload (#30128) * include custom document id to elasticsearch batch upload --------- Signed-off-by: Matheus Almeida --- .changeset/stale-adults-hammer.md | 5 ++ .../config/vocabularies/Backstage/accept.txt | 2 + docs/features/search/search-engines.md | 20 +++++ .../config.d.ts | 5 ++ .../report.api.md | 4 +- .../src/engines/ElasticSearchClientWrapper.ts | 2 +- .../src/engines/ElasticSearchSearchEngine.ts | 3 + .../ElasticSearchSearchEngineIndexer.test.ts | 76 +++++++++++++++++++ .../ElasticSearchSearchEngineIndexer.ts | 6 +- 9 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 .changeset/stale-adults-hammer.md diff --git a/.changeset/stale-adults-hammer.md b/.changeset/stale-adults-hammer.md new file mode 100644 index 0000000000..568973b368 --- /dev/null +++ b/.changeset/stale-adults-hammer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-elasticsearch': patch +--- + +Added support for batchKeyField in the Elasticsearch indexer to allow consistent document IDs during bulk uploads. diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index b0225be547..8f3dcb65ff 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -249,6 +249,8 @@ lockdown lockfile lockfiles lookbehind +lookup +lookups lowercased lunr Luxon diff --git a/docs/features/search/search-engines.md b/docs/features/search/search-engines.md index 1d66a6ae97..07cf11aae5 100644 --- a/docs/features/search/search-engines.md +++ b/docs/features/search/search-engines.md @@ -239,6 +239,26 @@ search: > You can also increase the batch size if you are using a large ES instance. +### Elasticsearch batch key field + +By default, during bulk uploads with the Elasticsearch indexer, each document is assigned an auto-generated `_id` unless a `batchKeyField` is explicitly set. This configuration is optional and most users won’t need to customize it. However, if your use case involves frequent lookups or updates to existing documents, setting `batchKeyField` can be beneficial. It allows you to define a consistent identifier for each document, helping to streamline updates and prevent duplicate entries. Be aware that if the value provided for `batchKeyField` is not unique across documents, Elasticsearch will overwrite any existing document with the same `_id`. + +**Using `batchKeyField` (Custom `_id`)** + +```yaml +search: + elasticsearch: + batchKeyField: document_id +``` + +**Default Behavior (Auto-generated `_id`)** + +```yaml +search: + elasticsearch: + # No batchKeyField specified — Elasticsearch will autogenerate _id +``` + ### Elasticsearch Index Name Customization By default, the Elasticsearch indexer creates index names based on their type, a separator, and the current date as a postfix. You can configure a custom prefix for all indices by adding the following section to your app configuration. diff --git a/plugins/search-backend-module-elasticsearch/config.d.ts b/plugins/search-backend-module-elasticsearch/config.d.ts index 731ad1c77e..00af29e372 100644 --- a/plugins/search-backend-module-elasticsearch/config.d.ts +++ b/plugins/search-backend-module-elasticsearch/config.d.ts @@ -29,6 +29,11 @@ export interface Config { * Batch size for elastic search indexing tasks. Defaults to 1000. */ batchSize?: number; + /** + * Defines the name of the field in each document that will be used to identify documents during a batch upload. + * If not provided, a custom ID will be generated for each document. + */ + batchKeyField?: string; /** * Options for configuring highlight settings * See https://www.elastic.co/guide/en/elasticsearch/reference/7.17/highlighting.html diff --git a/plugins/search-backend-module-elasticsearch/report.api.md b/plugins/search-backend-module-elasticsearch/report.api.md index 19c44bd566..94cce4b319 100644 --- a/plugins/search-backend-module-elasticsearch/report.api.md +++ b/plugins/search-backend-module-elasticsearch/report.api.md @@ -137,7 +137,7 @@ export class ElasticSearchClientWrapper { // (undocumented) bulk(bulkOptions: { datasource: Readable; - onDocument: () => ElasticSearchIndexAction; + onDocument: (doc: any) => ElasticSearchIndexAction; refreshOnCompletion?: string | boolean; }): BulkHelper; // (undocumented) @@ -348,6 +348,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { indexPrefix: string, logger: LoggerService, batchSize: number, + batchKeyField?: string | undefined, highlightOptions?: ElasticSearchHighlightOptions, queryOptions?: ElasticSearchQueryConfig, ); @@ -393,6 +394,7 @@ export type ElasticSearchSearchEngineIndexerOptions = { logger: LoggerService; elasticSearchClientWrapper: ElasticSearchClientWrapper; batchSize: number; + batchKeyField?: string; skipRefresh?: boolean; }; diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientWrapper.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientWrapper.ts index 4bf8a3d998..14539b716f 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientWrapper.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientWrapper.ts @@ -115,7 +115,7 @@ export class ElasticSearchClientWrapper { bulk(bulkOptions: { datasource: Readable; - onDocument: () => ElasticSearchIndexAction; + onDocument: (doc: any) => ElasticSearchIndexAction; refreshOnCompletion?: string | boolean; }) { if (this.openSearchClient) { diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 2bd5c88f89..2e95385eb4 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -142,6 +142,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { private readonly indexPrefix: string, private readonly logger: LoggerService, private readonly batchSize: number, + private readonly batchKeyField?: string, highlightOptions?: ElasticSearchHighlightOptions, queryOptions?: ElasticSearchQueryConfig, ) { @@ -189,6 +190,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { logger, config.getOptionalNumber('search.elasticsearch.batchSize') ?? DEFAULT_INDEXER_BATCH_SIZE, + config.getOptionalString('search.elasticsearch.batchKeyField'), config.getOptional( 'search.elasticsearch.highlightOptions', ), @@ -345,6 +347,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { elasticSearchClientWrapper: this.elasticSearchClientWrapper, logger: indexerLogger, batchSize: this.batchSize, + batchKeyField: this.batchKeyField, skipRefresh: ( this diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts index 1590d5fb2a..0ba2882c61 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts @@ -351,4 +351,80 @@ describe('ElasticSearchSearchEngineIndexer', () => { expect(bulkSpy).toHaveBeenCalledTimes(1); expect(refreshSpy).toHaveBeenCalledTimes(0); }); + + it('indexes documents with custom batch key field', async () => { + indexer = new ElasticSearchSearchEngineIndexer({ + type: 'some-type', + indexPrefix: '', + indexSeparator: '-index__', + alias: 'some-type-index__search', + logger: mockServices.logger.mock(), + elasticSearchClientWrapper: clientWrapper, + batchSize: 1000, + skipRefresh: false, + batchKeyField: 'customId', + }); + + const documents = [ + { + title: 'testTerm', + text: 'testText', + location: 'test/location', + customId: '123', + }, + { + title: 'Another test', + text: 'Some more text', + location: 'test/location/2', + customId: '456', + }, + ]; + + await TestPipeline.fromIndexer(indexer).withDocuments(documents).execute(); + + expect(bulkSpy).toHaveBeenCalled(); + + const bulkBody = bulkSpy.mock.calls[0][0].body; + + expect(bulkBody[0]).toEqual( + expect.objectContaining({ + _id: '123', + index: expect.objectContaining({ + _index: expect.stringContaining('some-type-index__'), + }), + }), + ); + expect(bulkBody[2]).toEqual( + expect.objectContaining({ + _id: '456', + index: expect.objectContaining({ + _index: expect.stringContaining('some-type-index__'), + }), + }), + ); + }, 40000); + + it('indexes documents without custom batch key field when not specified', async () => { + const documents = [ + { + title: 'testTerm', + text: 'testText', + location: 'test/location', + customId: '123', + }, + ]; + + await TestPipeline.fromIndexer(indexer).withDocuments(documents).execute(); + + const bulkBody = bulkSpy.mock.calls[0][0].body; + + expect(bulkBody[0]).not.toHaveProperty('_id'); + expect(bulkBody[0]).toEqual( + expect.objectContaining({ + index: expect.objectContaining({ + _index: expect.stringContaining('some-type-index__'), + }), + }), + ); + }); }); diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts index 75b880af67..b57524e13a 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts @@ -32,6 +32,7 @@ export type ElasticSearchSearchEngineIndexerOptions = { logger: LoggerService; elasticSearchClientWrapper: ElasticSearchClientWrapper; batchSize: number; + batchKeyField?: string; skipRefresh?: boolean; }; @@ -87,10 +88,13 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { // documents have been successfully written to ES. this.bulkResult = this.elasticSearchClientWrapper.bulk({ datasource: this.sourceStream, - onDocument() { + onDocument(doc) { that.processed++; return { index: { _index: that.indexName }, + ...(options.batchKeyField && doc[options.batchKeyField] + ? { _id: doc[options.batchKeyField] } + : {}), }; }, refreshOnCompletion: options.skipRefresh !== true, From 31540c24c3be2b8e1dabfab274fce34c6073ba90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 17:13:11 +0000 Subject: [PATCH 202/233] chore(deps-dev): bump vite from 7.1.2 to 7.1.5 Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.2 to 7.1.5. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.1.5/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 7.1.5 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 40 +++++++++++++++++++++++++++++++--------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 4b74eda8a9..ee484c917a 100644 --- a/package.json +++ b/package.json @@ -165,7 +165,7 @@ "storybook": "^9.1.5", "typedoc": "^0.28.0", "typescript": "~5.7.0", - "vite": "^7.1.2" + "vite": "^7.1.5" }, "packageManager": "yarn@4.8.1", "engines": { diff --git a/yarn.lock b/yarn.lock index 3a5aa9ac30..b34a88fa24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30275,7 +30275,7 @@ __metadata: languageName: node linkType: hard -"fdir@npm:^6.4.4, fdir@npm:^6.4.6": +"fdir@npm:^6.4.4": version: 6.4.6 resolution: "fdir@npm:6.4.6" peerDependencies: @@ -30287,6 +30287,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.5.0": + version: 6.5.0 + resolution: "fdir@npm:6.5.0" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10/14ca1c9f0a0e8f4f2e9bf4e8551065a164a09545dae548c12a18d238b72e51e5a7b39bd8e5494b56463a0877672d0a6c1ef62c6fa0677db1b0c847773be939b1 + languageName: node + linkType: hard + "fecha@npm:^4.2.0": version: 4.2.0 resolution: "fecha@npm:4.2.0" @@ -44174,7 +44186,7 @@ __metadata: storybook: "npm:^9.1.5" typedoc: "npm:^0.28.0" typescript: "npm:~5.7.0" - vite: "npm:^7.1.2" + vite: "npm:^7.1.5" yaml: "npm:^2.7.0" languageName: unknown linkType: soft @@ -46848,7 +46860,17 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.9": +"tinyglobby@npm:^0.2.15": + version: 0.2.15 + resolution: "tinyglobby@npm:0.2.15" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.3" + checksum: 10/d72bd826a8b0fa5fa3929e7fe5ba48fceb2ae495df3a231b6c5408cd7d8c00b58ab5a9c2a76ba56a62ee9b5e083626f1f33599734bed1ffc4b792406408f0ca2 + languageName: node + linkType: hard + +"tinyglobby@npm:^0.2.9": version: 0.2.14 resolution: "tinyglobby@npm:0.2.14" dependencies: @@ -48656,17 +48678,17 @@ __metadata: languageName: node linkType: hard -"vite@npm:^7.1.2": - version: 7.1.2 - resolution: "vite@npm:7.1.2" +"vite@npm:^7.1.5": + version: 7.1.5 + resolution: "vite@npm:7.1.5" dependencies: esbuild: "npm:^0.25.0" - fdir: "npm:^6.4.6" + fdir: "npm:^6.5.0" fsevents: "npm:~2.3.3" picomatch: "npm:^4.0.3" postcss: "npm:^8.5.6" rollup: "npm:^4.43.0" - tinyglobby: "npm:^0.2.14" + tinyglobby: "npm:^0.2.15" peerDependencies: "@types/node": ^20.19.0 || >=22.12.0 jiti: ">=1.21.0" @@ -48707,7 +48729,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10/942188896db181bd5f95c576303eb617cd08580eb129d0223bc87a8ed9b8dc0d60d8487c077fd9a01087c55600af132c5fc677e51b4b48a4a54a376056969edb + checksum: 10/59edeef7e98757a668b2ad8a1731a5657fa83e22a165a36b7359225ea98a9be39b2f486710c0cf5085edb85daee7c8b6b6b0bd85d0ef32a1aa84aef71aabd0f0 languageName: node linkType: hard From 133ac7ad669b1e74e4156ef5780d5e61f5daca0b Mon Sep 17 00:00:00 2001 From: Paul Schultz Date: Tue, 2 Sep 2025 15:18:35 -0500 Subject: [PATCH 203/233] fix(repo-tools): resolve knip-reports failure with spaces in workspace paths Signed-off-by: Paul Schultz --- .changeset/fix-knip-reports-spaces-path.md | 5 + .../app-next-example-plugin/knip-report.md | 4 +- packages/app-next/knip-report.md | 62 ++++++------ packages/app/knip-report.md | 24 ++--- packages/backend-app-api/knip-report.md | 2 +- packages/backend-defaults/knip-report.md | 6 +- .../knip-report.md | 20 ++-- packages/backend-openapi-utils/knip-report.md | 2 +- packages/backend-plugin-api/knip-report.md | 2 +- packages/backend-test-utils/knip-report.md | 21 ++-- packages/backend/knip-report.md | 16 +-- packages/catalog-model/knip-report.md | 2 +- packages/cli-node/knip-report.md | 2 +- packages/cli/knip-report.md | 97 ++++++++++--------- packages/core-app-api/knip-report.md | 12 +-- packages/core-compat-api/knip-report.md | 6 +- packages/core-components/knip-report.md | 14 +-- packages/core-plugin-api/knip-report.md | 2 +- packages/create-app/knip-report.md | 2 +- packages/e2e-test-utils/knip-report.md | 2 +- packages/e2e-test/knip-report.md | 2 +- packages/frontend-app-api/knip-report.md | 15 ++- packages/frontend-defaults/knip-report.md | 2 +- .../knip-report.md | 2 +- packages/frontend-internal/knip-report.md | 14 +-- packages/frontend-plugin-api/knip-report.md | 8 +- packages/frontend-test-utils/knip-report.md | 4 +- packages/integration-aws-node/knip-report.md | 8 +- packages/integration-react/knip-report.md | 4 +- packages/opaque-internal/knip-report.md | 4 +- packages/repo-tools/knip-report.md | 14 +-- .../commands/knip-reports/knip-extractor.ts | 12 ++- packages/repo-tools/src/commands/util.ts | 59 +++++++---- .../techdocs-cli-embedded-app/knip-report.md | 10 +- packages/techdocs-cli/knip-report.md | 6 +- packages/theme/knip-report.md | 2 +- packages/ui/knip-report.md | 30 +++--- packages/version-bridge/knip-report.md | 2 +- .../knip-report.md | 4 +- plugins/api-docs/knip-report.md | 10 +- plugins/app-backend/knip-report.md | 6 +- plugins/app/knip-report.md | 16 +-- .../knip-report.md | 4 +- .../knip-report.md | 2 +- .../knip-report.md | 2 +- .../knip-report.md | 4 +- .../knip-report.md | 2 +- .../knip-report.md | 4 +- .../knip-report.md | 6 +- .../knip-report.md | 2 +- .../knip-report.md | 2 +- .../knip-report.md | 10 +- .../knip-report.md | 6 +- .../knip-report.md | 4 +- .../knip-report.md | 6 +- .../knip-report.md | 4 +- plugins/auth-react/knip-report.md | 2 +- plugins/bitbucket-cloud-common/knip-report.md | 4 +- .../catalog-backend-module-aws/knip-report.md | 6 +- .../knip-report.md | 2 +- .../knip-report.md | 4 +- .../knip-report.md | 4 +- .../catalog-backend-module-gcp/knip-report.md | 2 +- .../knip-report.md | 2 +- .../knip-report.md | 2 +- .../knip-report.md | 14 +-- .../knip-report.md | 2 +- .../knip-report.md | 2 +- .../knip-report.md | 2 +- .../knip-report.md | 2 +- .../knip-report.md | 2 +- .../knip-report.md | 2 +- .../knip-report.md | 4 +- plugins/catalog-backend/knip-report.md | 2 +- plugins/catalog-graph/knip-report.md | 2 +- plugins/catalog-react/knip-report.md | 2 +- .../knip-report.md | 2 +- plugins/catalog/knip-report.md | 2 +- plugins/devtools-backend/knip-report.md | 6 +- .../knip-report.md | 2 +- plugins/events-backend/knip-report.md | 2 +- plugins/events-node/knip-report.md | 2 +- plugins/example-todo-list/knip-report.md | 4 +- plugins/home-react/knip-report.md | 2 +- plugins/kubernetes-backend/knip-report.md | 21 ++-- plugins/kubernetes-cluster/knip-report.md | 12 +-- plugins/kubernetes-node/knip-report.md | 4 +- plugins/kubernetes/knip-report.md | 22 ++--- plugins/mcp-actions-backend/knip-report.md | 4 +- .../knip-report.md | 2 +- .../knip-report.md | 10 +- plugins/notifications-backend/knip-report.md | 10 +- plugins/notifications-common/knip-report.md | 2 +- plugins/notifications-node/knip-report.md | 10 +- plugins/notifications/knip-report.md | 8 +- plugins/org-react/knip-report.md | 2 +- plugins/org/knip-report.md | 2 +- .../knip-report.md | 4 +- plugins/permission-backend/knip-report.md | 2 +- plugins/proxy-backend/knip-report.md | 2 +- plugins/proxy-node/knip-report.md | 4 +- .../knip-report.md | 2 +- .../knip-report.md | 2 +- .../knip-report.md | 6 +- .../knip-report.md | 6 +- .../knip-report.md | 2 +- .../knip-report.md | 2 +- .../knip-report.md | 2 +- .../knip-report.md | 2 +- plugins/scaffolder-backend/knip-report.md | 30 +++--- .../scaffolder-node-test-utils/knip-report.md | 6 +- plugins/scaffolder-react/knip-report.md | 4 +- plugins/scaffolder/knip-report.md | 10 +- plugins/search-backend/knip-report.md | 4 +- plugins/search-react/knip-report.md | 2 +- plugins/signals-backend/knip-report.md | 10 +- plugins/signals-node/knip-report.md | 12 +-- plugins/signals-react/knip-report.md | 6 +- plugins/signals/knip-report.md | 12 +-- .../techdocs-addons-test-utils/knip-report.md | 4 +- plugins/techdocs-backend/knip-report.md | 10 +- plugins/techdocs-react/knip-report.md | 4 +- plugins/techdocs/knip-report.md | 2 +- plugins/user-settings-backend/knip-report.md | 2 +- 124 files changed, 496 insertions(+), 448 deletions(-) create mode 100644 .changeset/fix-knip-reports-spaces-path.md diff --git a/.changeset/fix-knip-reports-spaces-path.md b/.changeset/fix-knip-reports-spaces-path.md new file mode 100644 index 0000000000..95064c9274 --- /dev/null +++ b/.changeset/fix-knip-reports-spaces-path.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Fixed knip-reports command failing when workspace path contains spaces and process termination issues by replacing `execFile` with `spawn` and removing `shell` option. diff --git a/packages/app-next-example-plugin/knip-report.md b/packages/app-next-example-plugin/knip-report.md index e01cd18c93..0f2b9c5ebc 100644 --- a/packages/app-next-example-plugin/knip-report.md +++ b/packages/app-next-example-plugin/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :---------- | :----------- | :------- | -| cross-fetch | package.json | error | -| msw | package.json | error | +| cross-fetch | packages/app-next-example-plugin/package.json | error | +| msw | packages/app-next-example-plugin/package.json | error | diff --git a/packages/app-next/knip-report.md b/packages/app-next/knip-report.md index de7de83ffc..8513269386 100644 --- a/packages/app-next/knip-report.md +++ b/packages/app-next/knip-report.md @@ -4,40 +4,40 @@ | Name | Location | Severity | | :----------------------------------------------- | :----------- | :------- | -| @backstage/plugin-techdocs-module-addons-contrib | package.json | error | -| @backstage/plugin-catalog-unprocessed-entities | package.json | error | -| @backstage/plugin-kubernetes-cluster | package.json | error | -| @backstage/plugin-permission-react | package.json | error | -| @backstage/plugin-scaffolder-react | package.json | error | -| @backstage/plugin-catalog-common | package.json | error | -| @backstage/plugin-techdocs-react | package.json | error | -| @backstage/plugin-catalog-graph | package.json | error | -| @backstage/plugin-search-common | package.json | error | -| @backstage/plugin-search-react | package.json | error | -| @backstage/integration-react | package.json | error | -| @backstage/plugin-auth-react | package.json | error | -| @backstage/plugin-scaffolder | package.json | error | -| @backstage/core-plugin-api | package.json | error | -| @backstage/plugin-api-docs | package.json | error | -| @backstage/plugin-catalog | package.json | error | -| @backstage/plugin-signals | package.json | error | -| @backstage/app-defaults | package.json | error | -| @backstage/plugin-app | package.json | error | -| @backstage/plugin-org | package.json | error | -| @backstage/config | package.json | error | -| @material-ui/lab | package.json | error | -| zen-observable | package.json | error | -| @octokit/rest | package.json | error | -| react-use | package.json | error | -| history | package.json | error | +| @backstage/plugin-techdocs-module-addons-contrib | packages/app-next/package.json | error | +| @backstage/plugin-catalog-unprocessed-entities | packages/app-next/package.json | error | +| @backstage/plugin-kubernetes-cluster | packages/app-next/package.json | error | +| @backstage/plugin-permission-react | packages/app-next/package.json | error | +| @backstage/plugin-scaffolder-react | packages/app-next/package.json | error | +| @backstage/plugin-catalog-common | packages/app-next/package.json | error | +| @backstage/plugin-techdocs-react | packages/app-next/package.json | error | +| @backstage/plugin-catalog-graph | packages/app-next/package.json | error | +| @backstage/plugin-search-common | packages/app-next/package.json | error | +| @backstage/plugin-search-react | packages/app-next/package.json | error | +| @backstage/integration-react | packages/app-next/package.json | error | +| @backstage/plugin-auth-react | packages/app-next/package.json | error | +| @backstage/plugin-scaffolder | packages/app-next/package.json | error | +| @backstage/core-plugin-api | packages/app-next/package.json | error | +| @backstage/plugin-api-docs | packages/app-next/package.json | error | +| @backstage/plugin-catalog | packages/app-next/package.json | error | +| @backstage/plugin-signals | packages/app-next/package.json | error | +| @backstage/app-defaults | packages/app-next/package.json | error | +| @backstage/plugin-app | packages/app-next/package.json | error | +| @backstage/plugin-org | packages/app-next/package.json | error | +| @backstage/config | packages/app-next/package.json | error | +| @material-ui/lab | packages/app-next/package.json | error | +| zen-observable | packages/app-next/package.json | error | +| @octokit/rest | packages/app-next/package.json | error | +| react-use | packages/app-next/package.json | error | +| history | packages/app-next/package.json | error | ## Unused devDependencies (5) | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @testing-library/user-event | package.json | error | -| @types/zen-observable | package.json | error | -| @testing-library/dom | package.json | error | -| @types/jquery | package.json | error | -| cross-env | package.json | error | +| @testing-library/user-event | packages/app-next/package.json | error | +| @types/zen-observable | packages/app-next/package.json | error | +| @testing-library/dom | packages/app-next/package.json | error | +| @types/jquery | packages/app-next/package.json | error | +| cross-env | packages/app-next/package.json | error | diff --git a/packages/app/knip-report.md b/packages/app/knip-report.md index b05d232915..753f119c52 100644 --- a/packages/app/knip-report.md +++ b/packages/app/knip-report.md @@ -4,26 +4,26 @@ | Name | Location | Severity | | :------------------------------ | :----------- | :------- | -| @backstage/plugin-search-common | package.json | error | -| @backstage/plugin-auth-react | package.json | error | -| @backstage/frontend-app-api | package.json | error | -| @material-ui/lab | package.json | error | -| zen-observable | package.json | error | -| @octokit/rest | package.json | error | -| react-router | package.json | error | -| history | package.json | error | +| @backstage/plugin-search-common | packages/app/package.json | error | +| @backstage/plugin-auth-react | packages/app/package.json | error | +| @backstage/frontend-app-api | packages/app/package.json | error | +| @material-ui/lab | packages/app/package.json | error | +| zen-observable | packages/app/package.json | error | +| @octokit/rest | packages/app/package.json | error | +| react-router | packages/app/package.json | error | +| history | packages/app/package.json | error | ## Unused devDependencies (3) | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @testing-library/user-event | package.json | error | -| @types/zen-observable | package.json | error | -| @types/jquery | package.json | error | +| @testing-library/user-event | packages/app/package.json | error | +| @types/zen-observable | packages/app/package.json | error | +| @types/jquery | packages/app/package.json | error | ## Unlisted dependencies (1) | Name | Location | Severity | | :---------- | :------------------------------------------------------- | :------- | -| @rjsf/utils | src/components/scaffolder/customScaffolderExtensions.tsx | error | +| @rjsf/utils | packages/app/src/components/scaffolder/customScaffolderExtensions.tsx | error | diff --git a/packages/backend-app-api/knip-report.md b/packages/backend-app-api/knip-report.md index 13b4ec495e..d056972992 100644 --- a/packages/backend-app-api/knip-report.md +++ b/packages/backend-app-api/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/backend-defaults | package.json | error | +| @backstage/backend-defaults | packages/backend-app-api/package.json | error | diff --git a/packages/backend-defaults/knip-report.md b/packages/backend-defaults/knip-report.md index 169963316a..8951112aaa 100644 --- a/packages/backend-defaults/knip-report.md +++ b/packages/backend-defaults/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :------------- | :----------- | :------- | -| better-sqlite3 | package.json | error | -| mysql2 | package.json | error | +| better-sqlite3 | packages/backend-defaults/package.json | error | +| mysql2 | packages/backend-defaults/package.json | error | ## Referenced optional peerDependencies (1) | Name | Location | Severity | | :-------------------------------- | :----------- | :------- | -| @google-cloud/cloud-sql-connector | package.json | error | +| @google-cloud/cloud-sql-connector | packages/backend-defaults/package.json | error | diff --git a/packages/backend-dynamic-feature-service/knip-report.md b/packages/backend-dynamic-feature-service/knip-report.md index 55e3fd302b..01af03f52e 100644 --- a/packages/backend-dynamic-feature-service/knip-report.md +++ b/packages/backend-dynamic-feature-service/knip-report.md @@ -4,14 +4,14 @@ | Name | Location | Severity | | :------------------------------------ | :----------- | :------- | -| @backstage/plugin-search-backend-node | package.json | error | -| @backstage/plugin-permission-common | package.json | error | -| @backstage/plugin-catalog-backend | package.json | error | -| @backstage/plugin-permission-node | package.json | error | -| @backstage/plugin-scaffolder-node | package.json | error | -| @backstage/plugin-events-backend | package.json | error | -| @backstage/plugin-search-common | package.json | error | -| @backstage/plugin-events-node | package.json | error | -| @backstage/plugin-auth-node | package.json | error | -| express-promise-router | package.json | error | +| @backstage/plugin-search-backend-node | packages/backend-dynamic-feature-service/package.json | error | +| @backstage/plugin-permission-common | packages/backend-dynamic-feature-service/package.json | error | +| @backstage/plugin-catalog-backend | packages/backend-dynamic-feature-service/package.json | error | +| @backstage/plugin-permission-node | packages/backend-dynamic-feature-service/package.json | error | +| @backstage/plugin-scaffolder-node | packages/backend-dynamic-feature-service/package.json | error | +| @backstage/plugin-events-backend | packages/backend-dynamic-feature-service/package.json | error | +| @backstage/plugin-search-common | packages/backend-dynamic-feature-service/package.json | error | +| @backstage/plugin-events-node | packages/backend-dynamic-feature-service/package.json | error | +| @backstage/plugin-auth-node | packages/backend-dynamic-feature-service/package.json | error | +| express-promise-router | packages/backend-dynamic-feature-service/package.json | error | diff --git a/packages/backend-openapi-utils/knip-report.md b/packages/backend-openapi-utils/knip-report.md index cf540a8d1d..d2f286fde1 100644 --- a/packages/backend-openapi-utils/knip-report.md +++ b/packages/backend-openapi-utils/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---------------------------- | :----------- | :------- | -| @backstage/backend-plugin-api | package.json | error | +| @backstage/backend-plugin-api | packages/backend-openapi-utils/package.json | error | diff --git a/packages/backend-plugin-api/knip-report.md b/packages/backend-plugin-api/knip-report.md index 89988530e6..869066c9f6 100644 --- a/packages/backend-plugin-api/knip-report.md +++ b/packages/backend-plugin-api/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/plugin-auth-node | package.json | error | +| @backstage/plugin-auth-node | packages/backend-plugin-api/package.json | error | diff --git a/packages/backend-test-utils/knip-report.md b/packages/backend-test-utils/knip-report.md index 6ba3e32cd6..860e474541 100644 --- a/packages/backend-test-utils/knip-report.md +++ b/packages/backend-test-utils/knip-report.md @@ -1,13 +1,20 @@ # Knip report -## Unused dependencies (6) +## Unused dependencies (7) | Name | Location | Severity | | :------------------------------- | :----------- | :------- | -| @types/express-serve-static-core | package.json | error | -| @backstage/plugin-auth-node | package.json | error | -| better-sqlite3 | package.json | error | -| @types/qs | package.json | error | -| mysql2 | package.json | error | -| pg | package.json | error | +| @types/express-serve-static-core | packages/backend-test-utils/package.json | error | +| @backstage/plugin-auth-node | packages/backend-test-utils/package.json | error | +| better-sqlite3 | packages/backend-test-utils/package.json | error | +| @types/qs | packages/backend-test-utils/package.json | error | +| lodash | packages/backend-test-utils/package.json | error | +| mysql2 | packages/backend-test-utils/package.json | error | +| pg | packages/backend-test-utils/package.json | error | + +## Unused devDependencies (1) + +| Name | Location | Severity | +| :------------ | :----------- | :------- | +| @types/lodash | packages/backend-test-utils/package.json | error | diff --git a/packages/backend/knip-report.md b/packages/backend/knip-report.md index d24322fd2b..2410758b6d 100644 --- a/packages/backend/knip-report.md +++ b/packages/backend/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :----------------------------------------------- | :----------- | :------- | -| @backstage/plugin-catalog-backend-module-openapi | package.json | error | -| @opentelemetry/auto-instrumentations-node | package.json | error | -| @backstage/plugin-search-backend-node | package.json | error | -| @backstage/plugin-permission-common | package.json | error | -| @opentelemetry/exporter-prometheus | package.json | error | -| @backstage/plugin-permission-node | package.json | error | -| @opentelemetry/sdk-node | package.json | error | -| example-app | package.json | error | +| @backstage/plugin-catalog-backend-module-openapi | packages/backend/package.json | error | +| @opentelemetry/auto-instrumentations-node | packages/backend/package.json | error | +| @backstage/plugin-search-backend-node | packages/backend/package.json | error | +| @backstage/plugin-permission-common | packages/backend/package.json | error | +| @opentelemetry/exporter-prometheus | packages/backend/package.json | error | +| @backstage/plugin-permission-node | packages/backend/package.json | error | +| @opentelemetry/sdk-node | packages/backend/package.json | error | +| example-app | packages/backend/package.json | error | diff --git a/packages/catalog-model/knip-report.md b/packages/catalog-model/knip-report.md index 9285fce097..a03ecb8555 100644 --- a/packages/catalog-model/knip-report.md +++ b/packages/catalog-model/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :----------------- | :----------- | :------- | -| @types/json-schema | package.json | error | +| @types/json-schema | packages/catalog-model/package.json | error | diff --git a/packages/cli-node/knip-report.md b/packages/cli-node/knip-report.md index 4eeac45d5b..2ada6fe6ce 100644 --- a/packages/cli-node/knip-report.md +++ b/packages/cli-node/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :----- | :----------- | :------- | -| semver | package.json | error | +| semver | packages/cli-node/package.json | error | diff --git a/packages/cli/knip-report.md b/packages/cli/knip-report.md index 8781b02f01..1073617a2c 100644 --- a/packages/cli/knip-report.md +++ b/packages/cli/knip-report.md @@ -1,65 +1,68 @@ # Knip report -## Unused dependencies (24) +## Unused dependencies (22) | Name | Location | Severity | | :--------------------------- | :----------- | :------- | -| @spotify/eslint-config-react | package.json | error | -| eslint-formatter-friendly | package.json | error | -| eslint-plugin-react-hooks | package.json | error | -| @backstage/catalog-model | package.json | error | -| @octokit/graphql-schema | package.json | error | -| @sucrase/webpack-loader | package.json | error | -| @backstage/integration | package.json | error | -| eslint-plugin-jsx-a11y | package.json | error | -| jest-environment-jsdom | package.json | error | -| terser-webpack-plugin | package.json | error | -| eslint-plugin-react | package.json | error | -| @octokit/oauth-app | package.json | error | -| @types/webpack-env | package.json | error | -| @octokit/graphql | package.json | error | -| jest-css-modules | package.json | error | -| git-url-parse | package.json | error | -| jest-runtime | package.json | error | -| cross-fetch | package.json | error | -| @swc/jest | package.json | error | -| process | package.json | error | -| sucrase | package.json | error | -| buffer | package.json | error | -| glob | package.json | error | -| util | package.json | error | +| @spotify/eslint-config-react | packages/cli/package.json | error | +| eslint-formatter-friendly | packages/cli/package.json | error | +| eslint-plugin-react-hooks | packages/cli/package.json | error | +| @backstage/catalog-model | packages/cli/package.json | error | +| @octokit/graphql-schema | packages/cli/package.json | error | +| @backstage/integration | packages/cli/package.json | error | +| eslint-plugin-jsx-a11y | packages/cli/package.json | error | +| jest-environment-jsdom | packages/cli/package.json | error | +| eslint-plugin-react | packages/cli/package.json | error | +| @octokit/oauth-app | packages/cli/package.json | error | +| @types/webpack-env | packages/cli/package.json | error | +| @octokit/graphql | packages/cli/package.json | error | +| jest-css-modules | packages/cli/package.json | error | +| git-url-parse | packages/cli/package.json | error | +| jest-runtime | packages/cli/package.json | error | +| cross-fetch | packages/cli/package.json | error | +| @swc/jest | packages/cli/package.json | error | +| process | packages/cli/package.json | error | +| sucrase | packages/cli/package.json | error | +| buffer | packages/cli/package.json | error | +| glob | packages/cli/package.json | error | +| util | packages/cli/package.json | error | ## Unused devDependencies (14) | Name | Location | Severity | | :--------------------------------------------------- | :----------- | :------- | -| @backstage/plugin-auth-backend-module-guest-provider | package.json | error | -| @types/rollup-plugin-peer-deps-external | package.json | error | -| @backstage/plugin-auth-backend | package.json | error | -| @backstage/plugin-catalog-node | package.json | error | -| @types/terser-webpack-plugin | package.json | error | -| @backstage/core-components | package.json | error | -| @backstage/catalog-client | package.json | error | -| @backstage/core-app-api | package.json | error | -| @types/webpack-sources | package.json | error | -| @backstage/dev-utils | package.json | error | -| @types/http-proxy | package.json | error | -| @types/svgo | package.json | error | -| @types/ejs | package.json | error | -| del | package.json | error | +| @backstage/plugin-auth-backend-module-guest-provider | packages/cli/package.json | error | +| @types/rollup-plugin-peer-deps-external | packages/cli/package.json | error | +| @backstage/plugin-auth-backend | packages/cli/package.json | error | +| @types/terser-webpack-plugin | packages/cli/package.json | error | +| @backstage/core-components | packages/cli/package.json | error | +| @backstage/catalog-client | packages/cli/package.json | error | +| @backstage/core-app-api | packages/cli/package.json | error | +| @types/webpack-sources | packages/cli/package.json | error | +| terser-webpack-plugin | packages/cli/package.json | error | +| @backstage/dev-utils | packages/cli/package.json | error | +| @types/http-proxy | packages/cli/package.json | error | +| @types/svgo | packages/cli/package.json | error | +| @types/ejs | packages/cli/package.json | error | +| del | packages/cli/package.json | error | -## Referenced optional peerDependencies (3) +## Referenced optional peerDependencies (8) -| Name | Location | Severity | -| :--------------------------- | :----------- | :------- | -| @rspack/plugin-react-refresh | package.json | error | -| @rspack/dev-server | package.json | error | -| @rspack/core | package.json | error | +| Name | Location | Severity | +| :----------------------------------- | :----------- | :------- | +| @pmmmwh/react-refresh-webpack-plugin | packages/cli/package.json | error | +| fork-ts-checker-webpack-plugin | packages/cli/package.json | error | +| @module-federation/enhanced | packages/cli/package.json | error | +| mini-css-extract-plugin | packages/cli/package.json | error | +| eslint-webpack-plugin | packages/cli/package.json | error | +| webpack-dev-server | packages/cli/package.json | error | +| esbuild-loader | packages/cli/package.json | error | +| webpack | packages/cli/package.json | error | ## Unlisted dependencies (2) | Name | Location | Severity | | :-------- | :------------------------------------------------- | :------- | -| react-dom | src/modules/build/lib/bundler/hasReactDomClient.ts | error | -| react | src/modules/build/lib/bundler/server.ts | error | +| react-dom | packages/cli/src/modules/build/lib/bundler/hasReactDomClient.ts | error | +| react | packages/cli/src/modules/build/lib/bundler/server.ts | error | diff --git a/packages/core-app-api/knip-report.md b/packages/core-app-api/knip-report.md index 76616b6d60..9918534487 100644 --- a/packages/core-app-api/knip-report.md +++ b/packages/core-app-api/knip-report.md @@ -4,15 +4,15 @@ | Name | Location | Severity | | :------ | :----------- | :------- | -| history | package.json | error | +| history | packages/core-app-api/package.json | error | ## Unused devDependencies (5) | Name | Location | Severity | | :--------------------------- | :----------- | :------- | -| @testing-library/react-hooks | package.json | error | -| react-router-dom-stable | package.json | error | -| react-router-dom-beta | package.json | error | -| react-router-stable | package.json | error | -| react-router-beta | package.json | error | +| @testing-library/react-hooks | packages/core-app-api/package.json | error | +| react-router-dom-stable | packages/core-app-api/package.json | error | +| react-router-dom-beta | packages/core-app-api/package.json | error | +| react-router-stable | packages/core-app-api/package.json | error | +| react-router-beta | packages/core-app-api/package.json | error | diff --git a/packages/core-compat-api/knip-report.md b/packages/core-compat-api/knip-report.md index a5794ee10b..7d2feee1f4 100644 --- a/packages/core-compat-api/knip-report.md +++ b/packages/core-compat-api/knip-report.md @@ -4,7 +4,7 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/frontend-app-api | package.json | error | -| @backstage/types | package.json | error | -| zod | package.json | error | +| @backstage/frontend-app-api | packages/core-compat-api/package.json | error | +| @backstage/types | packages/core-compat-api/package.json | error | +| zod | packages/core-compat-api/package.json | error | diff --git a/packages/core-components/knip-report.md b/packages/core-components/knip-report.md index 69f3218a24..1f66f844fa 100644 --- a/packages/core-components/knip-report.md +++ b/packages/core-components/knip-report.md @@ -4,21 +4,21 @@ | Name | Location | Severity | | :-------- | :----------- | :------- | -| pluralize | package.json | error | +| pluralize | packages/core-components/package.json | error | ## Unused devDependencies (3) | Name | Location | Severity | | :---------------------- | :----------- | :------- | -| @backstage/app-defaults | package.json | error | -| @types/google-protobuf | package.json | error | -| cross-fetch | package.json | error | +| @backstage/app-defaults | packages/core-components/package.json | error | +| @types/google-protobuf | packages/core-components/package.json | error | +| cross-fetch | packages/core-components/package.json | error | ## Unlisted dependencies (3) | Name | Location | Severity | | :---------------- | :------------------------------------------------------ | :------- | -| copy-to-clipboard | src/components/LogViewer/useLogViewerSelection.test.tsx | error | -| copy-to-clipboard | src/components/LogViewer/RealLogViewer.test.tsx | error | -| csstype | src/components/Lifecycle/Lifecycle.tsx | error | +| copy-to-clipboard | packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx | error | +| copy-to-clipboard | packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx | error | +| csstype | packages/core-components/src/components/Lifecycle/Lifecycle.tsx | error | diff --git a/packages/core-plugin-api/knip-report.md b/packages/core-plugin-api/knip-report.md index 169117772f..3aaa33dfa4 100644 --- a/packages/core-plugin-api/knip-report.md +++ b/packages/core-plugin-api/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @testing-library/user-event | package.json | error | +| @testing-library/user-event | packages/core-plugin-api/package.json | error | diff --git a/packages/create-app/knip-report.md b/packages/create-app/knip-report.md index 864a5956ef..dc4b092da6 100644 --- a/packages/create-app/knip-report.md +++ b/packages/create-app/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-------------------- | :----------- | :------- | -| @types/command-exists | package.json | error | +| @types/command-exists | packages/create-app/package.json | error | diff --git a/packages/e2e-test-utils/knip-report.md b/packages/e2e-test-utils/knip-report.md index 5798802e35..b3b407dc65 100644 --- a/packages/e2e-test-utils/knip-report.md +++ b/packages/e2e-test-utils/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :--------------- | :----------- | :------- | -| @playwright/test | package.json | error | +| @playwright/test | packages/e2e-test-utils/package.json | error | diff --git a/packages/e2e-test/knip-report.md b/packages/e2e-test/knip-report.md index 33d38c8508..516a9d0872 100644 --- a/packages/e2e-test/knip-report.md +++ b/packages/e2e-test/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-------------------- | :----------- | :------- | -| @backstage/create-app | package.json | error | +| @backstage/create-app | packages/e2e-test/package.json | error | diff --git a/packages/frontend-app-api/knip-report.md b/packages/frontend-app-api/knip-report.md index b050438d7c..77a3cfea97 100644 --- a/packages/frontend-app-api/knip-report.md +++ b/packages/frontend-app-api/knip-report.md @@ -1,10 +1,17 @@ # Knip report -## Unused dependencies (3) +## Unused dependencies (4) | Name | Location | Severity | | :--------------------------- | :----------- | :------- | -| @backstage/frontend-defaults | package.json | error | -| @backstage/errors | package.json | error | -| zod | package.json | error | +| @backstage/frontend-defaults | packages/frontend-app-api/package.json | error | +| @backstage/version-bridge | packages/frontend-app-api/package.json | error | +| @backstage/errors | packages/frontend-app-api/package.json | error | +| zod | packages/frontend-app-api/package.json | error | + +## Unused devDependencies (1) + +| Name | Location | Severity | +| :----------------------------- | :----------- | :------- | +| @backstage/frontend-test-utils | packages/frontend-app-api/package.json | error | diff --git a/packages/frontend-defaults/knip-report.md b/packages/frontend-defaults/knip-report.md index e6696c0d8d..8c0979b2c4 100644 --- a/packages/frontend-defaults/knip-report.md +++ b/packages/frontend-defaults/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :--------------- | :----------- | :------- | -| @react-hookz/web | package.json | error | +| @react-hookz/web | packages/frontend-defaults/package.json | error | diff --git a/packages/frontend-dynamic-feature-loader/knip-report.md b/packages/frontend-dynamic-feature-loader/knip-report.md index 3d019810db..0645b2b1cc 100644 --- a/packages/frontend-dynamic-feature-loader/knip-report.md +++ b/packages/frontend-dynamic-feature-loader/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---------------- | :----------- | :------- | -| @backstage/config | package.json | error | +| @backstage/config | packages/frontend-dynamic-feature-loader/package.json | error | diff --git a/packages/frontend-internal/knip-report.md b/packages/frontend-internal/knip-report.md index edbbb970ac..e23233d94e 100644 --- a/packages/frontend-internal/knip-report.md +++ b/packages/frontend-internal/knip-report.md @@ -4,16 +4,16 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @backstage/version-bridge | package.json | error | -| zod | package.json | error | +| @backstage/version-bridge | packages/frontend-internal/package.json | error | +| zod | packages/frontend-internal/package.json | error | ## Unused devDependencies (5) | Name | Location | Severity | | :----------------------------- | :----------- | :------- | -| @backstage/frontend-test-utils | package.json | error | -| @backstage/frontend-app-api | package.json | error | -| @testing-library/jest-dom | package.json | error | -| @testing-library/react | package.json | error | -| @backstage/test-utils | package.json | error | +| @backstage/frontend-test-utils | packages/frontend-internal/package.json | error | +| @backstage/frontend-app-api | packages/frontend-internal/package.json | error | +| @testing-library/jest-dom | packages/frontend-internal/package.json | error | +| @testing-library/react | packages/frontend-internal/package.json | error | +| @backstage/test-utils | packages/frontend-internal/package.json | error | diff --git a/packages/frontend-plugin-api/knip-report.md b/packages/frontend-plugin-api/knip-report.md index 402fea3d47..d76f9ef7d9 100644 --- a/packages/frontend-plugin-api/knip-report.md +++ b/packages/frontend-plugin-api/knip-report.md @@ -4,13 +4,13 @@ | Name | Location | Severity | | :------------------------- | :----------- | :------- | -| @backstage/core-components | package.json | error | -| @material-ui/core | package.json | error | -| lodash | package.json | error | +| @backstage/core-components | packages/frontend-plugin-api/package.json | error | +| @material-ui/core | packages/frontend-plugin-api/package.json | error | +| lodash | packages/frontend-plugin-api/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/frontend-app-api | package.json | error | +| @backstage/frontend-app-api | packages/frontend-plugin-api/package.json | error | diff --git a/packages/frontend-test-utils/knip-report.md b/packages/frontend-test-utils/knip-report.md index 50f4f788e3..5415a4c648 100644 --- a/packages/frontend-test-utils/knip-report.md +++ b/packages/frontend-test-utils/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @backstage/version-bridge | package.json | error | -| zod | package.json | error | +| @backstage/version-bridge | packages/frontend-test-utils/package.json | error | +| zod | packages/frontend-test-utils/package.json | error | diff --git a/packages/integration-aws-node/knip-report.md b/packages/integration-aws-node/knip-report.md index 8927891173..efa9f826b6 100644 --- a/packages/integration-aws-node/knip-report.md +++ b/packages/integration-aws-node/knip-report.md @@ -4,13 +4,13 @@ | Name | Location | Severity | | :-------------------------------- | :----------- | :------- | -| @aws-sdk/credential-provider-node | package.json | error | -| @backstage/errors | package.json | error | +| @aws-sdk/credential-provider-node | packages/integration-aws-node/package.json | error | +| @backstage/errors | packages/integration-aws-node/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :----------------------- | :----------- | :------- | -| @backstage/config-loader | package.json | error | -| @backstage/test-utils | package.json | error | +| @backstage/config-loader | packages/integration-aws-node/package.json | error | +| @backstage/test-utils | packages/integration-aws-node/package.json | error | diff --git a/packages/integration-react/knip-report.md b/packages/integration-react/knip-report.md index 551f19ab2f..dd0df6e7f4 100644 --- a/packages/integration-react/knip-report.md +++ b/packages/integration-react/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------------------- | :----------- | :------- | -| @testing-library/dom | package.json | error | -| msw | package.json | error | +| @testing-library/dom | packages/integration-react/package.json | error | +| msw | packages/integration-react/package.json | error | diff --git a/packages/opaque-internal/knip-report.md b/packages/opaque-internal/knip-report.md index b131ea8948..b49ecc8f5f 100644 --- a/packages/opaque-internal/knip-report.md +++ b/packages/opaque-internal/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @testing-library/jest-dom | package.json | error | -| @testing-library/react | package.json | error | +| @testing-library/jest-dom | packages/opaque-internal/package.json | error | +| @testing-library/react | packages/opaque-internal/package.json | error | diff --git a/packages/repo-tools/knip-report.md b/packages/repo-tools/knip-report.md index 02f92286c1..f87d954145 100644 --- a/packages/repo-tools/knip-report.md +++ b/packages/repo-tools/knip-report.md @@ -4,21 +4,21 @@ | Name | Location | Severity | | :---------------------------------- | :----------- | :------- | -| @openapitools/openapi-generator-cli | package.json | error | -| @stoplight/spectral-runtime | package.json | error | -| @electric-sql/pglite | package.json | error | -| is-glob | package.json | error | +| @openapitools/openapi-generator-cli | packages/repo-tools/package.json | error | +| @stoplight/spectral-runtime | packages/repo-tools/package.json | error | +| @electric-sql/pglite | packages/repo-tools/package.json | error | +| is-glob | packages/repo-tools/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :------------- | :----------- | :------- | -| @types/is-glob | package.json | error | -| typedoc | package.json | error | +| @types/is-glob | packages/repo-tools/package.json | error | +| typedoc | packages/repo-tools/package.json | error | ## Referenced optional peerDependencies (1) | Name | Location | Severity | | :------- | :----------- | :------- | -| prettier | package.json | error | +| prettier | packages/repo-tools/package.json | error | diff --git a/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts b/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts index a97d1ddd8f..68bb0a9835 100644 --- a/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts +++ b/packages/repo-tools/src/commands/knip-reports/knip-extractor.ts @@ -106,13 +106,17 @@ async function handlePackage({ let report = await run( `${knipDir}/knip.js`, - `-W ${packageDir}`, // Run the desired workspace - '--config knip.json', + '-W', // Run the desired workspace + packageDir, + '--config', + 'knip.json', '--no-exit-code', // Removing this will end the process in case there are findings by knip '--no-progress', // Remove unnecessary debugging from output // TODO: Add more checks when dependencies start to look ok, see https://knip.dev/reference/cli#--include - '--include dependencies,unlisted', - '--reporter markdown', + '--include', + 'dependencies,unlisted', + '--reporter', + 'markdown', ); // Adjust report paths to be relative to workspace diff --git a/packages/repo-tools/src/commands/util.ts b/packages/repo-tools/src/commands/util.ts index ff4cdad1b8..c232a8aff2 100644 --- a/packages/repo-tools/src/commands/util.ts +++ b/packages/repo-tools/src/commands/util.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { execFile } from 'child_process'; +import { spawn } from 'child_process'; import os from 'os'; import pLimit from 'p-limit'; @@ -29,26 +29,43 @@ export function createBinRunner(cwd: string, path: string) { limiter( () => new Promise((resolve, reject) => { - execFile( - 'node', - [path, ...command], - { - cwd, - shell: true, - timeout: 3 * 60 * 1000, - maxBuffer: 1024 * 1024, - }, - (err, stdout, stderr) => { - if (err) { - console.log('err', err); - reject(new Error(`${err.message}\n${stderr}`)); - } else if (stderr) { - reject(new Error(`Command printed error output: ${stderr}`)); - } else { - resolve(stdout); - } - }, - ); + // Handle the case where path is empty and the script path is the first command argument + const args = path ? [path, ...command] : command; + const child = spawn('node', args, { + cwd, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + let stdout = ''; + let stderr = ''; + + child.stdout?.on('data', data => { + stdout += data.toString(); + }); + + child.stderr?.on('data', data => { + stderr += data.toString(); + }); + + child.on('error', err => { + reject(new Error(`Process error: ${err.message}`)); + }); + + child.on('close', (code, signal) => { + if (signal) { + reject( + new Error( + `Process was killed with signal ${signal}\n${stderr}`, + ), + ); + } else if (code !== 0) { + reject(new Error(`Process exited with code ${code}\n${stderr}`)); + } else if (stderr.trim()) { + reject(new Error(`Command printed error output: ${stderr}`)); + } else { + resolve(stdout); + } + }); }), ); } diff --git a/packages/techdocs-cli-embedded-app/knip-report.md b/packages/techdocs-cli-embedded-app/knip-report.md index 96aa361a87..3711ac996d 100644 --- a/packages/techdocs-cli-embedded-app/knip-report.md +++ b/packages/techdocs-cli-embedded-app/knip-report.md @@ -4,14 +4,14 @@ | Name | Location | Severity | | :-------- | :----------- | :------- | -| react-use | package.json | error | -| history | package.json | error | +| react-use | packages/techdocs-cli-embedded-app/package.json | error | +| history | packages/techdocs-cli-embedded-app/package.json | error | ## Unused devDependencies (3) | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @testing-library/user-event | package.json | error | -| @testing-library/dom | package.json | error | -| cross-env | package.json | error | +| @testing-library/user-event | packages/techdocs-cli-embedded-app/package.json | error | +| @testing-library/dom | packages/techdocs-cli-embedded-app/package.json | error | +| cross-env | packages/techdocs-cli-embedded-app/package.json | error | diff --git a/packages/techdocs-cli/knip-report.md b/packages/techdocs-cli/knip-report.md index 5948844a00..f13d2d7ab6 100644 --- a/packages/techdocs-cli/knip-report.md +++ b/packages/techdocs-cli/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :----------- | :----------- | :------- | -| global-agent | package.json | error | +| global-agent | packages/techdocs-cli/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| techdocs-cli-embedded-app | package.json | error | -| @types/webpack-env | package.json | error | +| techdocs-cli-embedded-app | packages/techdocs-cli/package.json | error | +| @types/webpack-env | packages/techdocs-cli/package.json | error | diff --git a/packages/theme/knip-report.md b/packages/theme/knip-report.md index 9aa0ae1d46..270d625e91 100644 --- a/packages/theme/knip-report.md +++ b/packages/theme/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :--------------- | :----------- | :------- | -| react-router-dom | package.json | error | +| react-router-dom | packages/theme/package.json | error | diff --git a/packages/ui/knip-report.md b/packages/ui/knip-report.md index c242c05e3f..3f3284a1a0 100644 --- a/packages/ui/knip-report.md +++ b/packages/ui/knip-report.md @@ -1,20 +1,22 @@ # Knip report -## Unused devDependencies (5) +## Unused dependencies (1) -| Name | Location | Severity | -| :------------------------------- | :----------- | :------- | -| @storybook/addon-styling-webpack | package.json | error | -| mini-css-extract-plugin | package.json | error | -| @storybook/blocks | package.json | error | -| globals | package.json | error | -| glob | package.json | error | +| Name | Location | Severity | +| :-------------------- | :----------- | :------- | +| @tanstack/react-table | packages/ui/package.json | error | -## Unlisted dependencies (3) +## Unused devDependencies (2) -| Name | Location | Severity | -| :--------------------- | :----------------------------------------- | :------- | -| @react-types/overlays | src/components/Tooltip/Tooltip.stories.tsx | error | -| react-aria | src/components/Menu/Combobox.tsx | error | -| @storybook/preview-api | .storybook/preview.tsx | error | +| Name | Location | Severity | +| :------ | :----------- | :------- | +| globals | packages/ui/package.json | error | +| glob | packages/ui/package.json | error | + +## Unlisted dependencies (2) + +| Name | Location | Severity | +| :-------------------- | :------------------------------------------- | :------- | +| react-stately | packages/ui/src/components/TagGroup/TagGroup.stories.tsx | error | +| @react-types/overlays | packages/ui/src/components/Tooltip/Tooltip.stories.tsx | error | diff --git a/packages/version-bridge/knip-report.md b/packages/version-bridge/knip-report.md index 9aa0ae1d46..2bdccfc4d2 100644 --- a/packages/version-bridge/knip-report.md +++ b/packages/version-bridge/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :--------------- | :----------- | :------- | -| react-router-dom | package.json | error | +| react-router-dom | packages/version-bridge/package.json | error | diff --git a/plugins/api-docs-module-protoc-gen-doc/knip-report.md b/plugins/api-docs-module-protoc-gen-doc/knip-report.md index 63b63f2f9b..5933dc809c 100644 --- a/plugins/api-docs-module-protoc-gen-doc/knip-report.md +++ b/plugins/api-docs-module-protoc-gen-doc/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :--------------- | :----------- | :------- | -| react-router-dom | package.json | error | -| react-dom | package.json | error | +| react-router-dom | plugins/api-docs-module-protoc-gen-doc/package.json | error | +| react-dom | plugins/api-docs-module-protoc-gen-doc/package.json | error | diff --git a/plugins/api-docs/knip-report.md b/plugins/api-docs/knip-report.md index 3c06f53535..406a44f365 100644 --- a/plugins/api-docs/knip-report.md +++ b/plugins/api-docs/knip-report.md @@ -4,14 +4,14 @@ | Name | Location | Severity | | :------------------- | :----------- | :------- | -| isomorphic-form-data | package.json | error | -| graphql-config | package.json | error | -| graphql-ws | package.json | error | +| isomorphic-form-data | plugins/api-docs/package.json | error | +| graphql-config | plugins/api-docs/package.json | error | +| graphql-ws | plugins/api-docs/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :---------------------- | :----------- | :------- | -| @backstage/core-app-api | package.json | error | -| @types/highlightjs | package.json | error | +| @backstage/core-app-api | plugins/api-docs/package.json | error | +| @types/highlightjs | plugins/api-docs/package.json | error | diff --git a/plugins/app-backend/knip-report.md b/plugins/app-backend/knip-report.md index 588eca0213..55ec2572b5 100644 --- a/plugins/app-backend/knip-report.md +++ b/plugins/app-backend/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/plugin-auth-node | package.json | error | -| yn | package.json | error | +| @backstage/plugin-auth-node | plugins/app-backend/package.json | error | +| yn | plugins/app-backend/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :------------------------- | :----------- | :------- | -| @backstage/backend-app-api | package.json | error | +| @backstage/backend-app-api | plugins/app-backend/package.json | error | diff --git a/plugins/app/knip-report.md b/plugins/app/knip-report.md index 9361e03abc..7b3dcb2e34 100644 --- a/plugins/app/knip-report.md +++ b/plugins/app/knip-report.md @@ -1,16 +1,18 @@ # Knip report -## Unused dependencies (2) +## Unused dependencies (4) -| Name | Location | Severity | -| :--------------- | :----------- | :------- | -| @material-ui/lab | package.json | error | -| react-use | package.json | error | +| Name | Location | Severity | +| :------------------------ | :----------- | :------- | +| @backstage/version-bridge | plugins/app/package.json | error | +| @material-ui/lab | plugins/app/package.json | error | +| react-use | plugins/app/package.json | error | +| zod | plugins/app/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :------------------- | :----------- | :------- | -| @backstage/dev-utils | package.json | error | -| msw | package.json | error | +| @backstage/dev-utils | plugins/app/package.json | error | +| msw | plugins/app/package.json | error | diff --git a/plugins/auth-backend-module-atlassian-provider/knip-report.md b/plugins/auth-backend-module-atlassian-provider/knip-report.md index 6624c86073..4604ddb386 100644 --- a/plugins/auth-backend-module-atlassian-provider/knip-report.md +++ b/plugins/auth-backend-module-atlassian-provider/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| passport | package.json | error | -| express | package.json | error | +| passport | plugins/auth-backend-module-atlassian-provider/package.json | error | +| express | plugins/auth-backend-module-atlassian-provider/package.json | error | diff --git a/plugins/auth-backend-module-aws-alb-provider/knip-report.md b/plugins/auth-backend-module-aws-alb-provider/knip-report.md index 14abcfe083..6f67a12ef6 100644 --- a/plugins/auth-backend-module-aws-alb-provider/knip-report.md +++ b/plugins/auth-backend-module-aws-alb-provider/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :----------------------------- | :----------- | :------- | -| @backstage/plugin-auth-backend | package.json | error | +| @backstage/plugin-auth-backend | plugins/auth-backend-module-aws-alb-provider/package.json | error | diff --git a/plugins/auth-backend-module-azure-easyauth-provider/knip-report.md b/plugins/auth-backend-module-azure-easyauth-provider/knip-report.md index d5c786591f..854da1aa31 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/knip-report.md +++ b/plugins/auth-backend-module-azure-easyauth-provider/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :----------------------- | :----------- | :------- | -| @backstage/catalog-model | package.json | error | +| @backstage/catalog-model | plugins/auth-backend-module-azure-easyauth-provider/package.json | error | diff --git a/plugins/auth-backend-module-bitbucket-provider/knip-report.md b/plugins/auth-backend-module-bitbucket-provider/knip-report.md index 6624c86073..816b276c84 100644 --- a/plugins/auth-backend-module-bitbucket-provider/knip-report.md +++ b/plugins/auth-backend-module-bitbucket-provider/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| passport | package.json | error | -| express | package.json | error | +| passport | plugins/auth-backend-module-bitbucket-provider/package.json | error | +| express | plugins/auth-backend-module-bitbucket-provider/package.json | error | diff --git a/plugins/auth-backend-module-bitbucket-server-provider/knip-report.md b/plugins/auth-backend-module-bitbucket-server-provider/knip-report.md index 4386ad9c4a..5aaf6b7a92 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/knip-report.md +++ b/plugins/auth-backend-module-bitbucket-server-provider/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| passport | package.json | error | +| passport | plugins/auth-backend-module-bitbucket-server-provider/package.json | error | diff --git a/plugins/auth-backend-module-gitlab-provider/knip-report.md b/plugins/auth-backend-module-gitlab-provider/knip-report.md index 6624c86073..693a54de62 100644 --- a/plugins/auth-backend-module-gitlab-provider/knip-report.md +++ b/plugins/auth-backend-module-gitlab-provider/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| passport | package.json | error | -| express | package.json | error | +| passport | plugins/auth-backend-module-gitlab-provider/package.json | error | +| express | plugins/auth-backend-module-gitlab-provider/package.json | error | diff --git a/plugins/auth-backend-module-guest-provider/knip-report.md b/plugins/auth-backend-module-guest-provider/knip-report.md index d2b8db6f32..41211cd509 100644 --- a/plugins/auth-backend-module-guest-provider/knip-report.md +++ b/plugins/auth-backend-module-guest-provider/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :-------------- | :----------- | :------- | -| passport-oauth2 | package.json | error | +| passport-oauth2 | plugins/auth-backend-module-guest-provider/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :---------------------------- | :----------- | :------- | -| @backstage/backend-test-utils | package.json | error | -| express | package.json | error | +| @backstage/backend-test-utils | plugins/auth-backend-module-guest-provider/package.json | error | +| express | plugins/auth-backend-module-guest-provider/package.json | error | diff --git a/plugins/auth-backend-module-oauth2-provider/knip-report.md b/plugins/auth-backend-module-oauth2-provider/knip-report.md index 4386ad9c4a..731dd67f6c 100644 --- a/plugins/auth-backend-module-oauth2-provider/knip-report.md +++ b/plugins/auth-backend-module-oauth2-provider/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| passport | package.json | error | +| passport | plugins/auth-backend-module-oauth2-provider/package.json | error | diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/knip-report.md b/plugins/auth-backend-module-oauth2-proxy-provider/knip-report.md index 14b2b4cc88..e034256a27 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/knip-report.md +++ b/plugins/auth-backend-module-oauth2-proxy-provider/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---------------------------- | :----------- | :------- | -| @backstage/backend-test-utils | package.json | error | +| @backstage/backend-test-utils | plugins/auth-backend-module-oauth2-proxy-provider/package.json | error | diff --git a/plugins/auth-backend-module-oidc-provider/knip-report.md b/plugins/auth-backend-module-oidc-provider/knip-report.md index a7256f0ead..aa5b910b2d 100644 --- a/plugins/auth-backend-module-oidc-provider/knip-report.md +++ b/plugins/auth-backend-module-oidc-provider/knip-report.md @@ -4,14 +4,14 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| passport | package.json | error | -| zod | package.json | error | +| passport | plugins/auth-backend-module-oidc-provider/package.json | error | +| zod | plugins/auth-backend-module-oidc-provider/package.json | error | ## Unused devDependencies (3) | Name | Location | Severity | | :--------------------- | :----------- | :------- | -| express-promise-router | package.json | error | -| express-session | package.json | error | -| cookie-parser | package.json | error | +| express-promise-router | plugins/auth-backend-module-oidc-provider/package.json | error | +| express-session | plugins/auth-backend-module-oidc-provider/package.json | error | +| cookie-parser | plugins/auth-backend-module-oidc-provider/package.json | error | diff --git a/plugins/auth-backend-module-okta-provider/knip-report.md b/plugins/auth-backend-module-okta-provider/knip-report.md index e761ead581..bec18b0f1a 100644 --- a/plugins/auth-backend-module-okta-provider/knip-report.md +++ b/plugins/auth-backend-module-okta-provider/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| passport | package.json | error | -| express | package.json | error | +| passport | plugins/auth-backend-module-okta-provider/package.json | error | +| express | plugins/auth-backend-module-okta-provider/package.json | error | ## Unlisted dependencies (1) | Name | Location | Severity | | :-------------- | :------------- | :------- | -| passport-oauth2 | src/types.d.ts | error | +| passport-oauth2 | plugins/auth-backend-module-okta-provider/src/types.d.ts | error | diff --git a/plugins/auth-backend-module-onelogin-provider/knip-report.md b/plugins/auth-backend-module-onelogin-provider/knip-report.md index 6624c86073..8b95b0648f 100644 --- a/plugins/auth-backend-module-onelogin-provider/knip-report.md +++ b/plugins/auth-backend-module-onelogin-provider/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| passport | package.json | error | -| express | package.json | error | +| passport | plugins/auth-backend-module-onelogin-provider/package.json | error | +| express | plugins/auth-backend-module-onelogin-provider/package.json | error | diff --git a/plugins/auth-backend-module-pinniped-provider/knip-report.md b/plugins/auth-backend-module-pinniped-provider/knip-report.md index aa669344ff..23b403971a 100644 --- a/plugins/auth-backend-module-pinniped-provider/knip-report.md +++ b/plugins/auth-backend-module-pinniped-provider/knip-report.md @@ -4,7 +4,7 @@ | Name | Location | Severity | | :-------------- | :----------- | :------- | -| express-session | package.json | error | -| cookie-parser | package.json | error | -| passport | package.json | error | +| express-session | plugins/auth-backend-module-pinniped-provider/package.json | error | +| cookie-parser | plugins/auth-backend-module-pinniped-provider/package.json | error | +| passport | plugins/auth-backend-module-pinniped-provider/package.json | error | diff --git a/plugins/auth-backend-module-vmware-cloud-provider/knip-report.md b/plugins/auth-backend-module-vmware-cloud-provider/knip-report.md index 9f46420ca0..3d57808d27 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/knip-report.md +++ b/plugins/auth-backend-module-vmware-cloud-provider/knip-report.md @@ -4,11 +4,11 @@ | Name | Location | Severity | | :----------------------- | :----------- | :------- | -| @backstage/catalog-model | package.json | error | +| @backstage/catalog-model | plugins/auth-backend-module-vmware-cloud-provider/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :---------------- | :----------- | :------- | -| @backstage/errors | package.json | error | +| @backstage/errors | plugins/auth-backend-module-vmware-cloud-provider/package.json | error | diff --git a/plugins/auth-react/knip-report.md b/plugins/auth-react/knip-report.md index df86118651..ed8bd3b071 100644 --- a/plugins/auth-react/knip-report.md +++ b/plugins/auth-react/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-- | :----------- | :------- | -| msw | package.json | error | +| msw | plugins/auth-react/package.json | error | diff --git a/plugins/bitbucket-cloud-common/knip-report.md b/plugins/bitbucket-cloud-common/knip-report.md index ead5329ef3..1a4ba56b63 100644 --- a/plugins/bitbucket-cloud-common/knip-report.md +++ b/plugins/bitbucket-cloud-common/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :---------------------------------- | :----------- | :------- | -| @openapitools/openapi-generator-cli | package.json | error | -| ts-morph | package.json | error | +| @openapitools/openapi-generator-cli | plugins/bitbucket-cloud-common/package.json | error | +| ts-morph | plugins/bitbucket-cloud-common/package.json | error | diff --git a/plugins/catalog-backend-module-aws/knip-report.md b/plugins/catalog-backend-module-aws/knip-report.md index c637c25d2f..ed7557334b 100644 --- a/plugins/catalog-backend-module-aws/knip-report.md +++ b/plugins/catalog-backend-module-aws/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :---------------------------- | :----------- | :------- | -| @aws-sdk/credential-providers | package.json | error | -| winston | package.json | error | +| @aws-sdk/credential-providers | plugins/catalog-backend-module-aws/package.json | error | +| winston | plugins/catalog-backend-module-aws/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-aws/package.json | error | diff --git a/plugins/catalog-backend-module-azure/knip-report.md b/plugins/catalog-backend-module-azure/knip-report.md index cb5315967a..622077aa40 100644 --- a/plugins/catalog-backend-module-azure/knip-report.md +++ b/plugins/catalog-backend-module-azure/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-azure/package.json | error | diff --git a/plugins/catalog-backend-module-bitbucket-cloud/knip-report.md b/plugins/catalog-backend-module-bitbucket-cloud/knip-report.md index 12b22ec44f..a6400f9cd2 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/knip-report.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/knip-report.md @@ -4,11 +4,11 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @backstage/catalog-client | package.json | error | +| @backstage/catalog-client | plugins/catalog-backend-module-bitbucket-cloud/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-bitbucket-cloud/package.json | error | diff --git a/plugins/catalog-backend-module-bitbucket-server/knip-report.md b/plugins/catalog-backend-module-bitbucket-server/knip-report.md index 12b22ec44f..067155bd06 100644 --- a/plugins/catalog-backend-module-bitbucket-server/knip-report.md +++ b/plugins/catalog-backend-module-bitbucket-server/knip-report.md @@ -4,11 +4,11 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @backstage/catalog-client | package.json | error | +| @backstage/catalog-client | plugins/catalog-backend-module-bitbucket-server/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-bitbucket-server/package.json | error | diff --git a/plugins/catalog-backend-module-gcp/knip-report.md b/plugins/catalog-backend-module-gcp/knip-report.md index 14b2b4cc88..6ca9506931 100644 --- a/plugins/catalog-backend-module-gcp/knip-report.md +++ b/plugins/catalog-backend-module-gcp/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---------------------------- | :----------- | :------- | -| @backstage/backend-test-utils | package.json | error | +| @backstage/backend-test-utils | plugins/catalog-backend-module-gcp/package.json | error | diff --git a/plugins/catalog-backend-module-gerrit/knip-report.md b/plugins/catalog-backend-module-gerrit/knip-report.md index cb5315967a..7a02c83e07 100644 --- a/plugins/catalog-backend-module-gerrit/knip-report.md +++ b/plugins/catalog-backend-module-gerrit/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-gerrit/package.json | error | diff --git a/plugins/catalog-backend-module-github-org/knip-report.md b/plugins/catalog-backend-module-github-org/knip-report.md index cb5315967a..d71302f8ae 100644 --- a/plugins/catalog-backend-module-github-org/knip-report.md +++ b/plugins/catalog-backend-module-github-org/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-github-org/package.json | error | diff --git a/plugins/catalog-backend-module-github/knip-report.md b/plugins/catalog-backend-module-github/knip-report.md index c200862961..35b39536c9 100644 --- a/plugins/catalog-backend-module-github/knip-report.md +++ b/plugins/catalog-backend-module-github/knip-report.md @@ -4,21 +4,21 @@ | Name | Location | Severity | | :-------------------------------- | :----------- | :------- | -| @backstage/plugin-catalog-backend | package.json | error | -| @backstage/catalog-client | package.json | error | +| @backstage/plugin-catalog-backend | plugins/catalog-backend-module-github/package.json | error | +| @backstage/catalog-client | plugins/catalog-backend-module-github/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-github/package.json | error | ## Unlisted dependencies (4) | Name | Location | Severity | | :---------------------- | :-------------------------------------------- | :------- | -| @octokit/webhooks-types | src/providers/GithubMultiOrgEntityProvider.ts | error | -| @octokit/webhooks-types | src/providers/GithubEntityProvider.test.ts | error | -| @octokit/webhooks-types | src/providers/GithubOrgEntityProvider.ts | error | -| @octokit/webhooks-types | src/providers/GithubEntityProvider.ts | error | +| @octokit/webhooks-types | plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts | error | +| @octokit/webhooks-types | plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts | error | +| @octokit/webhooks-types | plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts | error | +| @octokit/webhooks-types | plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts | error | diff --git a/plugins/catalog-backend-module-gitlab-org/knip-report.md b/plugins/catalog-backend-module-gitlab-org/knip-report.md index cb5315967a..43328f5e48 100644 --- a/plugins/catalog-backend-module-gitlab-org/knip-report.md +++ b/plugins/catalog-backend-module-gitlab-org/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-gitlab-org/package.json | error | diff --git a/plugins/catalog-backend-module-gitlab/knip-report.md b/plugins/catalog-backend-module-gitlab/knip-report.md index cb5315967a..8c7b1cc0a0 100644 --- a/plugins/catalog-backend-module-gitlab/knip-report.md +++ b/plugins/catalog-backend-module-gitlab/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-gitlab/package.json | error | diff --git a/plugins/catalog-backend-module-incremental-ingestion/knip-report.md b/plugins/catalog-backend-module-incremental-ingestion/knip-report.md index 21e59cdcea..e67f6704ce 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/knip-report.md +++ b/plugins/catalog-backend-module-incremental-ingestion/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---------------------------------- | :----------- | :------- | -| @backstage/plugin-permission-common | package.json | error | +| @backstage/plugin-permission-common | plugins/catalog-backend-module-incremental-ingestion/package.json | error | diff --git a/plugins/catalog-backend-module-msgraph/knip-report.md b/plugins/catalog-backend-module-msgraph/knip-report.md index cb5315967a..1b4a6feaab 100644 --- a/plugins/catalog-backend-module-msgraph/knip-report.md +++ b/plugins/catalog-backend-module-msgraph/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-msgraph/package.json | error | diff --git a/plugins/catalog-backend-module-openapi/knip-report.md b/plugins/catalog-backend-module-openapi/knip-report.md index 559313ad8c..91d1d44c3e 100644 --- a/plugins/catalog-backend-module-openapi/knip-report.md +++ b/plugins/catalog-backend-module-openapi/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------ | :----------- | :------- | -| openapi-types | package.json | error | +| openapi-types | plugins/catalog-backend-module-openapi/package.json | error | diff --git a/plugins/catalog-backend-module-puppetdb/knip-report.md b/plugins/catalog-backend-module-puppetdb/knip-report.md index acef584675..ac2ab908f0 100644 --- a/plugins/catalog-backend-module-puppetdb/knip-report.md +++ b/plugins/catalog-backend-module-puppetdb/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---- | :----------- | :------- | -| luxon | package.json | error | +| luxon | plugins/catalog-backend-module-puppetdb/package.json | error | diff --git a/plugins/catalog-backend-module-unprocessed/knip-report.md b/plugins/catalog-backend-module-unprocessed/knip-report.md index fe81af1cb9..b4dbca6967 100644 --- a/plugins/catalog-backend-module-unprocessed/knip-report.md +++ b/plugins/catalog-backend-module-unprocessed/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :----------------------------- | :----------- | :------- | -| @backstage/plugin-catalog-node | package.json | error | -| @backstage/plugin-auth-node | package.json | error | +| @backstage/plugin-catalog-node | plugins/catalog-backend-module-unprocessed/package.json | error | +| @backstage/plugin-auth-node | plugins/catalog-backend-module-unprocessed/package.json | error | diff --git a/plugins/catalog-backend/knip-report.md b/plugins/catalog-backend/knip-report.md index d547deea65..b221ae3cf4 100644 --- a/plugins/catalog-backend/knip-report.md +++ b/plugins/catalog-backend/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------- | :----------- | :------- | -| better-sqlite3 | package.json | error | +| better-sqlite3 | plugins/catalog-backend/package.json | error | diff --git a/plugins/catalog-graph/knip-report.md b/plugins/catalog-graph/knip-report.md index 6b1996899f..92dd638989 100644 --- a/plugins/catalog-graph/knip-report.md +++ b/plugins/catalog-graph/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------ | :----------- | :------- | -| p-limit | package.json | error | +| p-limit | plugins/catalog-graph/package.json | error | diff --git a/plugins/catalog-react/knip-report.md b/plugins/catalog-react/knip-report.md index 4b33c02956..d99b18f20d 100644 --- a/plugins/catalog-react/knip-report.md +++ b/plugins/catalog-react/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------------ | :----------- | :------- | -| react-test-renderer | package.json | error | +| react-test-renderer | plugins/catalog-react/package.json | error | diff --git a/plugins/catalog-unprocessed-entities/knip-report.md b/plugins/catalog-unprocessed-entities/knip-report.md index ccda99cbfb..5f40723fab 100644 --- a/plugins/catalog-unprocessed-entities/knip-report.md +++ b/plugins/catalog-unprocessed-entities/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :--------------------- | :----------- | :------- | -| @testing-library/react | package.json | error | +| @testing-library/react | plugins/catalog-unprocessed-entities/package.json | error | diff --git a/plugins/catalog/knip-report.md b/plugins/catalog/knip-report.md index 7ceae94282..5efd01f3a6 100644 --- a/plugins/catalog/knip-report.md +++ b/plugins/catalog/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------ | :----------- | :------- | -| history | package.json | error | +| history | plugins/catalog/package.json | error | diff --git a/plugins/devtools-backend/knip-report.md b/plugins/devtools-backend/knip-report.md index dc1b572006..904730f95f 100644 --- a/plugins/devtools-backend/knip-report.md +++ b/plugins/devtools-backend/knip-report.md @@ -4,7 +4,7 @@ | Name | Location | Severity | | :-------------------------------- | :----------- | :------- | -| @backstage/plugin-permission-node | package.json | error | -| semver | package.json | error | -| yn | package.json | error | +| @backstage/plugin-permission-node | plugins/devtools-backend/package.json | error | +| semver | plugins/devtools-backend/package.json | error | +| yn | plugins/devtools-backend/package.json | error | diff --git a/plugins/events-backend-module-aws-sqs/knip-report.md b/plugins/events-backend-module-aws-sqs/knip-report.md index c9ce845fd0..3f4259cf67 100644 --- a/plugins/events-backend-module-aws-sqs/knip-report.md +++ b/plugins/events-backend-module-aws-sqs/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------- | :----------- | :------- | -| @aws-sdk/types | package.json | error | +| @aws-sdk/types | plugins/events-backend-module-aws-sqs/package.json | error | diff --git a/plugins/events-backend/knip-report.md b/plugins/events-backend/knip-report.md index 7df8949921..9da2b3fe7c 100644 --- a/plugins/events-backend/knip-report.md +++ b/plugins/events-backend/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------------------- | :----------- | :------- | -| @backstage/backend-app-api | package.json | error | +| @backstage/backend-app-api | plugins/events-backend/package.json | error | diff --git a/plugins/events-node/knip-report.md b/plugins/events-node/knip-report.md index 3f80c71260..cb8d19b560 100644 --- a/plugins/events-node/knip-report.md +++ b/plugins/events-node/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :--------------- | :----------- | :------- | -| @backstage/types | package.json | error | +| @backstage/types | plugins/events-node/package.json | error | diff --git a/plugins/example-todo-list/knip-report.md b/plugins/example-todo-list/knip-report.md index ba9c1dddc5..552227ac1d 100644 --- a/plugins/example-todo-list/knip-report.md +++ b/plugins/example-todo-list/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :--------------------- | :----------- | :------- | -| @testing-library/react | package.json | error | -| @testing-library/dom | package.json | error | +| @testing-library/react | plugins/example-todo-list/package.json | error | +| @testing-library/dom | plugins/example-todo-list/package.json | error | diff --git a/plugins/home-react/knip-report.md b/plugins/home-react/knip-report.md index c9f34521a3..3fea06e47b 100644 --- a/plugins/home-react/knip-report.md +++ b/plugins/home-react/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :----------------------- | :----------- | :------- | -| @types/react-grid-layout | package.json | error | +| @types/react-grid-layout | plugins/home-react/package.json | error | diff --git a/plugins/kubernetes-backend/knip-report.md b/plugins/kubernetes-backend/knip-report.md index 7804e0ecb8..4d8545fd20 100644 --- a/plugins/kubernetes-backend/knip-report.md +++ b/plugins/kubernetes-backend/knip-report.md @@ -1,21 +1,22 @@ # Knip report -## Unused dependencies (7) +## Unused dependencies (8) | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/plugin-auth-node | package.json | error | -| stream-buffers | package.json | error | -| compression | package.json | error | -| helmet | package.json | error | -| morgan | package.json | error | -| cors | package.json | error | -| yn | package.json | error | +| @backstage/plugin-auth-node | plugins/kubernetes-backend/package.json | error | +| stream-buffers | plugins/kubernetes-backend/package.json | error | +| compression | plugins/kubernetes-backend/package.json | error | +| winston | plugins/kubernetes-backend/package.json | error | +| helmet | plugins/kubernetes-backend/package.json | error | +| morgan | plugins/kubernetes-backend/package.json | error | +| cors | plugins/kubernetes-backend/package.json | error | +| yn | plugins/kubernetes-backend/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :------------------------- | :----------- | :------- | -| @backstage/backend-app-api | package.json | error | -| @types/aws4 | package.json | error | +| @backstage/backend-app-api | plugins/kubernetes-backend/package.json | error | +| @types/aws4 | plugins/kubernetes-backend/package.json | error | diff --git a/plugins/kubernetes-cluster/knip-report.md b/plugins/kubernetes-cluster/knip-report.md index a5a618460d..55eba7a829 100644 --- a/plugins/kubernetes-cluster/knip-report.md +++ b/plugins/kubernetes-cluster/knip-report.md @@ -4,15 +4,15 @@ | Name | Location | Severity | | :---------------------- | :----------- | :------- | -| @kubernetes-models/base | package.json | error | -| cronstrue | package.json | error | -| js-yaml | package.json | error | -| lodash | package.json | error | -| luxon | package.json | error | +| @kubernetes-models/base | plugins/kubernetes-cluster/package.json | error | +| cronstrue | plugins/kubernetes-cluster/package.json | error | +| js-yaml | plugins/kubernetes-cluster/package.json | error | +| lodash | plugins/kubernetes-cluster/package.json | error | +| luxon | plugins/kubernetes-cluster/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :------------------- | :----------- | :------- | -| @testing-library/dom | package.json | error | +| @testing-library/dom | plugins/kubernetes-cluster/package.json | error | diff --git a/plugins/kubernetes-node/knip-report.md b/plugins/kubernetes-node/knip-report.md index 1c25fa062b..31fc64b3c7 100644 --- a/plugins/kubernetes-node/knip-report.md +++ b/plugins/kubernetes-node/knip-report.md @@ -4,11 +4,11 @@ | Name | Location | Severity | | :------ | :----------- | :------- | -| winston | package.json | error | +| winston | plugins/kubernetes-node/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :------------------------- | :----------- | :------- | -| @backstage/backend-app-api | package.json | error | +| @backstage/backend-app-api | plugins/kubernetes-node/package.json | error | diff --git a/plugins/kubernetes/knip-report.md b/plugins/kubernetes/knip-report.md index bb11f63d0d..11b66acd1b 100644 --- a/plugins/kubernetes/knip-report.md +++ b/plugins/kubernetes/knip-report.md @@ -4,15 +4,15 @@ | Name | Location | Severity | | :------------------------------ | :----------- | :------- | -| @kubernetes-models/apimachinery | package.json | error | -| @kubernetes-models/base | package.json | error | -| @kubernetes/client-node | package.json | error | -| @xterm/addon-attach | package.json | error | -| kubernetes-models | package.json | error | -| @xterm/addon-fit | package.json | error | -| @xterm/xterm | package.json | error | -| cronstrue | package.json | error | -| js-yaml | package.json | error | -| lodash | package.json | error | -| luxon | package.json | error | +| @kubernetes-models/apimachinery | plugins/kubernetes/package.json | error | +| @kubernetes-models/base | plugins/kubernetes/package.json | error | +| @kubernetes/client-node | plugins/kubernetes/package.json | error | +| @xterm/addon-attach | plugins/kubernetes/package.json | error | +| kubernetes-models | plugins/kubernetes/package.json | error | +| @xterm/addon-fit | plugins/kubernetes/package.json | error | +| @xterm/xterm | plugins/kubernetes/package.json | error | +| cronstrue | plugins/kubernetes/package.json | error | +| js-yaml | plugins/kubernetes/package.json | error | +| lodash | plugins/kubernetes/package.json | error | +| luxon | plugins/kubernetes/package.json | error | diff --git a/plugins/mcp-actions-backend/knip-report.md b/plugins/mcp-actions-backend/knip-report.md index 830018776f..79729ec010 100644 --- a/plugins/mcp-actions-backend/knip-report.md +++ b/plugins/mcp-actions-backend/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @backstage/catalog-client | package.json | error | -| zod | package.json | error | +| @backstage/catalog-client | plugins/mcp-actions-backend/package.json | error | +| zod | plugins/mcp-actions-backend/package.json | error | diff --git a/plugins/notifications-backend-module-email/knip-report.md b/plugins/notifications-backend-module-email/knip-report.md index 38a3e4ea8a..a36d75cae4 100644 --- a/plugins/notifications-backend-module-email/knip-report.md +++ b/plugins/notifications-backend-module-email/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------- | :----------- | :------- | -| @aws-sdk/types | package.json | error | +| @aws-sdk/types | plugins/notifications-backend-module-email/package.json | error | diff --git a/plugins/notifications-backend-module-slack/knip-report.md b/plugins/notifications-backend-module-slack/knip-report.md index eb139623ce..d3eb017b29 100644 --- a/plugins/notifications-backend-module-slack/knip-report.md +++ b/plugins/notifications-backend-module-slack/knip-report.md @@ -4,14 +4,14 @@ | Name | Location | Severity | | :----------- | :----------- | :------- | -| @slack/types | package.json | error | -| @slack/bolt | package.json | error | +| @slack/types | plugins/notifications-backend-module-slack/package.json | error | +| @slack/bolt | plugins/notifications-backend-module-slack/package.json | error | ## Unused devDependencies (3) | Name | Location | Severity | | :-------------------- | :----------- | :------- | -| @backstage/test-utils | package.json | error | -| @faker-js/faker | package.json | error | -| msw | package.json | error | +| @backstage/test-utils | plugins/notifications-backend-module-slack/package.json | error | +| @faker-js/faker | plugins/notifications-backend-module-slack/package.json | error | +| msw | plugins/notifications-backend-module-slack/package.json | error | diff --git a/plugins/notifications-backend/knip-report.md b/plugins/notifications-backend/knip-report.md index 858e4d279e..3514fea30a 100644 --- a/plugins/notifications-backend/knip-report.md +++ b/plugins/notifications-backend/knip-report.md @@ -4,14 +4,14 @@ | Name | Location | Severity | | :---------------------------- | :----------- | :------- | -| @backstage/plugin-events-node | package.json | error | -| @backstage/plugin-auth-node | package.json | error | -| winston | package.json | error | -| yn | package.json | error | +| @backstage/plugin-events-node | plugins/notifications-backend/package.json | error | +| @backstage/plugin-auth-node | plugins/notifications-backend/package.json | error | +| winston | plugins/notifications-backend/package.json | error | +| yn | plugins/notifications-backend/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :-- | :----------- | :------- | -| msw | package.json | error | +| msw | plugins/notifications-backend/package.json | error | diff --git a/plugins/notifications-common/knip-report.md b/plugins/notifications-common/knip-report.md index 6bc617b812..90eba3c41e 100644 --- a/plugins/notifications-common/knip-report.md +++ b/plugins/notifications-common/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :----------------- | :----------- | :------- | -| @material-ui/icons | package.json | error | +| @material-ui/icons | plugins/notifications-common/package.json | error | diff --git a/plugins/notifications-node/knip-report.md b/plugins/notifications-node/knip-report.md index 1206fc9c3b..624533dcfc 100644 --- a/plugins/notifications-node/knip-report.md +++ b/plugins/notifications-node/knip-report.md @@ -4,9 +4,9 @@ | Name | Location | Severity | | :----------------------------- | :----------- | :------- | -| @backstage/plugin-signals-node | package.json | error | -| @backstage/catalog-client | package.json | error | -| @backstage/catalog-model | package.json | error | -| knex | package.json | error | -| uuid | package.json | error | +| @backstage/plugin-signals-node | plugins/notifications-node/package.json | error | +| @backstage/catalog-client | plugins/notifications-node/package.json | error | +| @backstage/catalog-model | plugins/notifications-node/package.json | error | +| knex | plugins/notifications-node/package.json | error | +| uuid | plugins/notifications-node/package.json | error | diff --git a/plugins/notifications/knip-report.md b/plugins/notifications/knip-report.md index da21340672..6cd21cf3b8 100644 --- a/plugins/notifications/knip-report.md +++ b/plugins/notifications/knip-report.md @@ -4,13 +4,13 @@ | Name | Location | Severity | | :--------------- | :----------- | :------- | -| @backstage/types | package.json | error | -| @material-ui/lab | package.json | error | +| @backstage/types | plugins/notifications/package.json | error | +| @material-ui/lab | plugins/notifications/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @testing-library/user-event | package.json | error | -| @backstage/core-app-api | package.json | error | +| @testing-library/user-event | plugins/notifications/package.json | error | +| @backstage/core-app-api | plugins/notifications/package.json | error | diff --git a/plugins/org-react/knip-report.md b/plugins/org-react/knip-report.md index b5cb1d8b22..a2a90b227e 100644 --- a/plugins/org-react/knip-report.md +++ b/plugins/org-react/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @backstage/catalog-client | package.json | error | +| @backstage/catalog-client | plugins/org-react/package.json | error | diff --git a/plugins/org/knip-report.md b/plugins/org/knip-report.md index d1f8bacd53..45c5be2d03 100644 --- a/plugins/org/knip-report.md +++ b/plugins/org/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :---------------------------------- | :----------- | :------- | -| @backstage/plugin-permission-common | package.json | error | +| @backstage/plugin-permission-common | plugins/org/package.json | error | diff --git a/plugins/permission-backend-module-policy-allow-all/knip-report.md b/plugins/permission-backend-module-policy-allow-all/knip-report.md index 02ee898bec..3790422ce9 100644 --- a/plugins/permission-backend-module-policy-allow-all/knip-report.md +++ b/plugins/permission-backend-module-policy-allow-all/knip-report.md @@ -4,11 +4,11 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/plugin-auth-node | package.json | error | +| @backstage/plugin-auth-node | plugins/permission-backend-module-policy-allow-all/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :---------------------------- | :----------- | :------- | -| @backstage/backend-test-utils | package.json | error | +| @backstage/backend-test-utils | plugins/permission-backend-module-policy-allow-all/package.json | error | diff --git a/plugins/permission-backend/knip-report.md b/plugins/permission-backend/knip-report.md index 679e0ffb63..61ea01af65 100644 --- a/plugins/permission-backend/knip-report.md +++ b/plugins/permission-backend/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :- | :----------- | :------- | -| yn | package.json | error | +| yn | plugins/permission-backend/package.json | error | diff --git a/plugins/proxy-backend/knip-report.md b/plugins/proxy-backend/knip-report.md index 7df8949921..3f66c63579 100644 --- a/plugins/proxy-backend/knip-report.md +++ b/plugins/proxy-backend/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------------------- | :----------- | :------- | -| @backstage/backend-app-api | package.json | error | +| @backstage/backend-app-api | plugins/proxy-backend/package.json | error | diff --git a/plugins/proxy-node/knip-report.md b/plugins/proxy-node/knip-report.md index e7921e52b4..7d1ae72c58 100644 --- a/plugins/proxy-node/knip-report.md +++ b/plugins/proxy-node/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :---------------------------- | :----------- | :------- | -| @backstage/backend-test-utils | package.json | error | -| @backstage/config | package.json | error | +| @backstage/backend-test-utils | plugins/proxy-node/package.json | error | +| @backstage/config | plugins/proxy-node/package.json | error | diff --git a/plugins/scaffolder-backend-module-bitbucket-server/knip-report.md b/plugins/scaffolder-backend-module-bitbucket-server/knip-report.md index b9ed10af2c..0dd672d03d 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/knip-report.md +++ b/plugins/scaffolder-backend-module-bitbucket-server/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-- | :----------- | :------- | -| zod | package.json | error | +| zod | plugins/scaffolder-backend-module-bitbucket-server/package.json | error | diff --git a/plugins/scaffolder-backend-module-bitbucket/knip-report.md b/plugins/scaffolder-backend-module-bitbucket/knip-report.md index 8c142397d7..c8b2aa1788 100644 --- a/plugins/scaffolder-backend-module-bitbucket/knip-report.md +++ b/plugins/scaffolder-backend-module-bitbucket/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------- | :----------- | :------- | -| fs-extra | package.json | error | +| fs-extra | plugins/scaffolder-backend-module-bitbucket/package.json | error | diff --git a/plugins/scaffolder-backend-module-cookiecutter/knip-report.md b/plugins/scaffolder-backend-module-cookiecutter/knip-report.md index 0e8b43df56..5d34678cdf 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/knip-report.md +++ b/plugins/scaffolder-backend-module-cookiecutter/knip-report.md @@ -4,7 +4,7 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/backend-defaults | package.json | error | -| winston | package.json | error | -| yn | package.json | error | +| @backstage/backend-defaults | plugins/scaffolder-backend-module-cookiecutter/package.json | error | +| winston | plugins/scaffolder-backend-module-cookiecutter/package.json | error | +| yn | plugins/scaffolder-backend-module-cookiecutter/package.json | error | diff --git a/plugins/scaffolder-backend-module-gcp/knip-report.md b/plugins/scaffolder-backend-module-gcp/knip-report.md index 5eace4cfc6..3e771319e4 100644 --- a/plugins/scaffolder-backend-module-gcp/knip-report.md +++ b/plugins/scaffolder-backend-module-gcp/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :--------------------- | :----------- | :------- | -| @backstage/integration | package.json | error | -| @backstage/errors | package.json | error | +| @backstage/integration | plugins/scaffolder-backend-module-gcp/package.json | error | +| @backstage/errors | plugins/scaffolder-backend-module-gcp/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :------------------------------------------- | :----------- | :------- | -| @backstage/plugin-scaffolder-node-test-utils | package.json | error | +| @backstage/plugin-scaffolder-node-test-utils | plugins/scaffolder-backend-module-gcp/package.json | error | diff --git a/plugins/scaffolder-backend-module-github/knip-report.md b/plugins/scaffolder-backend-module-github/knip-report.md index f6577387b7..75e49c21a1 100644 --- a/plugins/scaffolder-backend-module-github/knip-report.md +++ b/plugins/scaffolder-backend-module-github/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------------ | :---------- | :------- | -| @octokit/core | src/util.ts | error | +| @octokit/core | plugins/scaffolder-backend-module-github/src/util.ts | error | diff --git a/plugins/scaffolder-backend-module-gitlab/knip-report.md b/plugins/scaffolder-backend-module-gitlab/knip-report.md index c955ca23f0..3e1fc873d4 100644 --- a/plugins/scaffolder-backend-module-gitlab/knip-report.md +++ b/plugins/scaffolder-backend-module-gitlab/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------ | :----------- | :------- | -| winston | package.json | error | +| winston | plugins/scaffolder-backend-module-gitlab/package.json | error | diff --git a/plugins/scaffolder-backend-module-rails/knip-report.md b/plugins/scaffolder-backend-module-rails/knip-report.md index 38a14d270f..c7301f2b85 100644 --- a/plugins/scaffolder-backend-module-rails/knip-report.md +++ b/plugins/scaffolder-backend-module-rails/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-------- | :----------- | :------- | -| jest-when | package.json | error | +| jest-when | plugins/scaffolder-backend-module-rails/package.json | error | diff --git a/plugins/scaffolder-backend-module-yeoman/knip-report.md b/plugins/scaffolder-backend-module-yeoman/knip-report.md index c955ca23f0..56fbe222c7 100644 --- a/plugins/scaffolder-backend-module-yeoman/knip-report.md +++ b/plugins/scaffolder-backend-module-yeoman/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :------ | :----------- | :------- | -| winston | package.json | error | +| winston | plugins/scaffolder-backend-module-yeoman/package.json | error | diff --git a/plugins/scaffolder-backend/knip-report.md b/plugins/scaffolder-backend/knip-report.md index b6fcf59bb3..eb8ddd05aa 100644 --- a/plugins/scaffolder-backend/knip-report.md +++ b/plugins/scaffolder-backend/knip-report.md @@ -4,24 +4,24 @@ | Name | Location | Severity | | :--------------------------------------------------------------- | :----------- | :------- | -| @backstage/plugin-catalog-backend-module-scaffolder-entity-model | package.json | error | -| @backstage/plugin-scaffolder-backend-module-bitbucket-server | package.json | error | -| @backstage/plugin-scaffolder-backend-module-bitbucket-cloud | package.json | error | -| @backstage/plugin-scaffolder-backend-module-bitbucket | package.json | error | -| @backstage/plugin-scaffolder-backend-module-gerrit | package.json | error | -| @backstage/plugin-scaffolder-backend-module-github | package.json | error | -| @backstage/plugin-scaffolder-backend-module-gitlab | package.json | error | -| @backstage/plugin-scaffolder-backend-module-azure | package.json | error | -| @backstage/plugin-scaffolder-backend-module-gitea | package.json | error | -| @backstage/plugin-bitbucket-cloud-common | package.json | error | -| @backstage/plugin-auth-node | package.json | error | -| concat-stream | package.json | error | -| p-limit | package.json | error | -| tar | package.json | error | +| @backstage/plugin-catalog-backend-module-scaffolder-entity-model | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-scaffolder-backend-module-bitbucket-server | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-scaffolder-backend-module-bitbucket-cloud | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-scaffolder-backend-module-bitbucket | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-scaffolder-backend-module-gerrit | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-scaffolder-backend-module-github | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-scaffolder-backend-module-gitlab | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-scaffolder-backend-module-azure | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-scaffolder-backend-module-gitea | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-bitbucket-cloud-common | plugins/scaffolder-backend/package.json | error | +| @backstage/plugin-auth-node | plugins/scaffolder-backend/package.json | error | +| concat-stream | plugins/scaffolder-backend/package.json | error | +| p-limit | plugins/scaffolder-backend/package.json | error | +| tar | plugins/scaffolder-backend/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :------------------------- | :----------- | :------- | -| @backstage/backend-app-api | package.json | error | +| @backstage/backend-app-api | plugins/scaffolder-backend/package.json | error | diff --git a/plugins/scaffolder-node-test-utils/knip-report.md b/plugins/scaffolder-node-test-utils/knip-report.md index beefceb792..9a0eed890a 100644 --- a/plugins/scaffolder-node-test-utils/knip-report.md +++ b/plugins/scaffolder-node-test-utils/knip-report.md @@ -4,7 +4,7 @@ | Name | Location | Severity | | :--------------- | :----------- | :------- | -| react-router-dom | package.json | error | -| react-dom | package.json | error | -| react | package.json | error | +| react-router-dom | plugins/scaffolder-node-test-utils/package.json | error | +| react-dom | plugins/scaffolder-node-test-utils/package.json | error | +| react | plugins/scaffolder-node-test-utils/package.json | error | diff --git a/plugins/scaffolder-react/knip-report.md b/plugins/scaffolder-react/knip-report.md index 38b9eed013..c1c8bd6b32 100644 --- a/plugins/scaffolder-react/knip-report.md +++ b/plugins/scaffolder-react/knip-report.md @@ -4,11 +4,11 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @backstage/catalog-client | package.json | error | +| @backstage/catalog-client | plugins/scaffolder-react/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :------------------------------- | :----------- | :------- | -| @backstage/plugin-catalog-common | package.json | error | +| @backstage/plugin-catalog-common | plugins/scaffolder-react/package.json | error | diff --git a/plugins/scaffolder/knip-report.md b/plugins/scaffolder/knip-report.md index 7d1731eb0b..38ae11af79 100644 --- a/plugins/scaffolder/knip-report.md +++ b/plugins/scaffolder/knip-report.md @@ -4,14 +4,14 @@ | Name | Location | Severity | | :------------------ | :----------- | :------- | -| json-schema-library | package.json | error | -| @rjsf/material-ui | package.json | error | -| react-resizable | package.json | error | -| git-url-parse | package.json | error | +| json-schema-library | plugins/scaffolder/package.json | error | +| @rjsf/material-ui | plugins/scaffolder/package.json | error | +| react-resizable | plugins/scaffolder/package.json | error | +| git-url-parse | plugins/scaffolder/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :--------------------- | :----------- | :------- | -| @types/react-resizable | package.json | error | +| @types/react-resizable | plugins/scaffolder/package.json | error | diff --git a/plugins/search-backend/knip-report.md b/plugins/search-backend/knip-report.md index 90e0425294..406f00e7a8 100644 --- a/plugins/search-backend/knip-report.md +++ b/plugins/search-backend/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :-------------------------------- | :----------- | :------- | -| @backstage/plugin-permission-node | package.json | error | -| yn | package.json | error | +| @backstage/plugin-permission-node | plugins/search-backend/package.json | error | +| yn | plugins/search-backend/package.json | error | diff --git a/plugins/search-react/knip-report.md b/plugins/search-react/knip-report.md index 7f6a4d4ccd..74ef5ba5c2 100644 --- a/plugins/search-react/knip-report.md +++ b/plugins/search-react/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/frontend-app-api | package.json | error | +| @backstage/frontend-app-api | plugins/search-react/package.json | error | diff --git a/plugins/signals-backend/knip-report.md b/plugins/signals-backend/knip-report.md index 8f8d01a437..2b66a8ba1a 100644 --- a/plugins/signals-backend/knip-report.md +++ b/plugins/signals-backend/knip-report.md @@ -4,14 +4,14 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/plugin-auth-node | package.json | error | -| http-proxy-middleware | package.json | error | -| winston | package.json | error | -| yn | package.json | error | +| @backstage/plugin-auth-node | plugins/signals-backend/package.json | error | +| http-proxy-middleware | plugins/signals-backend/package.json | error | +| winston | plugins/signals-backend/package.json | error | +| yn | plugins/signals-backend/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :-- | :----------- | :------- | -| msw | package.json | error | +| msw | plugins/signals-backend/package.json | error | diff --git a/plugins/signals-node/knip-report.md b/plugins/signals-node/knip-report.md index 34f37bfdb1..42ea194792 100644 --- a/plugins/signals-node/knip-report.md +++ b/plugins/signals-node/knip-report.md @@ -4,15 +4,15 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/plugin-auth-node | package.json | error | -| @backstage/config | package.json | error | -| express | package.json | error | -| uuid | package.json | error | -| ws | package.json | error | +| @backstage/plugin-auth-node | plugins/signals-node/package.json | error | +| @backstage/config | plugins/signals-node/package.json | error | +| express | plugins/signals-node/package.json | error | +| uuid | plugins/signals-node/package.json | error | +| ws | plugins/signals-node/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :------------- | :----------- | :------- | -| @types/express | package.json | error | +| @types/express | plugins/signals-node/package.json | error | diff --git a/plugins/signals-react/knip-report.md b/plugins/signals-react/knip-report.md index 0d1d20d45b..e210ba0b51 100644 --- a/plugins/signals-react/knip-report.md +++ b/plugins/signals-react/knip-report.md @@ -4,12 +4,12 @@ | Name | Location | Severity | | :---------------- | :----------- | :------- | -| @material-ui/core | package.json | error | +| @material-ui/core | plugins/signals-react/package.json | error | ## Unused devDependencies (2) | Name | Location | Severity | | :--------------------- | :----------- | :------- | -| @testing-library/react | package.json | error | -| @backstage/test-utils | package.json | error | +| @testing-library/react | plugins/signals-react/package.json | error | +| @backstage/test-utils | plugins/signals-react/package.json | error | diff --git a/plugins/signals/knip-report.md b/plugins/signals/knip-report.md index e9c2c167b1..a27d63358a 100644 --- a/plugins/signals/knip-report.md +++ b/plugins/signals/knip-report.md @@ -4,15 +4,15 @@ | Name | Location | Severity | | :----------------- | :----------- | :------- | -| @material-ui/icons | package.json | error | -| @material-ui/lab | package.json | error | -| react-use | package.json | error | +| @material-ui/icons | plugins/signals/package.json | error | +| @material-ui/lab | plugins/signals/package.json | error | +| react-use | plugins/signals/package.json | error | ## Unused devDependencies (3) | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @testing-library/user-event | package.json | error | -| @backstage/core-app-api | package.json | error | -| msw | package.json | error | +| @testing-library/user-event | plugins/signals/package.json | error | +| @backstage/core-app-api | plugins/signals/package.json | error | +| msw | plugins/signals/package.json | error | diff --git a/plugins/techdocs-addons-test-utils/knip-report.md b/plugins/techdocs-addons-test-utils/knip-report.md index ca9b81ba38..2d84f97e2f 100644 --- a/plugins/techdocs-addons-test-utils/knip-report.md +++ b/plugins/techdocs-addons-test-utils/knip-report.md @@ -4,11 +4,11 @@ | Name | Location | Severity | | :------------------------ | :----------- | :------- | -| @backstage/plugin-catalog | package.json | error | +| @backstage/plugin-catalog | plugins/techdocs-addons-test-utils/package.json | error | ## Unused devDependencies (1) | Name | Location | Severity | | :------------------- | :----------- | :------- | -| @testing-library/dom | package.json | error | +| @testing-library/dom | plugins/techdocs-addons-test-utils/package.json | error | diff --git a/plugins/techdocs-backend/knip-report.md b/plugins/techdocs-backend/knip-report.md index 764d9383f0..164f2d0595 100644 --- a/plugins/techdocs-backend/knip-report.md +++ b/plugins/techdocs-backend/knip-report.md @@ -4,9 +4,9 @@ | Name | Location | Severity | | :----------------------------------------------- | :----------- | :------- | -| @backstage/plugin-search-backend-module-techdocs | package.json | error | -| @backstage/plugin-permission-common | package.json | error | -| @backstage/plugin-techdocs-common | package.json | error | -| @backstage/plugin-catalog-common | package.json | error | -| lodash | package.json | error | +| @backstage/plugin-search-backend-module-techdocs | plugins/techdocs-backend/package.json | error | +| @backstage/plugin-permission-common | plugins/techdocs-backend/package.json | error | +| @backstage/plugin-techdocs-common | plugins/techdocs-backend/package.json | error | +| @backstage/plugin-catalog-common | plugins/techdocs-backend/package.json | error | +| lodash | plugins/techdocs-backend/package.json | error | diff --git a/plugins/techdocs-react/knip-report.md b/plugins/techdocs-react/knip-report.md index 651079cda4..3e7476ef55 100644 --- a/plugins/techdocs-react/knip-report.md +++ b/plugins/techdocs-react/knip-report.md @@ -4,6 +4,6 @@ | Name | Location | Severity | | :------------------------- | :----------- | :------- | -| @backstage/core-components | package.json | error | -| react-helmet | package.json | error | +| @backstage/core-components | plugins/techdocs-react/package.json | error | +| react-helmet | plugins/techdocs-react/package.json | error | diff --git a/plugins/techdocs/knip-report.md b/plugins/techdocs/knip-report.md index 2b0b502c71..45a1d2af25 100644 --- a/plugins/techdocs/knip-report.md +++ b/plugins/techdocs/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-- | :----------- | :------- | -| jss | package.json | error | +| jss | plugins/techdocs/package.json | error | diff --git a/plugins/user-settings-backend/knip-report.md b/plugins/user-settings-backend/knip-report.md index 89988530e6..d5bbb23d04 100644 --- a/plugins/user-settings-backend/knip-report.md +++ b/plugins/user-settings-backend/knip-report.md @@ -4,5 +4,5 @@ | Name | Location | Severity | | :-------------------------- | :----------- | :------- | -| @backstage/plugin-auth-node | package.json | error | +| @backstage/plugin-auth-node | plugins/user-settings-backend/package.json | error | From ba64e737179c50cc7dd84aa61a4509664862ca5b Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 15 Sep 2025 20:38:20 +0100 Subject: [PATCH 204/233] Restrict Chromatic to only build BUI Signed-off-by: Charles de Dreuille --- .github/workflows/verify_storybook.yml | 6 +++--- .gitignore | 3 ++- .storybook/main.ts | 28 +++++++++++++++----------- package.json | 7 ++++--- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index 5a71caba85..1230d09168 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -49,8 +49,8 @@ jobs: with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - - name: Build Storybook - run: yarn build-storybook + - name: Build Storybook for Chromatic + run: yarn build-storybook:chromatic - name: Deploy Storybook to Chromatic uses: chromaui/action@1cfa065cbdab28f6ca3afaeb3d761383076a35aa # v11 @@ -59,4 +59,4 @@ jobs: # projectToken intentionally shared to allow collaborators to run Chromatic on forks # https://www.chromatic.com/docs/custom-ci-provider#run-chromatic-on-external-forks-of-open-source-projects projectToken: chpt_dab72dc0f97d55b - storybookBuildDir: dist-storybook + storybookBuildDir: dist-storybook-chromatic diff --git a/.gitignore b/.gitignore index aaf05b51cd..a4a3ec321b 100644 --- a/.gitignore +++ b/.gitignore @@ -186,4 +186,5 @@ docs.json tsconfig.typedoc.tmp.json # Storybook -dist-storybook/ \ No newline at end of file +dist-storybook/ +dist-storybook-chromatic/ \ No newline at end of file diff --git a/.storybook/main.ts b/.storybook/main.ts index 1b9e4e2bde..4be06af7fc 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -2,17 +2,21 @@ import type { StorybookConfig } from '@storybook/react-vite'; import { join, dirname, posix } from 'path'; -// This set of stories are the ones that we publish to backstage.io. -const backstageCoreStories = [ - 'packages/ui', - 'packages/core-components', - 'packages/app', - 'plugins/org', - 'plugins/search', - 'plugins/search-react', - 'plugins/home', - 'plugins/catalog-react', -]; +const isChromatic = process.env.STORYBOOK_STORY_SET === 'chromatic'; + +// All stories for full development +const allStories = isChromatic + ? ['packages/ui'] + : [ + 'packages/ui', + 'packages/core-components', + 'packages/app', + 'plugins/org', + 'plugins/search', + 'plugins/search-react', + 'plugins/home', + 'plugins/catalog-react', + ]; const rootPath = '../'; const storiesSrcMdx = 'src/**/*.mdx'; @@ -21,7 +25,7 @@ const storiesSrcGlob = 'src/**/*.stories.@(js|jsx|mjs|ts|tsx)'; const getStoriesPath = (element: string, pattern: string) => posix.join(rootPath, element, pattern); -const stories = backstageCoreStories.flatMap(element => [ +const stories = allStories.flatMap(element => [ getStoriesPath(element, storiesSrcMdx), getStoriesPath(element, storiesSrcGlob), ]); diff --git a/package.json b/package.json index 4b74eda8a9..34fc4fdf0a 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "scripts": { "build-storybook": "storybook build --output-dir dist-storybook", + "build-storybook:chromatic": "STORYBOOK_STORY_SET=chromatic storybook build --output-dir dist-storybook-chromatic", "build:all": "backstage-cli repo build --all", "build:api-docs": "LANG=en_EN yarn build:api-reports --docs --exclude 'plugins/@(api-docs|api-docs-module-protoc-gen-doc|app-visualizer|catalog-graph|catalog-import|catalog-unprocessed-entities|config-schema|example-todo-list|example-todo-list-backend)'", "build:api-reports": "yarn build:api-reports:only --tsc", @@ -101,20 +102,20 @@ "@changesets/assemble-release-plan@^6.0.0": "patch:@changesets/assemble-release-plan@npm%3A6.0.0#./.yarn/patches/@changesets-assemble-release-plan-npm-6.0.0-f7b3005037.patch", "@material-ui/pickers@^3.2.10": "patch:@material-ui/pickers@npm%3A3.3.11#./.yarn/patches/@material-ui-pickers-npm-3.3.11-1c8f68ea20.patch", "@material-ui/pickers@^3.3.10": "patch:@material-ui/pickers@npm%3A3.3.11#./.yarn/patches/@material-ui-pickers-npm-3.3.11-1c8f68ea20.patch", + "@storybook/react@npm:9.1.5": "patch:@storybook/react@npm%3A9.1.5#~/.yarn/patches/@storybook-react-npm-9.1.5-2331f18b6b.patch", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@yarnpkg/plugin-npm@npm:^3.1.0": "patch:@yarnpkg/plugin-npm@npm%3A3.1.0#~/.yarn/patches/@yarnpkg-plugin-npm-npm-3.1.0-6533d0f5a1.patch", "ast-types@0.14.2": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch", "ast-types@^0.14.1": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch", - "ast-types@npm:^0.13.4": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", "ast-types@npm:0.14.2": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", + "ast-types@npm:^0.13.4": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", "ast-types@npm:^0.16.1": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", "csstype@npm:^3.0.2": "3.0.9", "csstype@npm:^3.1.2": "3.0.9", "csstype@npm:^3.1.3": "3.0.9", "jest-haste-map@^29.7.0": "patch:jest-haste-map@npm%3A29.7.0#./.yarn/patches/jest-haste-map-npm-29.7.0-e3be419eff.patch", - "recast@npm:0.23.9>ast-types": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch", - "@storybook/react@npm:9.1.5": "patch:@storybook/react@npm%3A9.1.5#~/.yarn/patches/@storybook-react-npm-9.1.5-2331f18b6b.patch" + "recast@npm:0.23.9>ast-types": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch" }, "dependencies": { "@backstage/errors": "workspace:^", From 9320b11e331aae4e923aca0f3743b17232fa0032 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 15 Sep 2025 21:01:53 +0100 Subject: [PATCH 205/233] Update report-alpha.api.md Signed-off-by: Charles de Dreuille --- plugins/catalog-react/report-alpha.api.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 30cdd91704..cf28d80c11 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -92,12 +92,12 @@ export const catalogReactTranslationRef: TranslationRef< readonly 'entityTableColumnTitle.title': 'Title'; readonly 'entityTableColumnTitle.description': 'Description'; readonly 'entityTableColumnTitle.domain': 'Domain'; + readonly 'entityTableColumnTitle.system': 'System'; + readonly 'entityTableColumnTitle.tags': 'Tags'; readonly 'entityTableColumnTitle.namespace': 'Namespace'; readonly 'entityTableColumnTitle.lifecycle': 'Lifecycle'; readonly 'entityTableColumnTitle.owner': 'Owner'; - readonly 'entityTableColumnTitle.system': 'System'; readonly 'entityTableColumnTitle.targets': 'Targets'; - readonly 'entityTableColumnTitle.tags': 'Tags'; } >; @@ -533,8 +533,8 @@ export const EntityTableColumnTitle: ({ translationKey, }: EntityTableColumnTitleProps) => | 'Title' - | 'Domain' | 'System' + | 'Domain' | 'Lifecycle' | 'Namespace' | 'Owner' From 15de5ccd86d41242de71d1f683087b52a8ef19cc Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 15 Sep 2025 21:26:08 +0100 Subject: [PATCH 206/233] Add modes + a11y Signed-off-by: Charles de Dreuille --- .storybook/main.ts | 1 + .storybook/modes.ts | 18 ++++++++++++++++++ .storybook/preview.tsx | 10 ++++++++++ package.json | 1 + yarn.lock | 21 +++++++++++++++++---- 5 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 .storybook/modes.ts diff --git a/.storybook/main.ts b/.storybook/main.ts index 4be06af7fc..788df83ba9 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -41,6 +41,7 @@ const config: StorybookConfig = { getAbsolutePath('@storybook/addon-links'), getAbsolutePath('@storybook/addon-themes'), getAbsolutePath('@storybook/addon-docs'), + getAbsolutePath('@storybook/addon-a11y'), ], framework: { name: getAbsolutePath('@storybook/react-vite'), diff --git a/.storybook/modes.ts b/.storybook/modes.ts new file mode 100644 index 0000000000..7087fa35eb --- /dev/null +++ b/.storybook/modes.ts @@ -0,0 +1,18 @@ +export const allModes = { + 'light backstage': { + themeMode: 'light', + themeName: 'backstage', + }, + 'dark backstage': { + themeMode: 'dark', + themeName: 'backstage', + }, + 'light spotify': { + themeMode: 'light', + themeName: 'spotify', + }, + 'dark spotify': { + themeMode: 'dark', + themeName: 'spotify', + }, +} as const; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index d934412521..6547950705 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -5,6 +5,7 @@ import { apis } from './support/apis'; import type { Decorator, Preview } from '@storybook/react-vite'; import { useGlobals } from 'storybook/preview-api'; import { UnifiedThemeProvider, themes } from '@backstage/theme'; +import { allModes } from './modes'; // Default Backstage theme CSS (from packages/ui) import '../packages/ui/src/css/styles.css'; @@ -90,6 +91,15 @@ const preview: Preview = { docs: { codePanel: true, }, + + chromatic: { + modes: { + 'light backstage': allModes['light backstage'], + 'dark backstage': allModes['dark backstage'], + 'light spotify': allModes['light spotify'], + 'dark spotify': allModes['dark spotify'], + }, + }, }, decorators: [ Story => { diff --git a/package.json b/package.json index 34fc4fdf0a..99a6192653 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,7 @@ "@octokit/rest": "^19.0.3", "@playwright/test": "^1.32.3", "@spotify/eslint-plugin": "^15.0.0", + "@storybook/addon-a11y": "^9.1.5", "@storybook/addon-docs": "^9.1.5", "@storybook/addon-links": "^9.1.5", "@storybook/addon-themes": "^9.1.5", diff --git a/yarn.lock b/yarn.lock index 3a5aa9ac30..0403d23ed1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18321,6 +18321,18 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-a11y@npm:^9.1.5": + version: 9.1.6 + resolution: "@storybook/addon-a11y@npm:9.1.6" + dependencies: + "@storybook/global": "npm:^5.0.0" + axe-core: "npm:^4.2.0" + peerDependencies: + storybook: ^9.1.6 + checksum: 10/345c44673ccede4073e7415eb7c73f7817cb64b24b75d6f2edae73a0a6e50cc91e098a48dfd55996188aeb04d308c147b6d660bcdd4e5449d601271a83d4f178 + languageName: node + linkType: hard + "@storybook/addon-docs@npm:^9.1.5": version: 9.1.5 resolution: "@storybook/addon-docs@npm:9.1.5" @@ -24098,10 +24110,10 @@ __metadata: languageName: node linkType: hard -"axe-core@npm:^4.10.0": - version: 4.10.0 - resolution: "axe-core@npm:4.10.0" - checksum: 10/6158489a7a704edc98bd30ed56243b8280c5203c60e095a2feb5bff95d9bf2ef10becfe359b1cbc8601338418999c26cf4eee704181dedbcb487f4d63a06d8d5 +"axe-core@npm:^4.10.0, axe-core@npm:^4.2.0": + version: 4.10.3 + resolution: "axe-core@npm:4.10.3" + checksum: 10/9ff51ad0fd0fdec5c0247ea74e8ace5990b54c7f01f8fa3e5cd8ba98b0db24d8ebd7bab4a9bd4d75c28c4edcd1eac455b44c8c6c258c6a98f3d2f88bc60af4cc languageName: node linkType: hard @@ -44141,6 +44153,7 @@ __metadata: "@octokit/rest": "npm:^19.0.3" "@playwright/test": "npm:^1.32.3" "@spotify/eslint-plugin": "npm:^15.0.0" + "@storybook/addon-a11y": "npm:^9.1.5" "@storybook/addon-docs": "npm:^9.1.5" "@storybook/addon-links": "npm:^9.1.5" "@storybook/addon-themes": "npm:^9.1.5" From 391ce9fae5a6209fb5a0d20051955f6315ef1481 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Mon, 15 Sep 2025 15:50:51 -0500 Subject: [PATCH 207/233] Added Generated Scaffolder API Docs Signed-off-by: Andre Wanlin --- microsite/docusaurus.config.ts | 5 +++++ microsite/sidebars.ts | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index d7538cd12d..d801d41ca3 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -297,6 +297,11 @@ const config: Config = { specPath: '../plugins/search-backend/src/schema/openapi.yaml', outputDir: '../docs/features/search/api', } satisfies OpenApiPlugin.Options, + scaffolder: { + ...defaultOpenApiOptions, + specPath: '../plugins/scaffolder-backend/src/schema/openapi.yaml', + outputDir: '../docs/features/software-templates/api', + } satisfies OpenApiPlugin.Options, }, }, ], diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 5a408d5d1f..5ebdb8b062 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -14,6 +14,10 @@ const catalogSidebar = tryToLoadCustomSidebar( const searchSidebar = tryToLoadCustomSidebar( '../docs/features/search/api/sidebar.ts', ); +const scaffolderSidebar = tryToLoadCustomSidebar( + '../docs/features/software-templates/api/sidebar.ts', +); + export default { docs: { @@ -229,6 +233,22 @@ export default { 'features/software-templates/dry-run-testing', 'features/software-templates/experimental', 'features/software-templates/templating-extensions', + { + type: 'category', + label: 'API', + link: + scaffolderSidebar.length > 0 + ? { + type: 'generated-index', + title: 'Scaffolder API', + slug: '/category/scaffolder-api', + } + : { + type: 'doc', + id: 'openapi/generated-docs/404', + }, + items: scaffolderSidebar, + }, ], }, { From 794adf81d77017db224819ec9764495d29dcd386 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 15 Sep 2025 21:52:29 +0100 Subject: [PATCH 208/233] Update preview.tsx Signed-off-by: Charles de Dreuille --- .storybook/preview.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 6547950705..8f987ebab1 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -95,9 +95,10 @@ const preview: Preview = { chromatic: { modes: { 'light backstage': allModes['light backstage'], - 'dark backstage': allModes['dark backstage'], - 'light spotify': allModes['light spotify'], - 'dark spotify': allModes['dark spotify'], + // TODO: Enable these modes when we have more Chromatic snapshots. + // 'dark backstage': allModes['dark backstage'], + // 'light spotify': allModes['light spotify'], + // 'dark spotify': allModes['dark spotify'], }, }, }, From 0a447c8c9ba4ecca1ad2c632936c6bee8fb516b8 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 15 Sep 2025 22:04:57 +0100 Subject: [PATCH 209/233] Enable TurboSnaps Signed-off-by: Charles de Dreuille --- .github/workflows/verify_storybook.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index 1230d09168..540a6a3f9a 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -60,3 +60,4 @@ jobs: # https://www.chromatic.com/docs/custom-ci-provider#run-chromatic-on-external-forks-of-open-source-projects projectToken: chpt_dab72dc0f97d55b storybookBuildDir: dist-storybook-chromatic + onlyChanged: true From d4d11c0b82a403a89df2d0d60308c5ead153589c Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Mon, 15 Sep 2025 16:25:17 -0500 Subject: [PATCH 210/233] Ran prettier Signed-off-by: Andre Wanlin --- microsite/sidebars.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 5ebdb8b062..72608f4bac 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -18,7 +18,6 @@ const scaffolderSidebar = tryToLoadCustomSidebar( '../docs/features/software-templates/api/sidebar.ts', ); - export default { docs: { Overview: [ From 4a5830f8fd8d11b7497ab6b7652fbdaf1f422dff Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 15 Sep 2025 22:32:51 +0100 Subject: [PATCH 211/233] Fix Chromatic Signed-off-by: Charles de Dreuille --- .github/workflows/verify_storybook.yml | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index 540a6a3f9a..7c88a8f82e 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -49,11 +49,11 @@ jobs: with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - - name: Build Storybook for Chromatic + - name: Build Storybook run: yarn build-storybook:chromatic - - name: Deploy Storybook to Chromatic - uses: chromaui/action@1cfa065cbdab28f6ca3afaeb3d761383076a35aa # v11 + - name: Run Chromatic + uses: chromaui/action@latest with: token: ${{ secrets.GITHUB_TOKEN }} # projectToken intentionally shared to allow collaborators to run Chromatic on forks diff --git a/package.json b/package.json index 99a6192653..a048db1c60 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ }, "scripts": { "build-storybook": "storybook build --output-dir dist-storybook", - "build-storybook:chromatic": "STORYBOOK_STORY_SET=chromatic storybook build --output-dir dist-storybook-chromatic", + "build-storybook:chromatic": "STORYBOOK_STORY_SET=chromatic storybook build --stats-json --output-dir dist-storybook-chromatic", "build:all": "backstage-cli repo build --all", "build:api-docs": "LANG=en_EN yarn build:api-reports --docs --exclude 'plugins/@(api-docs|api-docs-module-protoc-gen-doc|app-visualizer|catalog-graph|catalog-import|catalog-unprocessed-entities|config-schema|example-todo-list|example-todo-list-backend)'", "build:api-reports": "yarn build:api-reports:only --tsc", From a95cebdcb9253528da95f24cfce260fe5988ab45 Mon Sep 17 00:00:00 2001 From: Paul Schultz Date: Mon, 15 Sep 2025 20:26:11 -0500 Subject: [PATCH 212/233] fix: clean up types Signed-off-by: Paul Schultz --- .changeset/rude-socks-serve.md | 7 +++++++ packages/types/src/observable.ts | 2 +- .../src/lib/SlackNotificationProcessor.ts | 4 +++- .../notifications-backend/migrations/20231215_init.js | 5 +++++ .../migrations/20240221_removeDone.js | 8 ++++++++ .../migrations/20240313_broadcast.js | 7 ++++++- .../migrations/20240808_settings.js | 6 ++++++ .../notifications-backend/migrations/20240902_addIcon.js | 8 ++++++++ .../migrations/20250317_addTopic.js | 9 +++++++++ 9 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 .changeset/rude-socks-serve.md diff --git a/.changeset/rude-socks-serve.md b/.changeset/rude-socks-serve.md new file mode 100644 index 0000000000..de75682e8c --- /dev/null +++ b/.changeset/rude-socks-serve.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-notifications-backend-module-slack': patch +'@backstage/plugin-notifications-backend': patch +'@backstage/types': patch +--- + +Internal refactoring for better type support diff --git a/packages/types/src/observable.ts b/packages/types/src/observable.ts index f7292c293c..667875ccd1 100644 --- a/packages/types/src/observable.ts +++ b/packages/types/src/observable.ts @@ -46,7 +46,7 @@ export type Subscription = { // We get the actual runtime polyfill from zen-observable declare global { interface SymbolConstructor { - readonly observable: symbol; + readonly observable: unique symbol; } } diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts index c587580657..f7dbc6a93e 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts @@ -42,7 +42,9 @@ export class SlackNotificationProcessor implements NotificationProcessor { private readonly catalog: CatalogService; private readonly auth: AuthService; private readonly slack: WebClient; - private readonly sendNotifications; + private readonly sendNotifications: ( + opts: ChatPostMessageArguments[], + ) => Promise; private readonly messagesSent: Counter; private readonly messagesFailed: Counter; private readonly broadcastChannels?: string[]; diff --git a/plugins/notifications-backend/migrations/20231215_init.js b/plugins/notifications-backend/migrations/20231215_init.js index a1e57237b6..fbe597534c 100644 --- a/plugins/notifications-backend/migrations/20231215_init.js +++ b/plugins/notifications-backend/migrations/20231215_init.js @@ -14,6 +14,11 @@ * limitations under the License. */ +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ exports.up = async function up(knex) { await knex.schema.createTable('notification', table => { table.uuid('id').primary(); diff --git a/plugins/notifications-backend/migrations/20240221_removeDone.js b/plugins/notifications-backend/migrations/20240221_removeDone.js index d67ac81825..2397ab69b3 100644 --- a/plugins/notifications-backend/migrations/20240221_removeDone.js +++ b/plugins/notifications-backend/migrations/20240221_removeDone.js @@ -14,6 +14,11 @@ * limitations under the License. */ +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ exports.up = async function up(knex) { await knex.schema.alterTable('notification', table => { table.text('link').nullable().alter(); @@ -21,6 +26,9 @@ exports.up = async function up(knex) { }); }; +/** + * @param {import('knex').Knex} knex + */ exports.down = async function down(knex) { await knex.schema.alterTable('notification', table => { table.text('link').notNullable().alter(); diff --git a/plugins/notifications-backend/migrations/20240313_broadcast.js b/plugins/notifications-backend/migrations/20240313_broadcast.js index 8b9ddeb24d..dd3ecd58cc 100644 --- a/plugins/notifications-backend/migrations/20240313_broadcast.js +++ b/plugins/notifications-backend/migrations/20240313_broadcast.js @@ -14,6 +14,11 @@ * limitations under the License. */ +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ exports.up = async function up(knex) { await knex.schema.createTable('broadcast', table => { table.uuid('id').primary(); @@ -37,7 +42,7 @@ exports.up = async function up(knex) { table .foreign('broadcast_id') - .references(['id']) + .references('id') .inTable('broadcast') .onDelete('CASCADE'); table.unique(['broadcast_id', 'user'], { diff --git a/plugins/notifications-backend/migrations/20240808_settings.js b/plugins/notifications-backend/migrations/20240808_settings.js index 4d4fbc24cd..8d2351d0c5 100644 --- a/plugins/notifications-backend/migrations/20240808_settings.js +++ b/plugins/notifications-backend/migrations/20240808_settings.js @@ -13,6 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ exports.up = async function up(knex) { await knex.schema.createTable('user_settings', table => { table.string('user').notNullable(); diff --git a/plugins/notifications-backend/migrations/20240902_addIcon.js b/plugins/notifications-backend/migrations/20240902_addIcon.js index a5327f8d73..f57948e08a 100644 --- a/plugins/notifications-backend/migrations/20240902_addIcon.js +++ b/plugins/notifications-backend/migrations/20240902_addIcon.js @@ -14,6 +14,11 @@ * limitations under the License. */ +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ exports.up = async function up(knex) { await knex.schema.alterTable('notification', table => { table.string('icon', 255).nullable(); @@ -23,6 +28,9 @@ exports.up = async function up(knex) { }); }; +/** + * @param {import('knex').Knex} knex + */ exports.down = async function down(knex) { await knex.schema.alterTable('notification', table => { table.dropColumn('icon'); diff --git a/plugins/notifications-backend/migrations/20250317_addTopic.js b/plugins/notifications-backend/migrations/20250317_addTopic.js index e17b8d0789..c2d4d7b10e 100644 --- a/plugins/notifications-backend/migrations/20250317_addTopic.js +++ b/plugins/notifications-backend/migrations/20250317_addTopic.js @@ -13,8 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +// @ts-check + const crypto = require('crypto'); +/** + * @param {import('knex').Knex} knex + */ exports.up = async function up(knex) { await knex.schema.alterTable('user_settings', table => { table.string('topic').nullable().after('origin'); @@ -41,6 +47,9 @@ exports.up = async function up(knex) { }); }; +/** + * @param {import('knex').Knex} knex + */ exports.down = async function down(knex) { await knex.schema.table('user_settings', table => { table.dropUnique([], 'user_settings_unique_idx'); From 60425a7fcf173f654a569535b5d54f4bc6740651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 16 Sep 2025 09:10:20 +0200 Subject: [PATCH 213/233] changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- plugins/catalog-react/report-alpha.api.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 30cdd91704..cf28d80c11 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -92,12 +92,12 @@ export const catalogReactTranslationRef: TranslationRef< readonly 'entityTableColumnTitle.title': 'Title'; readonly 'entityTableColumnTitle.description': 'Description'; readonly 'entityTableColumnTitle.domain': 'Domain'; + readonly 'entityTableColumnTitle.system': 'System'; + readonly 'entityTableColumnTitle.tags': 'Tags'; readonly 'entityTableColumnTitle.namespace': 'Namespace'; readonly 'entityTableColumnTitle.lifecycle': 'Lifecycle'; readonly 'entityTableColumnTitle.owner': 'Owner'; - readonly 'entityTableColumnTitle.system': 'System'; readonly 'entityTableColumnTitle.targets': 'Targets'; - readonly 'entityTableColumnTitle.tags': 'Tags'; } >; @@ -533,8 +533,8 @@ export const EntityTableColumnTitle: ({ translationKey, }: EntityTableColumnTitleProps) => | 'Title' - | 'Domain' | 'System' + | 'Domain' | 'Lifecycle' | 'Namespace' | 'Owner' From d5fccd00ff2f329a87c80d07eed7ea8f8c31151d Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 16 Sep 2025 10:46:46 +0200 Subject: [PATCH 214/233] chore: add docs Signed-off-by: benjdlambert --- plugins/mcp-actions-backend/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/plugins/mcp-actions-backend/README.md b/plugins/mcp-actions-backend/README.md index c44bd7f0a6..3b0a01f525 100644 --- a/plugins/mcp-actions-backend/README.md +++ b/plugins/mcp-actions-backend/README.md @@ -103,6 +103,29 @@ node -p 'require("crypto").randomBytes(24).toString("base64")' Set the `MCP_TOKEN` environment variable with this token, and configure your MCP client to use it in the [Authorization header](#configuring-mcp-clients) +#### Experimental: Dynamic Client Registration + +> This is highly experimental, proceed with caution. + +You can configure both the `auth-backend` and install the `auth` frontend plugin in order to enable [Dynamic Client Registration](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#dynamic-client-registration) with MCP Clients. + +This means that there is no token required in your MCP settings, and a token will be given to a client that requests a token on your behalf. When adding the MCP server to an MCP client like Cursor or Claude, a popup that requires your approval will be opened in your Backstage instance, which is powered by the `auth` plugin. + +You will need to add the `@backstage/plugin-auth` package to your `app` `package.json`, and enable the following config in `app-config.yaml`: + +```yaml +auth: + experimentalDynamicClientRegistration: + # enable the feature + enabled: true + + # this is optional and will default to *, but you can limit the callback URLs which are valid for added security + allowedRedirectUriPatterns: + - cursor://* +``` + +> Note: the `@backstage/plugin-auth` package is only available currently in the new frontend system. + ## Configuring MCP Clients The MCP server supports both Server-Sent Events (SSE) and Streamable HTTP protocols. From 6a40d329559ced37c61103cf7627faa5a9c99b17 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Tue, 16 Sep 2025 11:17:49 +0200 Subject: [PATCH 215/233] Update plugins/mcp-actions-backend/README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Ben Lambert --- plugins/mcp-actions-backend/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/mcp-actions-backend/README.md b/plugins/mcp-actions-backend/README.md index 3b0a01f525..99e364a8ce 100644 --- a/plugins/mcp-actions-backend/README.md +++ b/plugins/mcp-actions-backend/README.md @@ -105,6 +105,7 @@ Set the `MCP_TOKEN` environment variable with this token, and configure your MCP #### Experimental: Dynamic Client Registration +> [!CAUTION] > This is highly experimental, proceed with caution. You can configure both the `auth-backend` and install the `auth` frontend plugin in order to enable [Dynamic Client Registration](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#dynamic-client-registration) with MCP Clients. From ae1f66928b0391b696102d3dbdd2529574acdd4a Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Tue, 16 Sep 2025 11:18:10 +0200 Subject: [PATCH 216/233] Apply suggestion from @freben MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Ben Lambert --- plugins/mcp-actions-backend/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/mcp-actions-backend/README.md b/plugins/mcp-actions-backend/README.md index 99e364a8ce..4b4dad153b 100644 --- a/plugins/mcp-actions-backend/README.md +++ b/plugins/mcp-actions-backend/README.md @@ -108,7 +108,7 @@ Set the `MCP_TOKEN` environment variable with this token, and configure your MCP > [!CAUTION] > This is highly experimental, proceed with caution. -You can configure both the `auth-backend` and install the `auth` frontend plugin in order to enable [Dynamic Client Registration](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#dynamic-client-registration) with MCP Clients. +You can configure the `auth-backend` and install the `auth` frontend plugin in order to enable [Dynamic Client Registration](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#dynamic-client-registration) with MCP Clients. This means that there is no token required in your MCP settings, and a token will be given to a client that requests a token on your behalf. When adding the MCP server to an MCP client like Cursor or Claude, a popup that requires your approval will be opened in your Backstage instance, which is powered by the `auth` plugin. From b1b17dd269143103e0fb892d06c45047a10da10a Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Tue, 16 Sep 2025 11:18:17 +0200 Subject: [PATCH 217/233] Apply suggestion from @freben MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Ben Lambert --- plugins/mcp-actions-backend/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/mcp-actions-backend/README.md b/plugins/mcp-actions-backend/README.md index 4b4dad153b..d81d0207c5 100644 --- a/plugins/mcp-actions-backend/README.md +++ b/plugins/mcp-actions-backend/README.md @@ -125,7 +125,8 @@ auth: - cursor://* ``` -> Note: the `@backstage/plugin-auth` package is only available currently in the new frontend system. +> [!NOTE] +> The `@backstage/plugin-auth` package is currently only available in the new frontend system. ## Configuring MCP Clients From 7f2a4a0de0461bd0bd8e1b977bc6847a9c49f7d9 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 16 Sep 2025 11:19:30 +0200 Subject: [PATCH 218/233] chore: fix changeset Signed-off-by: benjdlambert --- .changeset/flat-groups-remain.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/flat-groups-remain.md diff --git a/.changeset/flat-groups-remain.md b/.changeset/flat-groups-remain.md new file mode 100644 index 0000000000..5e61c8925d --- /dev/null +++ b/.changeset/flat-groups-remain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-mcp-actions-backend': patch +--- + +Updating docs From 58874c4e9c21bed2591dfd7bba965c729e689f2a Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Mon, 1 Sep 2025 08:57:39 +0300 Subject: [PATCH 219/233] feat(catalog): config to support disabling processors and providers this adds support to disable individual catalog processors and providers via config. relates to #31007 Signed-off-by: Hellgren Heikki --- .changeset/wet-crabs-send.md | 5 ++ plugins/catalog-backend/config.d.ts | 32 +++++++++++ .../src/service/CatalogBuilder.ts | 57 +++++++++++++++++-- 3 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 .changeset/wet-crabs-send.md diff --git a/.changeset/wet-crabs-send.md b/.changeset/wet-crabs-send.md new file mode 100644 index 0000000000..edac0562fd --- /dev/null +++ b/.changeset/wet-crabs-send.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Add support to disable catalog providers and processors via configuration diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index a506476dbf..659cf82280 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -221,5 +221,37 @@ export interface Config { * housing catalog-info files. */ processingInterval?: HumanDuration | false; + + /** + * Catalog provide specific configuration. + * Additional configuration for providers are specified in the catalog + * modules. + */ + providers?: { + /** + * Name is the provider ID, e.g. "bitbucketServer" + */ + [name: string]: { + /** + * Whether the provider is enabled or not. Defaults to true. + */ + enabled?: boolean; + }; + }; + /** + * Configuration for entity processors. Additional configuration for + * processors are specified in the catalog modules. + */ + processors?: { + /** + * Name is the processor ID, e.g. "catalog-processor" + */ + [name: string]: { + /** + * Whether the processor is enabled or not. Defaults to true. + */ + enabled?: boolean; + }; + }; }; } diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index e697c6e77f..aab2efd1cb 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -568,9 +568,11 @@ export class CatalogBuilder { const locationStore = new DefaultLocationStore(dbClient); const configLocationProvider = new ConfigLocationEntityProvider(config); - const entityProviders = lodash.uniqBy( - [...this.entityProviders, locationStore, configLocationProvider], - provider => provider.getProviderName(), + const entityProviders = this.filterProviders( + lodash.uniqBy( + [...this.entityProviders, locationStore, configLocationProvider], + provider => provider.getProviderName(), + ), ); const processingEngine = new DefaultCatalogProcessingEngine({ @@ -725,7 +727,30 @@ export class CatalogBuilder { this.checkMissingExternalProcessors(processors); - return processors; + return this.filterProcessors(processors); + } + + private filterProcessors(processors: CatalogProcessor[]) { + const { config } = this.env; + const processorsConfig = config.getOptionalConfig('catalog.processors'); + if (!processorsConfig) { + return processors; + } + const keys = Object.keys(processorsConfig); + for (const key of keys) { + if (!processors.find(p => p.getProcessorName() === key)) { + this.env.logger.warn( + `Invalid catalog processor configuration catalog.processors.${key}: no such processor`, + ); + } + } + + return processors.filter(p => { + const processorConfig = processorsConfig.getOptionalConfig( + p.getProcessorName(), + ); + return processorConfig?.getOptionalBoolean('enabled') ?? true; + }); } // TODO(Rugvip): These old processors are removed, for a while we'll be throwing @@ -838,6 +863,30 @@ export class CatalogBuilder { ); } + private filterProviders(providers: EntityProvider[]) { + const { config, logger } = this.env; + const providersConfig = config.getOptionalConfig('catalog.providers'); + if (!providersConfig) { + return providers; + } + + const keys = Object.keys(providersConfig); + for (const key of keys) { + if (!providers.find(p => p.getProviderName() === key)) { + logger.warn( + `Invalid catalog provider configuration catalog.providers.${key}: no such provider`, + ); + } + } + + return providers.filter(p => { + const providerConfig = providersConfig.getOptionalConfig( + p.getProviderName(), + ); + return providerConfig?.getOptionalBoolean('enabled') ?? true; + }); + } + private static getDefaultProcessingInterval( config: Config, ): ProcessingIntervalFunction { From 81d0334ddc123f33a1ea801046decf9dddfdeb97 Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Tue, 16 Sep 2025 12:24:03 +0300 Subject: [PATCH 220/233] fix: remove logging of invalid configs for now Signed-off-by: Hellgren Heikki --- .../src/service/CatalogBuilder.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index aab2efd1cb..72d54b3ef7 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -736,14 +736,6 @@ export class CatalogBuilder { if (!processorsConfig) { return processors; } - const keys = Object.keys(processorsConfig); - for (const key of keys) { - if (!processors.find(p => p.getProcessorName() === key)) { - this.env.logger.warn( - `Invalid catalog processor configuration catalog.processors.${key}: no such processor`, - ); - } - } return processors.filter(p => { const processorConfig = processorsConfig.getOptionalConfig( @@ -870,15 +862,6 @@ export class CatalogBuilder { return providers; } - const keys = Object.keys(providersConfig); - for (const key of keys) { - if (!providers.find(p => p.getProviderName() === key)) { - logger.warn( - `Invalid catalog provider configuration catalog.providers.${key}: no such provider`, - ); - } - } - return providers.filter(p => { const providerConfig = providersConfig.getOptionalConfig( p.getProviderName(), From a7a471e907c1358384c0cba79c6eadbaead5016f Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Tue, 16 Sep 2025 12:39:00 +0300 Subject: [PATCH 221/233] fix: handle non-object config values Signed-off-by: Hellgren Heikki --- .../src/service/CatalogBuilder.ts | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 72d54b3ef7..39387707ea 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -738,10 +738,15 @@ export class CatalogBuilder { } return processors.filter(p => { - const processorConfig = processorsConfig.getOptionalConfig( - p.getProcessorName(), - ); - return processorConfig?.getOptionalBoolean('enabled') ?? true; + try { + const processorConfig = processorsConfig.getOptionalConfig( + p.getProcessorName(), + ); + return processorConfig?.getOptionalBoolean('enabled') ?? true; + } catch (_) { + // In case the processor config is not an object, just include the processor + return true; + } }); } @@ -863,10 +868,15 @@ export class CatalogBuilder { } return providers.filter(p => { - const providerConfig = providersConfig.getOptionalConfig( - p.getProviderName(), - ); - return providerConfig?.getOptionalBoolean('enabled') ?? true; + try { + const providerConfig = providersConfig.getOptionalConfig( + p.getProviderName(), + ); + return providerConfig?.getOptionalBoolean('enabled') ?? true; + } catch (_) { + // In case the provider config is not an object, just include the provider + return true; + } }); } From 934928ed380a747ecda5ff7ab8aa87d3bd9c584a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 16 Sep 2025 11:50:44 +0200 Subject: [PATCH 222/233] Update plugins/catalog-backend/src/service/CatalogBuilder.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- plugins/catalog-backend/src/service/CatalogBuilder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 39387707ea..1e2957dadd 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -861,7 +861,7 @@ export class CatalogBuilder { } private filterProviders(providers: EntityProvider[]) { - const { config, logger } = this.env; + const { config } = this.env; const providersConfig = config.getOptionalConfig('catalog.providers'); if (!providersConfig) { return providers; From 2bbd24f58fd1ff666e34710378978ae7fb413d33 Mon Sep 17 00:00:00 2001 From: Hellgren Heikki Date: Fri, 22 Aug 2025 10:37:53 +0300 Subject: [PATCH 223/233] feat(catalog): processor priority This change enables the ordering of catalog processors by their priority, allowing for more control over the catalog processing sequence. The default priority is set to 20, and processors can be assigned a custom priority to influence their execution order. Lower number indicates higher priority. The priority can be set by implementing the `getPriority` method in the processor class or by adding a `catalog.processors..priority` configuration in the `app-config.yaml` file. The configuration takes precedence over the method. closes #30963 Signed-off-by: Hellgren Heikki --- .changeset/curvy-terms-jam.md | 14 ++++++++++ .../software-catalog/life-of-an-entity.md | 4 +++ plugins/catalog-backend/config.d.ts | 6 +++++ .../src/service/CatalogBuilder.ts | 27 ++++++++++++++++++- plugins/catalog-node/report.api.md | 1 + plugins/catalog-node/src/api/processor.ts | 8 ++++++ 6 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 .changeset/curvy-terms-jam.md diff --git a/.changeset/curvy-terms-jam.md b/.changeset/curvy-terms-jam.md new file mode 100644 index 0000000000..d8070bc8ce --- /dev/null +++ b/.changeset/curvy-terms-jam.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-node': patch +--- + +Order catalog processors by priority. + +This change enables the ordering of catalog processors by their priority, +allowing for more control over the catalog processing sequence. +The default priority is set to 20, and processors can be assigned a custom +priority to influence their execution order. Lower number indicates higher priority. +The priority can be set by implementing the `getPriority` method in the processor class +or by adding a `catalog.processors..priority` configuration +in the `app-config.yaml` file. The configuration takes precedence over the method. diff --git a/docs/features/software-catalog/life-of-an-entity.md b/docs/features/software-catalog/life-of-an-entity.md index 15f30210e8..628b083ed2 100644 --- a/docs/features/software-catalog/life-of-an-entity.md +++ b/docs/features/software-catalog/life-of-an-entity.md @@ -132,6 +132,10 @@ in the registration order, but this does not apply over multiple catalog modules the order of registration depends on the order in which the modules are loaded by the framework. +It's possible to customize the order of the processors by modifying the +`catalog.processors..priority` configuration option. +The default priority is `20`, and lower value means that the processor runs earlier. + ::: Each step has the opportunity to optionally modify the entity, and to optionally diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 659cf82280..f1b72e5b45 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -251,6 +251,12 @@ export interface Config { * Whether the processor is enabled or not. Defaults to true. */ enabled?: boolean; + /** + * The priority of the processor, which is used to determine the order in which + * processors are run. The default priority is 20, and lower value means + * that the processor runs earlier. + */ + priority?: number; }; }; }; diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 1e2957dadd..dcd677152e 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -727,7 +727,32 @@ export class CatalogBuilder { this.checkMissingExternalProcessors(processors); - return this.filterProcessors(processors); + const filteredProcessors = this.filterProcessors(processors); + + // Lastly sort the processors by priority. Config can override the + // priority of a processor to allow control of 3rd party processors. + filteredProcessors.sort((a, b) => { + const getProcessorPriority = (processor: CatalogProcessor) => { + try { + return ( + config.getOptionalNumber( + `catalog.processors.${processor.getProcessorName()}.priority`, + ) ?? + processor.getPriority?.() ?? + 20 + ); + } catch (_) { + // In case the processor config is not an object, just return default priority + return 20; + } + }; + + const aPriority = getProcessorPriority(a); + const bPriority = getProcessorPriority(b); + return aPriority - bPriority; + }); + + return filteredProcessors; } private filterProcessors(processors: CatalogProcessor[]) { diff --git a/plugins/catalog-node/report.api.md b/plugins/catalog-node/report.api.md index 25ab9145ef..9a9f91865c 100644 --- a/plugins/catalog-node/report.api.md +++ b/plugins/catalog-node/report.api.md @@ -60,6 +60,7 @@ export type CatalogProcessor = { emit: CatalogProcessorEmit, cache: CatalogProcessorCache, ): Promise; + getPriority?(): number; }; // @public diff --git a/plugins/catalog-node/src/api/processor.ts b/plugins/catalog-node/src/api/processor.ts index e43fdef071..3aa5bf0fa7 100644 --- a/plugins/catalog-node/src/api/processor.ts +++ b/plugins/catalog-node/src/api/processor.ts @@ -100,6 +100,14 @@ export type CatalogProcessor = { emit: CatalogProcessorEmit, cache: CatalogProcessorCache, ): Promise; + + /** + * Returns a processor priority, which is used to determine the order in which + * processors are run. The default priority is 20, and lower value means + * that the processor runs earlier. + * @returns A number representing the priority of the processor. + */ + getPriority?(): number; }; /** From b799a2d07fbe42d2ad4dc53336d2cd1aefc83494 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 Sep 2025 13:22:58 +0000 Subject: [PATCH 224/233] Version Packages --- .changeset/afraid-streets-pay.md | 5 - .changeset/all-bats-shine.md | 5 - .changeset/angry-heads-design.md | 5 - .changeset/better-eagles-tickle.md | 5 - .changeset/big-cameras-turn.md | 5 - .changeset/brave-bugs-know.md | 5 - .changeset/brave-jars-speak.md | 5 - .changeset/brown-pens-shave.md | 18 - .changeset/bumpy-eyes-divide.md | 5 - .changeset/busy-chairs-itch.md | 5 - .changeset/chilly-llamas-attend.md | 5 - .changeset/clear-houses-wonder.md | 5 - .changeset/cold-donuts-train.md | 5 - .changeset/cold-garlics-care.md | 5 - .changeset/cool-games-rescue.md | 5 - .changeset/create-app-1757429965.md | 5 - .changeset/cuddly-lemons-cheat.md | 5 - .changeset/curvy-sites-rhyme.md | 5 - .changeset/curvy-terms-jam.md | 14 - .changeset/dark-teams-give.md | 5 - .changeset/dirty-spies-drop.md | 5 - .changeset/dull-eagles-sin.md | 6 - .changeset/easy-wings-turn.md | 5 - .changeset/eighty-numbers-act.md | 5 - .changeset/eleven-doors-down.md | 5 - .changeset/eleven-doors-own.md | 5 - .changeset/fast-corners-roll.md | 11 - .changeset/fine-hands-think.md | 5 - .changeset/fix-knip-reports-spaces-path.md | 5 - .changeset/fix-select-aria-props.md | 5 - .changeset/flat-colts-know.md | 5 - .changeset/flat-groups-remain.md | 5 - .changeset/funny-eagles-try.md | 7 - .changeset/giant-buttons-flash.md | 5 - .changeset/giant-zebras-peel.md | 5 - .changeset/gold-words-smoke.md | 5 - .changeset/heavy-cats-unite.md | 6 - .changeset/heavy-lies-listen.md | 5 - .changeset/hot-friends-act.md | 5 - .changeset/hungry-carrots-grow.md | 5 - .changeset/icy-camels-throw.md | 7 - .changeset/itchy-moons-start.md | 5 - .changeset/kind-eyes-worry.md | 5 - .changeset/large-otters-invent.md | 9 - .changeset/late-swans-press.md | 5 - .changeset/legal-lemons-attend.md | 5 - .changeset/lemon-jobs-create.md | 5 - .changeset/lemon-terms-cheer.md | 5 - .changeset/loose-memes-bathe.md | 5 - .changeset/loud-forks-teach.md | 5 - .changeset/lovely-actors-love.md | 5 - .changeset/lovely-frogs-share.md | 6 - .changeset/lucky-glasses-slide.md | 5 - .changeset/mean-sites-cheer.md | 5 - .changeset/nine-socks-share.md | 5 - .changeset/olive-moons-burn.md | 24 - .changeset/pre.json | 256 -- .changeset/puny-books-fetch.md | 5 - .changeset/puny-tires-fold.md | 5 - .changeset/quiet-papayas-mate.md | 5 - .changeset/red-shrimps-fall.md | 5 - .changeset/ripe-boxes-stand.md | 5 - .changeset/ripe-plants-pump.md | 5 - .changeset/rotten-hairs-attack.md | 25 - .changeset/rude-socks-serve.md | 7 - .changeset/sharp-carrots-spend.md | 5 - .changeset/shy-chicken-smash.md | 5 - .changeset/silent-results-stick.md | 5 - .changeset/silly-horses-share.md | 9 - .changeset/silver-baboons-ring.md | 5 - .changeset/sixty-pans-prove.md | 5 - .changeset/sixty-places-push.md | 5 - .changeset/slick-lamps-clap.md | 11 - .changeset/slick-worms-drum.md | 5 - .changeset/social-beers-unite.md | 5 - .changeset/stale-adults-hammer.md | 5 - .changeset/stupid-areas-share.md | 5 - .changeset/stupid-pans-flash.md | 35 - .changeset/sweet-lemons-wonder.md | 5 - .changeset/tangy-squids-film.md | 5 - .changeset/tasty-yaks-kiss.md | 5 - .changeset/ten-boxes-lie.md | 5 - .changeset/thin-phones-press.md | 5 - .changeset/tiny-spoons-mix.md | 5 - .changeset/tired-cobras-fly.md | 5 - .changeset/tough-cars-cry.md | 5 - .changeset/tricky-buses-lead.md | 5 - .changeset/twenty-shoes-tickle.md | 12 - .changeset/two-coins-stop.md | 5 - .changeset/warm-emus-itch.md | 5 - .changeset/wet-crabs-send.md | 5 - .changeset/wet-kiwis-strive.md | 5 - .changeset/wet-onions-sneeze.md | 5 - .changeset/whole-dingos-lay.md | 5 - .changeset/yellow-dragons-float.md | 5 - .changeset/young-doodles-enter.md | 5 - docs/releases/v1.43.0-changelog.md | 2119 +++++++++++++++++ package.json | 2 +- packages/app-defaults/CHANGELOG.md | 13 + packages/app-defaults/package.json | 2 +- packages/app-next-example-plugin/CHANGELOG.md | 8 + packages/app-next-example-plugin/package.json | 2 +- packages/app-next/CHANGELOG.md | 41 + packages/app-next/package.json | 2 +- packages/app/CHANGELOG.md | 36 + packages/app/package.json | 2 +- packages/backend-app-api/CHANGELOG.md | 7 + packages/backend-app-api/package.json | 2 +- packages/backend-defaults/CHANGELOG.md | 22 + packages/backend-defaults/package.json | 2 +- .../CHANGELOG.md | 19 + .../package.json | 2 +- packages/backend-openapi-utils/CHANGELOG.md | 8 + packages/backend-openapi-utils/package.json | 2 +- packages/backend-plugin-api/CHANGELOG.md | 9 + packages/backend-plugin-api/package.json | 2 +- packages/backend-test-utils/CHANGELOG.md | 16 + packages/backend-test-utils/package.json | 2 +- packages/backend/CHANGELOG.md | 39 + packages/backend/package.json | 2 +- packages/catalog-client/CHANGELOG.md | 27 + packages/catalog-client/package.json | 2 +- packages/cli/CHANGELOG.md | 22 + packages/cli/package.json | 2 +- packages/config-loader/CHANGELOG.md | 8 + packages/config-loader/package.json | 2 +- packages/core-app-api/CHANGELOG.md | 13 + packages/core-app-api/package.json | 2 +- packages/core-compat-api/CHANGELOG.md | 10 + packages/core-compat-api/package.json | 2 +- packages/core-components/CHANGELOG.md | 14 + packages/core-components/package.json | 2 +- packages/core-plugin-api/CHANGELOG.md | 11 + packages/core-plugin-api/package.json | 2 +- packages/create-app/CHANGELOG.md | 8 + packages/create-app/package.json | 2 +- packages/dev-utils/CHANGELOG.md | 12 + packages/dev-utils/package.json | 2 +- packages/e2e-test/CHANGELOG.md | 7 + packages/e2e-test/package.json | 2 +- packages/frontend-app-api/CHANGELOG.md | 15 + packages/frontend-app-api/package.json | 2 +- packages/frontend-defaults/CHANGELOG.md | 11 + packages/frontend-defaults/package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- packages/frontend-internal/CHANGELOG.md | 8 + packages/frontend-internal/package.json | 2 +- packages/frontend-plugin-api/CHANGELOG.md | 14 + packages/frontend-plugin-api/package.json | 2 +- packages/frontend-test-utils/CHANGELOG.md | 11 + packages/frontend-test-utils/package.json | 2 +- packages/integration-react/CHANGELOG.md | 8 + packages/integration-react/package.json | 2 +- packages/integration/CHANGELOG.md | 10 + packages/integration/package.json | 2 +- packages/repo-tools/CHANGELOG.md | 9 + packages/repo-tools/package.json | 2 +- packages/scaffolder-internal/CHANGELOG.md | 8 + packages/scaffolder-internal/package.json | 2 +- .../techdocs-cli-embedded-app/CHANGELOG.md | 15 + .../techdocs-cli-embedded-app/package.json | 2 +- packages/techdocs-cli/CHANGELOG.md | 9 + packages/techdocs-cli/package.json | 2 +- packages/types/CHANGELOG.md | 6 + packages/types/package.json | 2 +- packages/ui/CHANGELOG.md | 8 + packages/ui/package.json | 2 +- plugins/api-docs/CHANGELOG.md | 12 + plugins/api-docs/package.json | 2 +- plugins/app-backend/CHANGELOG.md | 12 + plugins/app-backend/package.json | 2 +- plugins/app-node/CHANGELOG.md | 8 + plugins/app-node/package.json | 2 +- plugins/app-visualizer/CHANGELOG.md | 9 + plugins/app-visualizer/package.json | 2 +- plugins/app/CHANGELOG.md | 15 + plugins/app/package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 14 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- plugins/auth-backend/CHANGELOG.md | 12 + plugins/auth-backend/package.json | 2 +- plugins/auth-node/CHANGELOG.md | 11 + plugins/auth-node/package.json | 2 +- plugins/auth-react/CHANGELOG.md | 9 + plugins/auth-react/package.json | 2 +- plugins/auth/CHANGELOG.md | 12 + plugins/auth/package.json | 2 +- plugins/bitbucket-cloud-common/CHANGELOG.md | 7 + plugins/bitbucket-cloud-common/package.json | 2 +- .../catalog-backend-module-aws/CHANGELOG.md | 10 + .../catalog-backend-module-aws/package.json | 2 +- .../catalog-backend-module-azure/CHANGELOG.md | 9 + .../catalog-backend-module-azure/package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../catalog-backend-module-gcp/CHANGELOG.md | 8 + .../catalog-backend-module-gcp/package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../catalog-backend-module-gitea/CHANGELOG.md | 9 + .../catalog-backend-module-gitea/package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 16 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- .../catalog-backend-module-ldap/CHANGELOG.md | 9 + .../catalog-backend-module-ldap/package.json | 2 +- .../catalog-backend-module-logs/CHANGELOG.md | 9 + .../catalog-backend-module-logs/package.json | 2 +- .../CHANGELOG.md | 19 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- plugins/catalog-backend/CHANGELOG.md | 54 + plugins/catalog-backend/package.json | 2 +- plugins/catalog-graph/CHANGELOG.md | 18 + plugins/catalog-graph/package.json | 2 +- plugins/catalog-import/CHANGELOG.md | 14 + plugins/catalog-import/package.json | 2 +- plugins/catalog-node/CHANGELOG.md | 41 + plugins/catalog-node/package.json | 2 +- plugins/catalog-react/CHANGELOG.md | 39 + plugins/catalog-react/package.json | 2 +- .../catalog-unprocessed-entities/CHANGELOG.md | 10 + .../catalog-unprocessed-entities/package.json | 2 +- plugins/catalog/CHANGELOG.md | 20 + plugins/catalog/package.json | 2 +- plugins/config-schema/CHANGELOG.md | 9 + plugins/config-schema/package.json | 2 +- plugins/devtools-backend/CHANGELOG.md | 11 + plugins/devtools-backend/package.json | 2 +- plugins/devtools/CHANGELOG.md | 10 + plugins/devtools/package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../events-backend-module-azure/CHANGELOG.md | 8 + .../events-backend-module-azure/package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../events-backend-module-gerrit/CHANGELOG.md | 8 + .../events-backend-module-gerrit/package.json | 2 +- .../events-backend-module-github/CHANGELOG.md | 10 + .../events-backend-module-github/package.json | 2 +- .../events-backend-module-gitlab/CHANGELOG.md | 8 + .../events-backend-module-gitlab/package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../events-backend-module-kafka/CHANGELOG.md | 9 + .../events-backend-module-kafka/package.json | 2 +- .../events-backend-test-utils/CHANGELOG.md | 7 + .../events-backend-test-utils/package.json | 2 +- plugins/events-backend/CHANGELOG.md | 11 + plugins/events-backend/package.json | 2 +- plugins/events-node/CHANGELOG.md | 9 + plugins/events-node/package.json | 2 +- .../example-todo-list-backend/CHANGELOG.md | 7 + .../example-todo-list-backend/package.json | 2 +- plugins/example-todo-list/CHANGELOG.md | 8 + plugins/example-todo-list/package.json | 2 +- plugins/gateway-backend/CHANGELOG.md | 7 + plugins/gateway-backend/package.json | 2 +- plugins/home-react/CHANGELOG.md | 9 + plugins/home-react/package.json | 2 +- plugins/home/CHANGELOG.md | 16 + plugins/home/package.json | 2 +- plugins/kubernetes-backend/CHANGELOG.md | 15 + plugins/kubernetes-backend/package.json | 2 +- plugins/kubernetes-cluster/CHANGELOG.md | 10 + plugins/kubernetes-cluster/package.json | 2 +- plugins/kubernetes-node/CHANGELOG.md | 8 + plugins/kubernetes-node/package.json | 2 +- plugins/kubernetes-react/CHANGELOG.md | 9 + plugins/kubernetes-react/package.json | 2 +- plugins/kubernetes/CHANGELOG.md | 12 + plugins/kubernetes/package.json | 2 +- plugins/mcp-actions-backend/CHANGELOG.md | 16 + plugins/mcp-actions-backend/package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- plugins/notifications-backend/CHANGELOG.md | 22 + plugins/notifications-backend/package.json | 2 +- plugins/notifications-node/CHANGELOG.md | 17 + plugins/notifications-node/package.json | 2 +- plugins/notifications/CHANGELOG.md | 12 + plugins/notifications/package.json | 2 +- plugins/org-react/CHANGELOG.md | 10 + plugins/org-react/package.json | 2 +- plugins/org/CHANGELOG.md | 12 + plugins/org/package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- plugins/permission-backend/CHANGELOG.md | 9 + plugins/permission-backend/package.json | 2 +- plugins/permission-node/CHANGELOG.md | 8 + plugins/permission-node/package.json | 2 +- plugins/proxy-backend/CHANGELOG.md | 9 + plugins/proxy-backend/package.json | 2 +- plugins/proxy-node/CHANGELOG.md | 7 + plugins/proxy-node/package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 61 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- plugins/scaffolder-backend/CHANGELOG.md | 29 + plugins/scaffolder-backend/package.json | 2 +- plugins/scaffolder-common/CHANGELOG.md | 8 + plugins/scaffolder-common/package.json | 2 +- .../scaffolder-node-test-utils/CHANGELOG.md | 10 + .../scaffolder-node-test-utils/package.json | 2 +- plugins/scaffolder-node/CHANGELOG.md | 10 + plugins/scaffolder-node/package.json | 2 +- plugins/scaffolder-react/CHANGELOG.md | 14 + plugins/scaffolder-react/package.json | 2 +- plugins/scaffolder/CHANGELOG.md | 20 + plugins/scaffolder/package.json | 2 +- .../CHANGELOG.md | 10 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- .../CHANGELOG.md | 9 + .../package.json | 2 +- plugins/search-backend-module-pg/CHANGELOG.md | 8 + plugins/search-backend-module-pg/package.json | 2 +- .../CHANGELOG.md | 8 + .../package.json | 2 +- .../CHANGELOG.md | 11 + .../package.json | 2 +- plugins/search-backend-node/CHANGELOG.md | 7 + plugins/search-backend-node/package.json | 2 +- plugins/search-backend/CHANGELOG.md | 13 + plugins/search-backend/package.json | 2 +- plugins/search-react/CHANGELOG.md | 10 + plugins/search-react/package.json | 2 +- plugins/search/CHANGELOG.md | 13 + plugins/search/package.json | 2 +- plugins/signals-backend/CHANGELOG.md | 11 + plugins/signals-backend/package.json | 2 +- plugins/signals-node/CHANGELOG.md | 10 + plugins/signals-node/package.json | 2 +- plugins/signals/CHANGELOG.md | 11 + plugins/signals/package.json | 2 +- .../techdocs-addons-test-utils/CHANGELOG.md | 18 + .../techdocs-addons-test-utils/package.json | 2 +- plugins/techdocs-backend/CHANGELOG.md | 19 + plugins/techdocs-backend/package.json | 2 +- .../CHANGELOG.md | 12 + .../package.json | 2 +- plugins/techdocs-node/CHANGELOG.md | 8 + plugins/techdocs-node/package.json | 2 +- plugins/techdocs-react/CHANGELOG.md | 10 + plugins/techdocs-react/package.json | 2 +- plugins/techdocs/CHANGELOG.md | 25 + plugins/techdocs/package.json | 2 +- plugins/user-settings-backend/CHANGELOG.md | 11 + plugins/user-settings-backend/package.json | 2 +- plugins/user-settings/CHANGELOG.md | 15 + plugins/user-settings/package.json | 2 +- 444 files changed, 4397 insertions(+), 1032 deletions(-) delete mode 100644 .changeset/afraid-streets-pay.md delete mode 100644 .changeset/all-bats-shine.md delete mode 100644 .changeset/angry-heads-design.md delete mode 100644 .changeset/better-eagles-tickle.md delete mode 100644 .changeset/big-cameras-turn.md delete mode 100644 .changeset/brave-bugs-know.md delete mode 100644 .changeset/brave-jars-speak.md delete mode 100644 .changeset/brown-pens-shave.md delete mode 100644 .changeset/bumpy-eyes-divide.md delete mode 100644 .changeset/busy-chairs-itch.md delete mode 100644 .changeset/chilly-llamas-attend.md delete mode 100644 .changeset/clear-houses-wonder.md delete mode 100644 .changeset/cold-donuts-train.md delete mode 100644 .changeset/cold-garlics-care.md delete mode 100644 .changeset/cool-games-rescue.md delete mode 100644 .changeset/create-app-1757429965.md delete mode 100644 .changeset/cuddly-lemons-cheat.md delete mode 100644 .changeset/curvy-sites-rhyme.md delete mode 100644 .changeset/curvy-terms-jam.md delete mode 100644 .changeset/dark-teams-give.md delete mode 100644 .changeset/dirty-spies-drop.md delete mode 100644 .changeset/dull-eagles-sin.md delete mode 100644 .changeset/easy-wings-turn.md delete mode 100644 .changeset/eighty-numbers-act.md delete mode 100644 .changeset/eleven-doors-down.md delete mode 100644 .changeset/eleven-doors-own.md delete mode 100644 .changeset/fast-corners-roll.md delete mode 100644 .changeset/fine-hands-think.md delete mode 100644 .changeset/fix-knip-reports-spaces-path.md delete mode 100644 .changeset/fix-select-aria-props.md delete mode 100644 .changeset/flat-colts-know.md delete mode 100644 .changeset/flat-groups-remain.md delete mode 100644 .changeset/funny-eagles-try.md delete mode 100644 .changeset/giant-buttons-flash.md delete mode 100644 .changeset/giant-zebras-peel.md delete mode 100644 .changeset/gold-words-smoke.md delete mode 100644 .changeset/heavy-cats-unite.md delete mode 100644 .changeset/heavy-lies-listen.md delete mode 100644 .changeset/hot-friends-act.md delete mode 100644 .changeset/hungry-carrots-grow.md delete mode 100644 .changeset/icy-camels-throw.md delete mode 100644 .changeset/itchy-moons-start.md delete mode 100644 .changeset/kind-eyes-worry.md delete mode 100644 .changeset/large-otters-invent.md delete mode 100644 .changeset/late-swans-press.md delete mode 100644 .changeset/legal-lemons-attend.md delete mode 100644 .changeset/lemon-jobs-create.md delete mode 100644 .changeset/lemon-terms-cheer.md delete mode 100644 .changeset/loose-memes-bathe.md delete mode 100644 .changeset/loud-forks-teach.md delete mode 100644 .changeset/lovely-actors-love.md delete mode 100644 .changeset/lovely-frogs-share.md delete mode 100644 .changeset/lucky-glasses-slide.md delete mode 100644 .changeset/mean-sites-cheer.md delete mode 100644 .changeset/nine-socks-share.md delete mode 100644 .changeset/olive-moons-burn.md delete mode 100644 .changeset/pre.json delete mode 100644 .changeset/puny-books-fetch.md delete mode 100644 .changeset/puny-tires-fold.md delete mode 100644 .changeset/quiet-papayas-mate.md delete mode 100644 .changeset/red-shrimps-fall.md delete mode 100644 .changeset/ripe-boxes-stand.md delete mode 100644 .changeset/ripe-plants-pump.md delete mode 100644 .changeset/rotten-hairs-attack.md delete mode 100644 .changeset/rude-socks-serve.md delete mode 100644 .changeset/sharp-carrots-spend.md delete mode 100644 .changeset/shy-chicken-smash.md delete mode 100644 .changeset/silent-results-stick.md delete mode 100644 .changeset/silly-horses-share.md delete mode 100644 .changeset/silver-baboons-ring.md delete mode 100644 .changeset/sixty-pans-prove.md delete mode 100644 .changeset/sixty-places-push.md delete mode 100644 .changeset/slick-lamps-clap.md delete mode 100644 .changeset/slick-worms-drum.md delete mode 100644 .changeset/social-beers-unite.md delete mode 100644 .changeset/stale-adults-hammer.md delete mode 100644 .changeset/stupid-areas-share.md delete mode 100644 .changeset/stupid-pans-flash.md delete mode 100644 .changeset/sweet-lemons-wonder.md delete mode 100644 .changeset/tangy-squids-film.md delete mode 100644 .changeset/tasty-yaks-kiss.md delete mode 100644 .changeset/ten-boxes-lie.md delete mode 100644 .changeset/thin-phones-press.md delete mode 100644 .changeset/tiny-spoons-mix.md delete mode 100644 .changeset/tired-cobras-fly.md delete mode 100644 .changeset/tough-cars-cry.md delete mode 100644 .changeset/tricky-buses-lead.md delete mode 100644 .changeset/twenty-shoes-tickle.md delete mode 100644 .changeset/two-coins-stop.md delete mode 100644 .changeset/warm-emus-itch.md delete mode 100644 .changeset/wet-crabs-send.md delete mode 100644 .changeset/wet-kiwis-strive.md delete mode 100644 .changeset/wet-onions-sneeze.md delete mode 100644 .changeset/whole-dingos-lay.md delete mode 100644 .changeset/yellow-dragons-float.md delete mode 100644 .changeset/young-doodles-enter.md create mode 100644 docs/releases/v1.43.0-changelog.md create mode 100644 plugins/auth-backend-module-openshift-provider/CHANGELOG.md diff --git a/.changeset/afraid-streets-pay.md b/.changeset/afraid-streets-pay.md deleted file mode 100644 index d8e0bde4ab..0000000000 --- a/.changeset/afraid-streets-pay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-defaults': patch ---- - -Deduplicate discovered features discovered with discoveryFeatureLoader diff --git a/.changeset/all-bats-shine.md b/.changeset/all-bats-shine.md deleted file mode 100644 index 68d86c8d15..0000000000 --- a/.changeset/all-bats-shine.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Updated the WebPack configuration to use `contenthash`. This fixes an issue were builds would sometimes generate output files with the same name but different content across builds, leading to breakages when loading the frontend app. diff --git a/.changeset/angry-heads-design.md b/.changeset/angry-heads-design.md deleted file mode 100644 index 0b4a866fb1..0000000000 --- a/.changeset/angry-heads-design.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Disallow import fallback of critical shared dependencies in module federation. diff --git a/.changeset/better-eagles-tickle.md b/.changeset/better-eagles-tickle.md deleted file mode 100644 index 418f0bcae0..0000000000 --- a/.changeset/better-eagles-tickle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/config-loader': patch ---- - -Allow using `BACKSTAGE_ENV` for loading environment specific config files diff --git a/.changeset/big-cameras-turn.md b/.changeset/big-cameras-turn.md deleted file mode 100644 index 6226b17806..0000000000 --- a/.changeset/big-cameras-turn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-defaults': patch ---- - -Fixed cache namespace and key prefix separator configuration to properly use configured values instead of hardcoded plugin ID. The cache manager now correctly combines the configured namespace with plugin IDs using the configured separator for Redis and Valkey. Memcache and memory store continue to use plugin ID as namespace. diff --git a/.changeset/brave-bugs-know.md b/.changeset/brave-bugs-know.md deleted file mode 100644 index 9c0ae9edf4..0000000000 --- a/.changeset/brave-bugs-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Creates a plugin that redirects from the Home page to the Catalog index page to avoid seeing a not found page error when starting the app. diff --git a/.changeset/brave-jars-speak.md b/.changeset/brave-jars-speak.md deleted file mode 100644 index 2060239326..0000000000 --- a/.changeset/brave-jars-speak.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-puppetdb': patch ---- - -**BREAKING ALPHA**: The module has been moved from the `/alpha` export to the root of the package. diff --git a/.changeset/brown-pens-shave.md b/.changeset/brown-pens-shave.md deleted file mode 100644 index d5810838ff..0000000000 --- a/.changeset/brown-pens-shave.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-github': patch ---- - -Add `auto_init` option to `github:repo:create` action to create repository with an initial commit containing a README.md file - -This initial commit is created by GitHub itself and the commit is signed, so the repository will not be empty after creation. - -```diff - - action: github:repo:create - id: init-new-repo - input: - repoUrl: 'github.com?repo=repo&owner=owner' - description: This is the description - visibility: private -+ autoInit: true - -``` diff --git a/.changeset/bumpy-eyes-divide.md b/.changeset/bumpy-eyes-divide.md deleted file mode 100644 index 6300f7aaf6..0000000000 --- a/.changeset/bumpy-eyes-divide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Added support for executing actions from the `ActionsRegistry` in the `scaffolder-backend` diff --git a/.changeset/busy-chairs-itch.md b/.changeset/busy-chairs-itch.md deleted file mode 100644 index ef8b0b8df4..0000000000 --- a/.changeset/busy-chairs-itch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Add missing class for flex: baseline diff --git a/.changeset/chilly-llamas-attend.md b/.changeset/chilly-llamas-attend.md deleted file mode 100644 index 0a0876ccac..0000000000 --- a/.changeset/chilly-llamas-attend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Updated dependency `dompurify` to `^3.2.4`. diff --git a/.changeset/clear-houses-wonder.md b/.changeset/clear-houses-wonder.md deleted file mode 100644 index 46ada62fbe..0000000000 --- a/.changeset/clear-houses-wonder.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -TechDocs page titles have been improved, especially for deeply nested pages. diff --git a/.changeset/cold-donuts-train.md b/.changeset/cold-donuts-train.md deleted file mode 100644 index fe84653601..0000000000 --- a/.changeset/cold-donuts-train.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Fix a potential race condition in EntityListProvider when selecting filters diff --git a/.changeset/cold-garlics-care.md b/.changeset/cold-garlics-care.md deleted file mode 100644 index 960ebe6bd4..0000000000 --- a/.changeset/cold-garlics-care.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-notifications': patch ---- - -Fixed missing app context when rendering the notifications view diff --git a/.changeset/cool-games-rescue.md b/.changeset/cool-games-rescue.md deleted file mode 100644 index 3bed707eb7..0000000000 --- a/.changeset/cool-games-rescue.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Fixed the `new-frontend-plugin` template that was incorrectly passing `id` instead of `pluginId` to `createFrontendPlugin` and unnecessarily importing `React`. diff --git a/.changeset/create-app-1757429965.md b/.changeset/create-app-1757429965.md deleted file mode 100644 index b50d431d4b..0000000000 --- a/.changeset/create-app-1757429965.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Bumped create-app version. diff --git a/.changeset/cuddly-lemons-cheat.md b/.changeset/cuddly-lemons-cheat.md deleted file mode 100644 index aa4ee786ff..0000000000 --- a/.changeset/cuddly-lemons-cheat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-gitlab': patch ---- - -When possible, requests a more limited set of results from the Gitlab projects API, which can reduce the amount of network traffic required to sync with Gitlab. diff --git a/.changeset/curvy-sites-rhyme.md b/.changeset/curvy-sites-rhyme.md deleted file mode 100644 index 1de9a21e77..0000000000 --- a/.changeset/curvy-sites-rhyme.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -The 'get-catalog-entity' action now throws a ConflictError instead of generic Error if multiple entities are found, so MCP call doesn't fail with 500. diff --git a/.changeset/curvy-terms-jam.md b/.changeset/curvy-terms-jam.md deleted file mode 100644 index d8070bc8ce..0000000000 --- a/.changeset/curvy-terms-jam.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-catalog-node': patch ---- - -Order catalog processors by priority. - -This change enables the ordering of catalog processors by their priority, -allowing for more control over the catalog processing sequence. -The default priority is set to 20, and processors can be assigned a custom -priority to influence their execution order. Lower number indicates higher priority. -The priority can be set by implementing the `getPriority` method in the processor class -or by adding a `catalog.processors..priority` configuration -in the `app-config.yaml` file. The configuration takes precedence over the method. diff --git a/.changeset/dark-teams-give.md b/.changeset/dark-teams-give.md deleted file mode 100644 index 692f015618..0000000000 --- a/.changeset/dark-teams-give.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Updating `catalog:get-catalog-entity` action to be `readOnly` and non destructive diff --git a/.changeset/dirty-spies-drop.md b/.changeset/dirty-spies-drop.md deleted file mode 100644 index 88bff91892..0000000000 --- a/.changeset/dirty-spies-drop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/app-defaults': minor ---- - -Add and configure the OpenShift authentication provider to the default APIs. diff --git a/.changeset/dull-eagles-sin.md b/.changeset/dull-eagles-sin.md deleted file mode 100644 index 8689ea3fc2..0000000000 --- a/.changeset/dull-eagles-sin.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-catalog-graph': minor ---- - -Support custom relations by using an API to define known relations and which to show by default -Fixes "simplified" bug (#30121) which caused graphs not to be simplified diff --git a/.changeset/easy-wings-turn.md b/.changeset/easy-wings-turn.md deleted file mode 100644 index 3ce4e76449..0000000000 --- a/.changeset/easy-wings-turn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-org': patch ---- - -Fixing issue with extra slash in the routing diff --git a/.changeset/eighty-numbers-act.md b/.changeset/eighty-numbers-act.md deleted file mode 100644 index dc7d504adc..0000000000 --- a/.changeset/eighty-numbers-act.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs-react': patch ---- - -Update to documentation regarding TechDocs redirects. diff --git a/.changeset/eleven-doors-down.md b/.changeset/eleven-doors-down.md deleted file mode 100644 index 0d380c8ddf..0000000000 --- a/.changeset/eleven-doors-down.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-mcp-actions-backend': patch ---- - -Proxy `/.well-known/oauth-authorization-server` to `/.well-known/openid-configuration` on `auth-backend` when `auth.experimentalDynamicClientRegistration.enabled` is enabled. diff --git a/.changeset/eleven-doors-own.md b/.changeset/eleven-doors-own.md deleted file mode 100644 index 1da0297e5c..0000000000 --- a/.changeset/eleven-doors-own.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Implementing Dynamic Client Registration with the OIDC server. You can enable this by setting `auth.experimentalDynamicClientRegistration.enabled` in `app-config.yaml`. This is highly experimental, but feedback welcome. diff --git a/.changeset/fast-corners-roll.md b/.changeset/fast-corners-roll.md deleted file mode 100644 index f883774aef..0000000000 --- a/.changeset/fast-corners-roll.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Modify the `backstage.json` also for custom patterns if it extends the default pattern. - -Examples: - -- `@backstage/*` (default pattern) -- `@{backstage,backstage-community}/*` -- `@{extra1,backstage,extra2}/*` diff --git a/.changeset/fine-hands-think.md b/.changeset/fine-hands-think.md deleted file mode 100644 index 0fa99c904c..0000000000 --- a/.changeset/fine-hands-think.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/frontend-plugin-api': patch ---- - -Improved the types of `createFrontendPlugin` and `createFrontendModule` so that errors due to incompatible options are indicated more clearly. diff --git a/.changeset/fix-knip-reports-spaces-path.md b/.changeset/fix-knip-reports-spaces-path.md deleted file mode 100644 index 95064c9274..0000000000 --- a/.changeset/fix-knip-reports-spaces-path.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/repo-tools': patch ---- - -Fixed knip-reports command failing when workspace path contains spaces and process termination issues by replacing `execFile` with `spawn` and removing `shell` option. diff --git a/.changeset/fix-select-aria-props.md b/.changeset/fix-select-aria-props.md deleted file mode 100644 index 66bb1de3ad..0000000000 --- a/.changeset/fix-select-aria-props.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Fix Select component to properly attach aria-label and aria-labelledby props to the rendered element for improved accessibility. diff --git a/.changeset/flat-colts-know.md b/.changeset/flat-colts-know.md deleted file mode 100644 index e79ba52ae8..0000000000 --- a/.changeset/flat-colts-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs-backend': minor ---- - -Techdocs: support HumanDuration for cache TTL and timeout configuration diff --git a/.changeset/flat-groups-remain.md b/.changeset/flat-groups-remain.md deleted file mode 100644 index 5e61c8925d..0000000000 --- a/.changeset/flat-groups-remain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-mcp-actions-backend': patch ---- - -Updating docs diff --git a/.changeset/funny-eagles-try.md b/.changeset/funny-eagles-try.md deleted file mode 100644 index c5550756e0..0000000000 --- a/.changeset/funny-eagles-try.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch -'@backstage/plugin-auth-react': patch -'@backstage/plugin-auth-node': patch ---- - -Updating plugin metadata diff --git a/.changeset/giant-buttons-flash.md b/.changeset/giant-buttons-flash.md deleted file mode 100644 index 97564d37ac..0000000000 --- a/.changeset/giant-buttons-flash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth': minor ---- - -Initial publish of the `auth` frontend package diff --git a/.changeset/giant-zebras-peel.md b/.changeset/giant-zebras-peel.md deleted file mode 100644 index c08caaddb7..0000000000 --- a/.changeset/giant-zebras-peel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-test-utils': minor ---- - -The `mockServices.rootConfig()` instance now has an `update` method that can be used to test configuration subscriptions and updates. diff --git a/.changeset/gold-words-smoke.md b/.changeset/gold-words-smoke.md deleted file mode 100644 index 85141e402c..0000000000 --- a/.changeset/gold-words-smoke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-app-backend': patch ---- - -Internal update to not expose the old `createRouter`. diff --git a/.changeset/heavy-cats-unite.md b/.changeset/heavy-cats-unite.md deleted file mode 100644 index 4875093b0a..0000000000 --- a/.changeset/heavy-cats-unite.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-home': patch ---- - -Fixed race condition in CustomHomepageGrid by waiting for storage to load before rendering custom layout to prevent -rendering of the default content. diff --git a/.changeset/heavy-lies-listen.md b/.changeset/heavy-lies-listen.md deleted file mode 100644 index 9f408b1ea8..0000000000 --- a/.changeset/heavy-lies-listen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes-backend': patch ---- - -Fix a bug where `getDefault` in the `kubernetesFetcherExtensionPoint` had the wrong `this` value diff --git a/.changeset/hot-friends-act.md b/.changeset/hot-friends-act.md deleted file mode 100644 index 37b2e3f112..0000000000 --- a/.changeset/hot-friends-act.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-plugin-api': minor ---- - -Make `openshiftAuthApiRef` available in `@backstage/core-plugin-api`. diff --git a/.changeset/hungry-carrots-grow.md b/.changeset/hungry-carrots-grow.md deleted file mode 100644 index 748ace8d94..0000000000 --- a/.changeset/hungry-carrots-grow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': minor ---- - -Adding redirect handling for TechDocs URLs that reference entities that now reference an external entity for TechDocs. Including tests and documentation. diff --git a/.changeset/icy-camels-throw.md b/.changeset/icy-camels-throw.md deleted file mode 100644 index db0521711c..0000000000 --- a/.changeset/icy-camels-throw.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-mcp-actions-backend': patch ---- - -The MCP backend will now convert known Backstage errors into textual responses with `isError: true`. -The error message can be useful for an LLM to understand and maybe give back to the user. -Previously all errors where thrown out to `@modelcontextprotocol/sdk` which causes a generic 500. diff --git a/.changeset/itchy-moons-start.md b/.changeset/itchy-moons-start.md deleted file mode 100644 index e5768b58b2..0000000000 --- a/.changeset/itchy-moons-start.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Render a TechDocs link on the Scaffolder Template List page when templates include either `backstage.io/techdocs-ref` or `backstage.io/techdocs-entity` annotations, using the shared `buildTechDocsURL` helper. Also adds tests to verify both annotations and optional `backstage.io/techdocs-entity-path` are respected. diff --git a/.changeset/kind-eyes-worry.md b/.changeset/kind-eyes-worry.md deleted file mode 100644 index 5568a00e28..0000000000 --- a/.changeset/kind-eyes-worry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-app': minor ---- - -Add implementation of OpenShift authentication provider. diff --git a/.changeset/large-otters-invent.md b/.changeset/large-otters-invent.md deleted file mode 100644 index 2efaf853a3..0000000000 --- a/.changeset/large-otters-invent.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/catalog-client': patch -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-events-backend': patch -'@backstage/plugin-search-backend': patch -'@backstage/plugin-events-node': patch ---- - -Updated generated schemas diff --git a/.changeset/late-swans-press.md b/.changeset/late-swans-press.md deleted file mode 100644 index 987672bd77..0000000000 --- a/.changeset/late-swans-press.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Prevent deadlock in catalog deferred stitching diff --git a/.changeset/legal-lemons-attend.md b/.changeset/legal-lemons-attend.md deleted file mode 100644 index 4b497d8620..0000000000 --- a/.changeset/legal-lemons-attend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/integration': minor ---- - -Added support for limiting GithubAppCredentialsMux to specific apps diff --git a/.changeset/lemon-jobs-create.md b/.changeset/lemon-jobs-create.md deleted file mode 100644 index d0e28b9c5c..0000000000 --- a/.changeset/lemon-jobs-create.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-user-settings': patch ---- - -Add the OpenShift authenticator provider to the default `user-settings` providers page. diff --git a/.changeset/lemon-terms-cheer.md b/.changeset/lemon-terms-cheer.md deleted file mode 100644 index c2f93d93fb..0000000000 --- a/.changeset/lemon-terms-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search-backend-module-explore': patch ---- - -Deprecate and mark explore collator as moved diff --git a/.changeset/loose-memes-bathe.md b/.changeset/loose-memes-bathe.md deleted file mode 100644 index 85eff94399..0000000000 --- a/.changeset/loose-memes-bathe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-msgraph': patch ---- - -Ensure that msgraph parent group stays same in case the group has multiple parents diff --git a/.changeset/loud-forks-teach.md b/.changeset/loud-forks-teach.md deleted file mode 100644 index 662ff458ca..0000000000 --- a/.changeset/loud-forks-teach.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/frontend-test-utils': patch ---- - -Internal update to use and throw app errors. diff --git a/.changeset/lovely-actors-love.md b/.changeset/lovely-actors-love.md deleted file mode 100644 index b428a11619..0000000000 --- a/.changeset/lovely-actors-love.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs-backend': minor ---- - -Adding new entity that specifies an external entity in the techdocs-entity annotation and updates to documentation regarding TechDocs redirects. diff --git a/.changeset/lovely-frogs-share.md b/.changeset/lovely-frogs-share.md deleted file mode 100644 index 5c630f301e..0000000000 --- a/.changeset/lovely-frogs-share.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-catalog': patch ---- - -Catalog table columns support i18n diff --git a/.changeset/lucky-glasses-slide.md b/.changeset/lucky-glasses-slide.md deleted file mode 100644 index 9b56901919..0000000000 --- a/.changeset/lucky-glasses-slide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Fixed an issue that could cause conflicts of detected modules in workspaces with multiple apps. diff --git a/.changeset/mean-sites-cheer.md b/.changeset/mean-sites-cheer.md deleted file mode 100644 index a35dfb8c33..0000000000 --- a/.changeset/mean-sites-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-gitlab': patch ---- - -The log message now indicates that the pipeline trigger token was deleted and not pipeline itself. diff --git a/.changeset/nine-socks-share.md b/.changeset/nine-socks-share.md deleted file mode 100644 index 82c96e554e..0000000000 --- a/.changeset/nine-socks-share.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': minor ---- - -Add optional `distance` property to `DependencyEdge` to reflect the distance to a root. diff --git a/.changeset/olive-moons-burn.md b/.changeset/olive-moons-burn.md deleted file mode 100644 index 625d97c5a7..0000000000 --- a/.changeset/olive-moons-burn.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@backstage/catalog-client': minor -'@backstage/plugin-catalog-react': minor -'@backstage/plugin-catalog-node': minor ---- - -Introduced new `streamEntities` async generator method for the catalog. - -Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. -This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them -all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination -or fetch all entities at once. - -Example usage: - -```ts -const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); -for await (const page of pageStream) { - // Handle page of entities - for (const entity of page) { - console.log(entity); - } -} -``` diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index 174818edf4..0000000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,256 +0,0 @@ -{ - "mode": "exit", - "tag": "next", - "initialVersions": { - "example-app": "0.2.112", - "@backstage/app-defaults": "1.6.5", - "example-app-next": "0.0.26", - "app-next-example-plugin": "0.0.26", - "example-backend": "0.0.41", - "@backstage/backend-app-api": "1.2.6", - "@backstage/backend-defaults": "0.12.0", - "@backstage/backend-dev-utils": "0.1.5", - "@backstage/backend-dynamic-feature-service": "0.7.3", - "@backstage/backend-openapi-utils": "0.6.0", - "@backstage/backend-plugin-api": "1.4.2", - "@backstage/backend-test-utils": "1.8.0", - "@backstage/catalog-client": "1.11.0", - "@backstage/catalog-model": "1.7.5", - "@backstage/cli": "0.34.0", - "@backstage/cli-common": "0.1.15", - "@backstage/cli-node": "0.2.14", - "@backstage/codemods": "0.1.52", - "@backstage/config": "1.3.3", - "@backstage/config-loader": "1.10.2", - "@backstage/core-app-api": "1.18.0", - "@backstage/core-compat-api": "0.5.0", - "@backstage/core-components": "0.17.5", - "@backstage/core-plugin-api": "1.10.9", - "@backstage/create-app": "0.7.2", - "@backstage/dev-utils": "1.1.13", - "e2e-test": "0.2.31", - "@backstage/e2e-test-utils": "0.1.1", - "@backstage/errors": "1.2.7", - "@backstage/eslint-plugin": "0.1.11", - "@backstage/frontend-app-api": "0.12.0", - "@backstage/frontend-defaults": "0.3.0", - "@backstage/frontend-dynamic-feature-loader": "0.1.4", - "@internal/frontend": "0.0.12", - "@backstage/frontend-plugin-api": "0.11.0", - "@backstage/frontend-test-utils": "0.3.5", - "@backstage/integration": "1.17.1", - "@backstage/integration-aws-node": "0.1.17", - "@backstage/integration-react": "1.2.9", - "@internal/opaque": "0.0.1", - "@backstage/release-manifests": "0.0.13", - "@backstage/repo-tools": "0.15.1", - "@internal/scaffolder": "0.0.12", - "@techdocs/cli": "1.9.6", - "techdocs-cli-embedded-app": "0.2.111", - "@backstage/test-utils": "1.7.11", - "@backstage/theme": "0.6.8", - "@backstage/types": "1.2.1", - "@backstage/ui": "0.7.0", - "@backstage/version-bridge": "1.0.11", - "yarn-plugin-backstage": "0.0.7", - "@backstage/plugin-api-docs": "0.12.10", - "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.10", - "@backstage/plugin-app": "0.2.0", - "@backstage/plugin-app-backend": "0.5.5", - "@backstage/plugin-app-node": "0.1.36", - "@backstage/plugin-app-visualizer": "0.1.22", - "@backstage/plugin-auth-backend": "0.25.3", - "@backstage/plugin-auth-backend-module-atlassian-provider": "0.4.6", - "@backstage/plugin-auth-backend-module-auth0-provider": "0.2.6", - "@backstage/plugin-auth-backend-module-aws-alb-provider": "0.4.6", - "@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.2.11", - "@backstage/plugin-auth-backend-module-bitbucket-provider": "0.3.6", - "@backstage/plugin-auth-backend-module-bitbucket-server-provider": "0.2.6", - "@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.4.6", - "@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.4.6", - "@backstage/plugin-auth-backend-module-github-provider": "0.3.6", - "@backstage/plugin-auth-backend-module-gitlab-provider": "0.3.6", - "@backstage/plugin-auth-backend-module-google-provider": "0.3.6", - "@backstage/plugin-auth-backend-module-guest-provider": "0.2.11", - "@backstage/plugin-auth-backend-module-microsoft-provider": "0.3.6", - "@backstage/plugin-auth-backend-module-oauth2-provider": "0.4.6", - "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.2.11", - "@backstage/plugin-auth-backend-module-oidc-provider": "0.4.6", - "@backstage/plugin-auth-backend-module-okta-provider": "0.2.6", - "@backstage/plugin-auth-backend-module-onelogin-provider": "0.3.6", - "@backstage/plugin-auth-backend-module-pinniped-provider": "0.3.6", - "@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.5.6", - "@backstage/plugin-auth-node": "0.6.6", - "@backstage/plugin-auth-react": "0.1.18", - "@backstage/plugin-bitbucket-cloud-common": "0.3.1", - "@backstage/plugin-catalog": "1.31.2", - "@backstage/plugin-catalog-backend": "3.0.1", - "@backstage/plugin-catalog-backend-module-aws": "0.4.14", - "@backstage/plugin-catalog-backend-module-azure": "0.3.8", - "@backstage/plugin-catalog-backend-module-backstage-openapi": "0.5.5", - "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.5.2", - "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.5.2", - "@backstage/plugin-catalog-backend-module-gcp": "0.3.11", - "@backstage/plugin-catalog-backend-module-gerrit": "0.3.5", - "@backstage/plugin-catalog-backend-module-gitea": "0.1.3", - "@backstage/plugin-catalog-backend-module-github": "0.10.2", - "@backstage/plugin-catalog-backend-module-github-org": "0.3.13", - "@backstage/plugin-catalog-backend-module-gitlab": "0.7.2", - "@backstage/plugin-catalog-backend-module-gitlab-org": "0.2.12", - "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.7.3", - "@backstage/plugin-catalog-backend-module-ldap": "0.11.8", - "@backstage/plugin-catalog-backend-module-logs": "0.1.13", - "@backstage/plugin-catalog-backend-module-msgraph": "0.7.3", - "@backstage/plugin-catalog-backend-module-openapi": "0.2.13", - "@backstage/plugin-catalog-backend-module-puppetdb": "0.2.13", - "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.2.11", - "@backstage/plugin-catalog-backend-module-unprocessed": "0.6.3", - "@backstage/plugin-catalog-common": "1.1.5", - "@backstage/plugin-catalog-graph": "0.4.22", - "@backstage/plugin-catalog-import": "0.13.4", - "@backstage/plugin-catalog-node": "1.18.0", - "@backstage/plugin-catalog-react": "1.20.0", - "@backstage/plugin-catalog-unprocessed-entities": "0.2.20", - "@backstage/plugin-catalog-unprocessed-entities-common": "0.0.9", - "@backstage/plugin-config-schema": "0.1.71", - "@backstage/plugin-devtools": "0.1.30", - "@backstage/plugin-devtools-backend": "0.5.8", - "@backstage/plugin-devtools-common": "0.1.17", - "@backstage/plugin-events-backend": "0.5.5", - "@backstage/plugin-events-backend-module-aws-sqs": "0.4.14", - "@backstage/plugin-events-backend-module-azure": "0.2.23", - "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.23", - "@backstage/plugin-events-backend-module-bitbucket-server": "0.1.4", - "@backstage/plugin-events-backend-module-gerrit": "0.2.23", - "@backstage/plugin-events-backend-module-github": "0.4.3", - "@backstage/plugin-events-backend-module-gitlab": "0.3.4", - "@backstage/plugin-events-backend-module-google-pubsub": "0.1.3", - "@backstage/plugin-events-backend-module-kafka": "0.1.2", - "@backstage/plugin-events-backend-test-utils": "0.1.47", - "@backstage/plugin-events-node": "0.4.14", - "@internal/plugin-todo-list": "1.0.42", - "@internal/plugin-todo-list-backend": "1.0.42", - "@internal/plugin-todo-list-common": "1.0.26", - "@backstage/plugin-gateway-backend": "1.0.4", - "@backstage/plugin-home": "0.8.11", - "@backstage/plugin-home-react": "0.1.29", - "@backstage/plugin-kubernetes": "0.12.10", - "@backstage/plugin-kubernetes-backend": "0.20.0", - "@backstage/plugin-kubernetes-cluster": "0.0.28", - "@backstage/plugin-kubernetes-common": "0.9.6", - "@backstage/plugin-kubernetes-node": "0.3.3", - "@backstage/plugin-kubernetes-react": "0.5.10", - "@backstage/plugin-mcp-actions-backend": "0.1.2", - "@backstage/plugin-notifications": "0.5.8", - "@backstage/plugin-notifications-backend": "0.5.9", - "@backstage/plugin-notifications-backend-module-email": "0.3.12", - "@backstage/plugin-notifications-backend-module-slack": "0.1.4", - "@backstage/plugin-notifications-common": "0.1.0", - "@backstage/plugin-notifications-node": "0.2.18", - "@backstage/plugin-org": "0.6.42", - "@backstage/plugin-org-react": "0.1.41", - "@backstage/plugin-permission-backend": "0.7.3", - "@backstage/plugin-permission-backend-module-allow-all-policy": "0.2.11", - "@backstage/plugin-permission-common": "0.9.1", - "@backstage/plugin-permission-node": "0.10.3", - "@backstage/plugin-permission-react": "0.4.36", - "@backstage/plugin-proxy-backend": "0.6.5", - "@backstage/plugin-proxy-node": "0.1.7", - "@backstage/plugin-scaffolder": "1.34.0", - "@backstage/plugin-scaffolder-backend": "2.2.0", - "@backstage/plugin-scaffolder-backend-module-azure": "0.2.12", - "@backstage/plugin-scaffolder-backend-module-bitbucket": "0.3.13", - "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.2.12", - "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.2.12", - "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.3.12", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.3.14", - "@backstage/plugin-scaffolder-backend-module-gcp": "0.2.12", - "@backstage/plugin-scaffolder-backend-module-gerrit": "0.2.12", - "@backstage/plugin-scaffolder-backend-module-gitea": "0.2.12", - "@backstage/plugin-scaffolder-backend-module-github": "0.8.2", - "@backstage/plugin-scaffolder-backend-module-gitlab": "0.9.4", - "@backstage/plugin-scaffolder-backend-module-notifications": "0.1.13", - "@backstage/plugin-scaffolder-backend-module-rails": "0.5.12", - "@backstage/plugin-scaffolder-backend-module-sentry": "0.2.12", - "@backstage/plugin-scaffolder-backend-module-yeoman": "0.4.13", - "@backstage/plugin-scaffolder-common": "1.7.0", - "@backstage/plugin-scaffolder-node": "0.11.0", - "@backstage/plugin-scaffolder-node-test-utils": "0.3.2", - "@backstage/plugin-scaffolder-react": "1.19.0", - "@backstage/plugin-search": "1.4.29", - "@backstage/plugin-search-backend": "2.0.5", - "@backstage/plugin-search-backend-module-catalog": "0.3.7", - "@backstage/plugin-search-backend-module-elasticsearch": "1.7.5", - "@backstage/plugin-search-backend-module-explore": "0.3.5", - "@backstage/plugin-search-backend-module-pg": "0.5.47", - "@backstage/plugin-search-backend-module-stack-overflow-collator": "0.3.12", - "@backstage/plugin-search-backend-module-techdocs": "0.4.5", - "@backstage/plugin-search-backend-node": "1.3.14", - "@backstage/plugin-search-common": "1.2.19", - "@backstage/plugin-search-react": "1.9.3", - "@backstage/plugin-signals": "0.0.22", - "@backstage/plugin-signals-backend": "0.3.7", - "@backstage/plugin-signals-node": "0.1.23", - "@backstage/plugin-signals-react": "0.0.15", - "@backstage/plugin-techdocs": "1.14.0", - "@backstage/plugin-techdocs-addons-test-utils": "1.0.52", - "@backstage/plugin-techdocs-backend": "2.0.5", - "@backstage/plugin-techdocs-common": "0.1.1", - "@backstage/plugin-techdocs-module-addons-contrib": "1.1.27", - "@backstage/plugin-techdocs-node": "1.13.6", - "@backstage/plugin-techdocs-react": "1.3.2", - "@backstage/plugin-user-settings": "0.8.25", - "@backstage/plugin-user-settings-backend": "0.3.5", - "@backstage/plugin-user-settings-common": "0.0.1", - "@backstage/plugin-auth": "0.0.0" - }, - "changesets": [ - "better-eagles-tickle", - "big-cameras-turn", - "brave-bugs-know", - "brave-jars-speak", - "busy-chairs-itch", - "cold-donuts-train", - "cool-games-rescue", - "create-app-1757429965", - "curvy-sites-rhyme", - "easy-wings-turn", - "eleven-doors-down", - "eleven-doors-own", - "fine-hands-think", - "fix-select-aria-props", - "flat-colts-know", - "funny-eagles-try", - "giant-buttons-flash", - "giant-zebras-peel", - "gold-words-smoke", - "heavy-cats-unite", - "heavy-lies-listen", - "icy-camels-throw", - "itchy-moons-start", - "late-swans-press", - "legal-lemons-attend", - "lemon-terms-cheer", - "lucky-glasses-slide", - "mean-sites-cheer", - "olive-moons-burn", - "puny-books-fetch", - "quiet-papayas-mate", - "red-shrimps-fall", - "ripe-plants-pump", - "sharp-carrots-spend", - "sixty-pans-prove", - "sixty-places-push", - "slick-lamps-clap", - "slick-worms-drum", - "social-beers-unite", - "sweet-lemons-wonder", - "tangy-squids-film", - "tricky-buses-lead", - "warm-emus-itch", - "wet-kiwis-strive", - "whole-dingos-lay", - "yellow-dragons-float", - "young-doodles-enter" - ] -} diff --git a/.changeset/puny-books-fetch.md b/.changeset/puny-books-fetch.md deleted file mode 100644 index 641285869f..0000000000 --- a/.changeset/puny-books-fetch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Updated the backend plugin template to use a new pattern for the `TodoListService` that reduces boilerplate. diff --git a/.changeset/puny-tires-fold.md b/.changeset/puny-tires-fold.md deleted file mode 100644 index e54187cc85..0000000000 --- a/.changeset/puny-tires-fold.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/frontend-app-api': minor ---- - -The `createSpecializedApp` no longer throws when encountering many common errors when starting up the app. It will instead return them through the `errors` property so that they can be handled more gracefully in the app. diff --git a/.changeset/quiet-papayas-mate.md b/.changeset/quiet-papayas-mate.md deleted file mode 100644 index 1d2089f4e8..0000000000 --- a/.changeset/quiet-papayas-mate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Dependency graph can now be opened in full screen mode diff --git a/.changeset/red-shrimps-fall.md b/.changeset/red-shrimps-fall.md deleted file mode 100644 index 1ee3aca7ed..0000000000 --- a/.changeset/red-shrimps-fall.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-user-settings': patch ---- - -Tool-tip text correction for the Theme selection in settings page diff --git a/.changeset/ripe-boxes-stand.md b/.changeset/ripe-boxes-stand.md deleted file mode 100644 index 9a1e72f052..0000000000 --- a/.changeset/ripe-boxes-stand.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-github': patch ---- - -Add block creations field in github branch protection scaffolder actions diff --git a/.changeset/ripe-plants-pump.md b/.changeset/ripe-plants-pump.md deleted file mode 100644 index 677c025105..0000000000 --- a/.changeset/ripe-plants-pump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes-backend': patch ---- - -Fix issue with default objects not being loaded properly diff --git a/.changeset/rotten-hairs-attack.md b/.changeset/rotten-hairs-attack.md deleted file mode 100644 index aa6dabc78c..0000000000 --- a/.changeset/rotten-hairs-attack.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': minor ---- - -Add support for specifying an entity `spec.type` in `catalog.rules` and `catalog.locations.rules` within the catalog configuration. - -For example, this enables allowing all `Template` entities with the type `website`: - -```diff - catalog: - rules: - - allow: - - Component - - API - - Resource - - System - - Domain - - Location -+ - allow: -+ - kind: Template -+ spec.type: website - locations: - - type: url - pattern: https://github.com/org/*\/blob/master/*.yaml -``` diff --git a/.changeset/rude-socks-serve.md b/.changeset/rude-socks-serve.md deleted file mode 100644 index de75682e8c..0000000000 --- a/.changeset/rude-socks-serve.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-notifications-backend-module-slack': patch -'@backstage/plugin-notifications-backend': patch -'@backstage/types': patch ---- - -Internal refactoring for better type support diff --git a/.changeset/sharp-carrots-spend.md b/.changeset/sharp-carrots-spend.md deleted file mode 100644 index fc846ce13f..0000000000 --- a/.changeset/sharp-carrots-spend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-github': minor ---- - -Added support for discovery by app diff --git a/.changeset/shy-chicken-smash.md b/.changeset/shy-chicken-smash.md deleted file mode 100644 index 7524a50580..0000000000 --- a/.changeset/shy-chicken-smash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/ui': patch ---- - -Removed the need to mock `window.matchMedia` in tests, falling back to default breakpoint values instead. diff --git a/.changeset/silent-results-stick.md b/.changeset/silent-results-stick.md deleted file mode 100644 index 6cb501027c..0000000000 --- a/.changeset/silent-results-stick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Fixed a regression that prevented uploads greater than 100KB. Uploads up to 10MB are supported again. diff --git a/.changeset/silly-horses-share.md b/.changeset/silly-horses-share.md deleted file mode 100644 index 969b337d6b..0000000000 --- a/.changeset/silly-horses-share.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/backend-defaults': patch ---- - -Add `backend.logger` config options to configure the `RootLoggerService`. - -Read more about the new configuration options in the -[Root Logger Service](https://backstage.io/docs/backend-system/core-services/root-logger/) -documentation. diff --git a/.changeset/silver-baboons-ring.md b/.changeset/silver-baboons-ring.md deleted file mode 100644 index 578914b35c..0000000000 --- a/.changeset/silver-baboons-ring.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/frontend-defaults': patch ---- - -The default app now leverages the new error reporting functionality from `@backstage/frontend-app-api`. If there are critical errors during startup, an error screen that shows a summary of all errors will now be shown, rather than leaving the screen blank. Other errors will be logged as warnings in the console. diff --git a/.changeset/sixty-pans-prove.md b/.changeset/sixty-pans-prove.md deleted file mode 100644 index 0aa2468281..0000000000 --- a/.changeset/sixty-pans-prove.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Reverts a change in CSS layout that shifted the content of the Techdocs too far to the left. diff --git a/.changeset/sixty-places-push.md b/.changeset/sixty-places-push.md deleted file mode 100644 index 523e466158..0000000000 --- a/.changeset/sixty-places-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-compat-api': patch ---- - -Fix for `compatWrapper` creating many wrapping `Providers` when they should not diff --git a/.changeset/slick-lamps-clap.md b/.changeset/slick-lamps-clap.md deleted file mode 100644 index a94b5b81f4..0000000000 --- a/.changeset/slick-lamps-clap.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-msgraph': minor ---- - -**BREAKING**: Encode query filters for requests made to msgraph. If you currently have manually encoded characters in a filter, this is a breaking change and must be updated to avoid requests being double encoded. - -```diff -user: -- filter: department in('MARKETING', 'RESEARCH %26 DEVELOPMENT') -+ filter: department in('MARKETING', 'RESEARCH & DEVELOPMENT') -``` diff --git a/.changeset/slick-worms-drum.md b/.changeset/slick-worms-drum.md deleted file mode 100644 index 4fb170655b..0000000000 --- a/.changeset/slick-worms-drum.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Fixes for rendering initials in the avatar component. diff --git a/.changeset/social-beers-unite.md b/.changeset/social-beers-unite.md deleted file mode 100644 index a8942e6452..0000000000 --- a/.changeset/social-beers-unite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Add missing dependency to `@backstage/cli`, `@backstage/core-plugin-api` and `@backstage/integration-react` diff --git a/.changeset/stale-adults-hammer.md b/.changeset/stale-adults-hammer.md deleted file mode 100644 index 568973b368..0000000000 --- a/.changeset/stale-adults-hammer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search-backend-module-elasticsearch': patch ---- - -Added support for batchKeyField in the Elasticsearch indexer to allow consistent document IDs during bulk uploads. diff --git a/.changeset/stupid-areas-share.md b/.changeset/stupid-areas-share.md deleted file mode 100644 index 2238f9d942..0000000000 --- a/.changeset/stupid-areas-share.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Support Techdocs redirect with dompurify 3.2.6+ diff --git a/.changeset/stupid-pans-flash.md b/.changeset/stupid-pans-flash.md deleted file mode 100644 index 3e8c2333dd..0000000000 --- a/.changeset/stupid-pans-flash.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-github': minor ---- - -Adding a new scaffolder action `github:issues:create` following the reference of `github:issues:label` with `dryRun` testing possibility - -It can be used like this - -``` - steps: - - id: create-simple-issue - name: Create Simple Issue - action: github:issues:create - input: - repoUrl: ${{ parameters.repoUrl }} - title: "[${{ parameters.projectName }}] Simple Bug Report" - body: | - ## Bug Description - This is a simple bug report created by the scaffolder template. - - ### Steps to Reproduce - 1. Run the application - 2. Navigate to the main page - 3. Click on the problematic button - - ### Expected Behavior - The button should work correctly. - - ### Actual Behavior - The button does not respond to clicks. - output: - links: - - title: Simple Issue - url: ${{ steps['create-simple-issue'].output.issueUrl }} -``` diff --git a/.changeset/sweet-lemons-wonder.md b/.changeset/sweet-lemons-wonder.md deleted file mode 100644 index 6e5d4e8ef3..0000000000 --- a/.changeset/sweet-lemons-wonder.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@techdocs/cli': patch ---- - -Fixed an issue where `@techdocs/cli serve` command did not pick up the latest changes to TechDocs. diff --git a/.changeset/tangy-squids-film.md b/.changeset/tangy-squids-film.md deleted file mode 100644 index acb5546031..0000000000 --- a/.changeset/tangy-squids-film.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-react': patch ---- - -Fix scaffolder task log stream not having a minimum height diff --git a/.changeset/tasty-yaks-kiss.md b/.changeset/tasty-yaks-kiss.md deleted file mode 100644 index 125b77a870..0000000000 --- a/.changeset/tasty-yaks-kiss.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-app-api': patch ---- - -feat: support no en languages diff --git a/.changeset/ten-boxes-lie.md b/.changeset/ten-boxes-lie.md deleted file mode 100644 index 751b01e7d9..0000000000 --- a/.changeset/ten-boxes-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend-module-openshift-provider': minor ---- - -Add new `auth-backend-module-openshift-provider`. This authentication provider enables Backstage to sign in with OpenShift. diff --git a/.changeset/thin-phones-press.md b/.changeset/thin-phones-press.md deleted file mode 100644 index 8b3f985772..0000000000 --- a/.changeset/thin-phones-press.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/integration': patch ---- - -Fixes issue with Github credentials provider which fails to match organization name if using allowedInstallationOwners diff --git a/.changeset/tiny-spoons-mix.md b/.changeset/tiny-spoons-mix.md deleted file mode 100644 index bb607aca3f..0000000000 --- a/.changeset/tiny-spoons-mix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs-addons-test-utils': minor ---- - -Adding catalogApiRef to test-utils to support catalog API usage by TechDocs reader page. diff --git a/.changeset/tired-cobras-fly.md b/.changeset/tired-cobras-fly.md deleted file mode 100644 index e2e19803f8..0000000000 --- a/.changeset/tired-cobras-fly.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/frontend-plugin-api': minor ---- - -Make `openshiftApiRef` available to the new frontend system. diff --git a/.changeset/tough-cars-cry.md b/.changeset/tough-cars-cry.md deleted file mode 100644 index 9235553da7..0000000000 --- a/.changeset/tough-cars-cry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Removed the deprecation warning when not passing an explicit type to `EntityCardBlueprint`. Omitting the type is now intended, allowing the layout to pick the default type instead, typically `content`. diff --git a/.changeset/tricky-buses-lead.md b/.changeset/tricky-buses-lead.md deleted file mode 100644 index f2bc8ec5dc..0000000000 --- a/.changeset/tricky-buses-lead.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Auto-focus the first menu item in `EntityContextMenu`, and do not render a divider if an empty array is passed to `UNSTABLE_extraContextMenuItems`. diff --git a/.changeset/twenty-shoes-tickle.md b/.changeset/twenty-shoes-tickle.md deleted file mode 100644 index da067d1ec9..0000000000 --- a/.changeset/twenty-shoes-tickle.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@backstage/plugin-notifications-backend': patch -'@backstage/plugin-notifications-node': patch ---- - -A new extension point was added that can be used to modify how the users receiving notifications -are resolved. The interface passed to the extension point should only return complete user entity references -based on the notification target references and the excluded entity references. Note that the inputs are lists -of entity references that can be any entity kind, not just user entities. - -Using this extension point will override the default behavior of resolving users with the -`DefaultNotificationRecipientResolver`. diff --git a/.changeset/two-coins-stop.md b/.changeset/two-coins-stop.md deleted file mode 100644 index 005ea81121..0000000000 --- a/.changeset/two-coins-stop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Prevent the MultiEntityPicker from removing options present in form state when new options are selected diff --git a/.changeset/warm-emus-itch.md b/.changeset/warm-emus-itch.md deleted file mode 100644 index a7bd51abdb..0000000000 --- a/.changeset/warm-emus-itch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Fix incorrect `defaultTarget` on `createComponentRouteRef`. diff --git a/.changeset/wet-crabs-send.md b/.changeset/wet-crabs-send.md deleted file mode 100644 index edac0562fd..0000000000 --- a/.changeset/wet-crabs-send.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Add support to disable catalog providers and processors via configuration diff --git a/.changeset/wet-kiwis-strive.md b/.changeset/wet-kiwis-strive.md deleted file mode 100644 index 70cc1a6367..0000000000 --- a/.changeset/wet-kiwis-strive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-node': patch ---- - -Changes OAuth cookies from domain-scoped to host-only by avoid setting the domain attribute in the default cookie configurer. diff --git a/.changeset/wet-onions-sneeze.md b/.changeset/wet-onions-sneeze.md deleted file mode 100644 index 0fd61d764a..0000000000 --- a/.changeset/wet-onions-sneeze.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-app-api': minor ---- - -Add `OpenShiftAuth` helper to create default OAuth flow for OpenShift. diff --git a/.changeset/whole-dingos-lay.md b/.changeset/whole-dingos-lay.md deleted file mode 100644 index edefa5c44e..0000000000 --- a/.changeset/whole-dingos-lay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Only run provider orphan cleanup if the engine is started in the first place diff --git a/.changeset/yellow-dragons-float.md b/.changeset/yellow-dragons-float.md deleted file mode 100644 index c1d8341e32..0000000000 --- a/.changeset/yellow-dragons-float.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Support `default*` for older packages as this package is in range for breaking `/alpha` changes diff --git a/.changeset/young-doodles-enter.md b/.changeset/young-doodles-enter.md deleted file mode 100644 index 63e1f33eda..0000000000 --- a/.changeset/young-doodles-enter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -update about card links style for pretty display with other language diff --git a/docs/releases/v1.43.0-changelog.md b/docs/releases/v1.43.0-changelog.md new file mode 100644 index 0000000000..6fa500bdd5 --- /dev/null +++ b/docs/releases/v1.43.0-changelog.md @@ -0,0 +1,2119 @@ +# Release v1.43.0 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.43.0](https://backstage.github.io/upgrade-helper/?to=1.43.0) + +## @backstage/app-defaults@1.7.0 + +### Minor Changes + +- 9956704: Add and configure the OpenShift authentication provider to the default APIs. + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-app-api@1.19.0 + +## @backstage/backend-test-utils@1.9.0 + +### Minor Changes + +- 4e2c237: The `mockServices.rootConfig()` instance now has an `update` method that can be used to test configuration subscriptions and updates. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-app-api@1.2.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/catalog-client@1.12.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- 0efcc97: Updated generated schemas + +## @backstage/core-app-api@1.19.0 + +### Minor Changes + +- 3fca906: Add `OpenShiftAuth` helper to create default OAuth flow for OpenShift. + +### Patch Changes + +- 5ae6d9d: feat: support no en languages +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/types@1.2.2 + +## @backstage/core-components@0.18.0 + +### Minor Changes + +- b9a87f4: Add optional `distance` property to `DependencyEdge` to reflect the distance to a root. + +### Patch Changes + +- 1ad3d94: Dependency graph can now be opened in full screen mode +- e409bec: Fixes for rendering initials in the avatar component. +- ae7d426: update about card links style for pretty display with other language +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + +## @backstage/core-plugin-api@1.11.0 + +### Minor Changes + +- 5114627: Make `openshiftAuthApiRef` available in `@backstage/core-plugin-api`. + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + +## @backstage/frontend-app-api@0.13.0 + +### Minor Changes + +- 6516c3d: The `createSpecializedApp` no longer throws when encountering many common errors when starting up the app. It will instead return them through the `errors` property so that they can be handled more gracefully in the app. + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/types@1.2.2 + - @backstage/frontend-defaults@0.3.1 + - @backstage/core-app-api@1.19.0 + +## @backstage/frontend-plugin-api@0.12.0 + +### Minor Changes + +- 894d514: Make `openshiftApiRef` available to the new frontend system. + +### Patch Changes + +- 2fb8b04: Improved the types of `createFrontendPlugin` and `createFrontendModule` so that errors due to incompatible options are indicated more clearly. +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + +## @backstage/integration@1.18.0 + +### Minor Changes + +- 03bdc68: Added support for limiting GithubAppCredentialsMux to specific apps + +### Patch Changes + +- 56897d7: Fixes issue with Github credentials provider which fails to match organization name if using allowedInstallationOwners + +## @backstage/plugin-app@0.3.0 + +### Minor Changes + +- 99790db: Add implementation of OpenShift authentication provider. + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/integration-react@1.2.10 + +## @backstage/plugin-auth@0.1.0 + +### Minor Changes + +- 54ddfef: Initial publish of the `auth` frontend package + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-components@0.18.0 + +## @backstage/plugin-auth-backend-module-openshift-provider@0.1.0 + +### Minor Changes + +- 5a84253: Add new `auth-backend-module-openshift-provider`. This authentication provider enables Backstage to sign in with OpenShift. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend@3.1.0 + +### Minor Changes + +- 9b40a55: Add support for specifying an entity `spec.type` in `catalog.rules` and `catalog.locations.rules` within the catalog configuration. + + For example, this enables allowing all `Template` entities with the type `website`: + + ```diff + catalog: + rules: + - allow: + - Component + - API + - Resource + - System + - Domain + - Location + + - allow: + + - kind: Template + + spec.type: website + locations: + - type: url + pattern: https://github.com/org/*\/blob/master/*.yaml + ``` + +### Patch Changes + +- 37b4eaf: The 'get-catalog-entity' action now throws a ConflictError instead of generic Error if multiple entities are found, so MCP call doesn't fail with 500. + +- 2bbd24f: Order catalog processors by priority. + + This change enables the ordering of catalog processors by their priority, + allowing for more control over the catalog processing sequence. + The default priority is set to 20, and processors can be assigned a custom + priority to influence their execution order. Lower number indicates higher priority. + The priority can be set by implementing the `getPriority` method in the processor class + or by adding a `catalog.processors..priority` configuration + in the `app-config.yaml` file. The configuration takes precedence over the method. + +- e934a27: Updating `catalog:get-catalog-entity` action to be `readOnly` and non destructive + +- 0efcc97: Updated generated schemas + +- 2204f5b: Prevent deadlock in catalog deferred stitching + +- 58874c4: Add support to disable catalog providers and processors via configuration + +- a4c82ad: Only run provider orphan cleanup if the engine is started in the first place + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + +## @backstage/plugin-catalog-backend-module-github@0.11.0 + +### Minor Changes + +- 03bdc68: Added support for discovery by app + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-msgraph@0.8.0 + +### Minor Changes + +- 577f0ed: **BREAKING**: Encode query filters for requests made to msgraph. If you currently have manually encoded characters in a filter, this is a breaking change and must be updated to avoid requests being double encoded. + + ```diff + user: + - filter: department in('MARKETING', 'RESEARCH %26 DEVELOPMENT') + + filter: department in('MARKETING', 'RESEARCH & DEVELOPMENT') + ``` + +### Patch Changes + +- 7597781: Ensure that msgraph parent group stays same in case the group has multiple parents +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-graph@0.5.0 + +### Minor Changes + +- ae6b606: Support custom relations by using an API to define known relations and which to show by default + Fixes "simplified" bug (#30121) which caused graphs not to be simplified + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + +## @backstage/plugin-catalog-node@1.19.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- 2bbd24f: Order catalog processors by priority. + + This change enables the ordering of catalog processors by their priority, + allowing for more control over the catalog processing sequence. + The default priority is set to 20, and processors can be assigned a custom + priority to influence their execution order. Lower number indicates higher priority. + The priority can be set by implementing the `getPriority` method in the processor class + or by adding a `catalog.processors..priority` configuration + in the `app-config.yaml` file. The configuration takes precedence over the method. + +- Updated dependencies + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + +## @backstage/plugin-catalog-react@1.21.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- 0174799: Fix a potential race condition in EntityListProvider when selecting filters +- 4316c11: Catalog table columns support i18n +- 79ff318: Removed the deprecation warning when not passing an explicit type to `EntityCardBlueprint`. Omitting the type is now intended, allowing the layout to pick the default type instead, typically `content`. +- ad0f58d: Support `default*` for older packages as this package is in range for breaking `/alpha` changes +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/frontend-test-utils@0.3.6 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/integration-react@1.2.10 + +## @backstage/plugin-scaffolder-backend-module-github@0.9.0 + +### Minor Changes + +- f0f06b4: Adding a new scaffolder action `github:issues:create` following the reference of `github:issues:label` with `dryRun` testing possibility + + It can be used like this + + steps: + - id: create-simple-issue + name: Create Simple Issue + action: github:issues:create + input: + repoUrl: ${{ parameters.repoUrl }} + title: "[${{ parameters.projectName }}] Simple Bug Report" + body: | + ## Bug Description + This is a simple bug report created by the scaffolder template. + + ### Steps to Reproduce + 1. Run the application + 2. Navigate to the main page + 3. Click on the problematic button + + ### Expected Behavior + The button should work correctly. + + ### Actual Behavior + The button does not respond to clicks. + output: + links: + - title: Simple Issue + url: ${{ steps['create-simple-issue'].output.issueUrl }} + +### Patch Changes + +- aee107b: Add `auto_init` option to `github:repo:create` action to create repository with an initial commit containing a README.md file + + This initial commit is created by GitHub itself and the commit is signed, so the repository will not be empty after creation. + + ```diff + - action: github:repo:create + id: init-new-repo + input: + repoUrl: 'github.com?repo=repo&owner=owner' + description: This is the description + visibility: private + + autoInit: true + + ``` + +- 6393b78: Add block creations field in github branch protection scaffolder actions + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-techdocs@1.15.0 + +### Minor Changes + +- a0b604c: Adding redirect handling for TechDocs URLs that reference entities that now reference an external entity for TechDocs. Including tests and documentation. + +### Patch Changes + +- 313cec7: Updated dependency `dompurify` to `^3.2.4`. +- 8d18d23: TechDocs page titles have been improved, especially for deeply nested pages. +- 1dfee19: Reverts a change in CSS layout that shifted the content of the Techdocs too far to the left. +- 4ce5831: Support Techdocs redirect with dompurify 3.2.6+ +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-auth-react@0.1.19 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/integration@1.18.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-search-react@1.9.4 + - @backstage/integration-react@1.2.10 + +## @backstage/plugin-techdocs-addons-test-utils@1.1.0 + +### Minor Changes + +- 72543e9: Adding catalogApiRef to test-utils to support catalog API usage by TechDocs reader page. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.15.0 + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-app-api@1.19.0 + - @backstage/plugin-search-react@1.9.4 + - @backstage/integration-react@1.2.10 + +## @backstage/plugin-techdocs-backend@2.1.0 + +### Minor Changes + +- 063cdc5: Techdocs: support HumanDuration for cache TTL and timeout configuration +- a0b604c: Adding new entity that specifies an external entity in the techdocs-entity annotation and updates to documentation regarding TechDocs redirects. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-module-techdocs@0.4.6 + - @backstage/plugin-techdocs-node@1.13.7 + +## @backstage/backend-app-api@1.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/backend-defaults@0.12.1 + +### Patch Changes + +- 33bd4d0: Deduplicate discovered features discovered with discoveryFeatureLoader + +- 4eda590: Fixed cache namespace and key prefix separator configuration to properly use configured values instead of hardcoded plugin ID. The cache manager now correctly combines the configured namespace with plugin IDs using the configured separator for Redis and Valkey. Memcache and memory store continue to use plugin ID as namespace. + +- f244e61: Add `backend.logger` config options to configure the `RootLoggerService`. + + Read more about the new configuration options in the + [Root Logger Service](https://backstage.io/docs/backend-system/core-services/root-logger/) + documentation. + +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-app-api@1.2.7 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + +## @backstage/backend-dynamic-feature-service@0.7.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/config-loader@1.10.3 + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-backend@0.5.6 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-app-node@0.1.37 + - @backstage/plugin-permission-node@0.10.4 + - @backstage/plugin-scaffolder-node@0.11.1 + - @backstage/plugin-search-backend-node@1.3.15 + +## @backstage/backend-openapi-utils@0.6.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/backend-plugin-api@1.4.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-node@0.10.4 + +## @backstage/cli@0.34.2 + +### Patch Changes + +- e6f45dc: Updated the WebPack configuration to use `contenthash`. This fixes an issue were builds would sometimes generate output files with the same name but different content across builds, leading to breakages when loading the frontend app. + +- fffd434: Disallow import fallback of critical shared dependencies in module federation. + +- 080f252: Fixed the `new-frontend-plugin` template that was incorrectly passing `id` instead of `pluginId` to `createFrontendPlugin` and unnecessarily importing `React`. + +- e0db9b8: Modify the `backstage.json` also for custom patterns if it extends the default pattern. + + Examples: + + - `@backstage/*` (default pattern) + - `@{backstage,backstage-community}/*` + - `@{extra1,backstage,extra2}/*` + +- 275bda8: Fixed an issue that could cause conflicts of detected modules in workspaces with multiple apps. + +- e1adce4: Updated the backend plugin template to use a new pattern for the `TodoListService` that reduces boilerplate. + +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + +## @backstage/config-loader@1.10.3 + +### Patch Changes + +- a73f495: Allow using `BACKSTAGE_ENV` for loading environment specific config files +- Updated dependencies + - @backstage/types@1.2.2 + +## @backstage/core-compat-api@0.5.2 + +### Patch Changes + +- dc01d6f: Fix for `compatWrapper` creating many wrapping `Providers` when they should not +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + +## @backstage/create-app@0.7.4 + +### Patch Changes + +- b2d9fc1: Creates a plugin that redirects from the Home page to the Catalog index page to avoid seeing a not found page error when starting the app. +- 020d484: Bumped create-app version. +- 02dbe8e: Add missing dependency to `@backstage/cli`, `@backstage/core-plugin-api` and `@backstage/integration-react` + +## @backstage/dev-utils@1.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/app-defaults@1.7.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-app-api@1.19.0 + - @backstage/integration-react@1.2.10 + +## @backstage/frontend-defaults@0.3.1 + +### Patch Changes + +- 6516c3d: The default app now leverages the new error reporting functionality from `@backstage/frontend-app-api`. If there are critical errors during startup, an error screen that shows a summary of all errors will now be shown, rather than leaving the screen blank. Other errors will be logged as warnings in the console. +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-app@0.3.0 + - @backstage/core-components@0.18.0 + - @backstage/frontend-app-api@0.13.0 + +## @backstage/frontend-dynamic-feature-loader@0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + +## @backstage/frontend-test-utils@0.3.6 + +### Patch Changes + +- 6516c3d: Internal update to use and throw app errors. +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-app@0.3.0 + - @backstage/frontend-app-api@0.13.0 + - @backstage/types@1.2.2 + +## @backstage/integration-react@1.2.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/integration@1.18.0 + +## @backstage/repo-tools@0.15.2 + +### Patch Changes + +- 133ac7a: Fixed knip-reports command failing when workspace path contains spaces and process termination issues by replacing `execFile` with `spawn` and removing `shell` option. +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/backend-plugin-api@1.4.3 + +## @techdocs/cli@1.9.8 + +### Patch Changes + +- db63208: Fixed an issue where `@techdocs/cli serve` command did not pick up the latest changes to TechDocs. +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-techdocs-node@1.13.7 + +## @backstage/types@1.2.2 + +### Patch Changes + +- a95cebd: Internal refactoring for better type support + +## @backstage/ui@0.7.1 + +### Patch Changes + +- 7307930: Add missing class for flex: baseline +- 89da341: Fix Select component to properly attach aria-label and aria-labelledby props to the rendered element for improved accessibility. +- 0ffa4c7: Removed the need to mock `window.matchMedia` in tests, falling back to default breakpoint values instead. + +## @backstage/plugin-api-docs@0.12.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + +## @backstage/plugin-app-backend@0.5.6 + +### Patch Changes + +- afd368e: Internal update to not expose the old `createRouter`. +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-app-node@0.1.37 + +## @backstage/plugin-app-node@0.1.37 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-app-visualizer@0.1.23 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + +## @backstage/plugin-auth-backend@0.25.4 + +### Patch Changes + +- 1d47bf3: Implementing Dynamic Client Registration with the OIDC server. You can enable this by setting `auth.experimentalDynamicClientRegistration.enabled` in `app-config.yaml`. This is highly experimental, but feedback welcome. +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-auth0-provider@0.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.25.4 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-bitbucket-server-provider@0.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-github-provider@0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-google-provider@0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-guest-provider@0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.25.4 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-okta-provider@0.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.5.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-node@0.6.7 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- 3aff9e1: Changes OAuth cookies from domain-scoped to host-only by avoid setting the domain attribute in the default cookie configurer. +- Updated dependencies + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-auth-react@0.1.19 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + +## @backstage/plugin-bitbucket-cloud-common@0.3.2 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + +## @backstage/plugin-catalog@1.31.3 + +### Patch Changes + +- 4316c11: Catalog table columns support i18n +- ce1239e: Auto-focus the first menu item in `EntityContextMenu`, and do not render a divider if an empty array is passed to `UNSTABLE_extraContextMenuItems`. +- 85c5e04: Fix incorrect `defaultTarget` on `createComponentRouteRef`. +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-search-react@1.9.4 + - @backstage/integration-react@1.2.10 + - @backstage/plugin-scaffolder-common@1.7.1 + +## @backstage/plugin-catalog-backend-module-aws@0.4.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-azure@0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-bitbucket-cloud-common@0.3.2 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-gcp@0.3.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-gitea@0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-github-org@0.3.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/plugin-catalog-backend-module-github@0.11.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-gitlab@0.7.3 + +### Patch Changes + +- ea80e76: When possible, requests a more limited set of results from the Gitlab projects API, which can reduce the amount of network traffic required to sync with Gitlab. +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-gitlab@0.7.3 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.7.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-ldap@0.11.9 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-logs@0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-openapi@0.2.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.14 + +### Patch Changes + +- afd368e: **BREAKING ALPHA**: The module has been moved from the `/alpha` export to the root of the package. +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-common@1.7.1 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.6.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-catalog-import@0.13.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/integration@1.18.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + - @backstage/integration-react@1.2.10 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.21 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + +## @backstage/plugin-config-schema@0.1.72 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-devtools@0.1.31 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + +## @backstage/plugin-devtools-backend@0.5.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/config-loader@1.10.3 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + +## @backstage/plugin-events-backend@0.5.6 + +### Patch Changes + +- 0efcc97: Updated generated schemas +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-aws-sqs@0.4.15 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-azure@0.2.24 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.24 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-bitbucket-server@0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-gerrit@0.2.24 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-github@0.4.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-gitlab@0.3.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-google-pubsub@0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-module-kafka@0.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-events-backend-test-utils@0.1.48 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + +## @backstage/plugin-events-node@0.4.15 + +### Patch Changes + +- 0efcc97: Updated generated schemas +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-gateway-backend@1.0.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-home@0.8.12 + +### Patch Changes + +- 929c55a: Fixed race condition in CustomHomepageGrid by waiting for storage to load before rendering custom layout to prevent + rendering of the default content. +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + - @backstage/core-app-api@1.19.0 + - @backstage/plugin-home-react@0.1.30 + +## @backstage/plugin-home-react@0.1.30 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + +## @backstage/plugin-kubernetes@0.12.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-kubernetes-react@0.5.11 + +## @backstage/plugin-kubernetes-backend@0.20.2 + +### Patch Changes + +- dd7b6d2: Fix a bug where `getDefault` in the `kubernetesFetcherExtensionPoint` had the wrong `this` value +- 80cf8c9: Fix issue with default objects not being loaded properly +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-kubernetes-node@0.3.4 + - @backstage/plugin-permission-node@0.10.4 + +## @backstage/plugin-kubernetes-cluster@0.0.29 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/plugin-kubernetes-react@0.5.11 + +## @backstage/plugin-kubernetes-node@0.3.4 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-kubernetes-react@0.5.11 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-mcp-actions-backend@0.1.3 + +### Patch Changes + +- 1d47bf3: Proxy `/.well-known/oauth-authorization-server` to `/.well-known/openid-configuration` on `auth-backend` when `auth.experimentalDynamicClientRegistration.enabled` is enabled. +- 7f2a4a0: Updating docs +- d08b0c9: The MCP backend will now convert known Backstage errors into textual responses with `isError: true`. + The error message can be useful for an LLM to understand and maybe give back to the user. + Previously all errors where thrown out to `@modelcontextprotocol/sdk` which causes a generic 500. +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-notifications@0.5.9 + +### Patch Changes + +- 4815b12: Fixed missing app context when rendering the notifications view +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + +## @backstage/plugin-notifications-backend@0.5.10 + +### Patch Changes + +- a95cebd: Internal refactoring for better type support + +- 7e7ed57: A new extension point was added that can be used to modify how the users receiving notifications + are resolved. The interface passed to the extension point should only return complete user entity references + based on the notification target references and the excluded entity references. Note that the inputs are lists + of entity references that can be any entity kind, not just user entities. + + Using this extension point will override the default behavior of resolving users with the + `DefaultNotificationRecipientResolver`. + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-node@0.2.19 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-signals-node@0.1.24 + +## @backstage/plugin-notifications-backend-module-email@0.3.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-node@0.2.19 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-notifications-backend-module-slack@0.1.5 + +### Patch Changes + +- a95cebd: Internal refactoring for better type support +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-node@0.2.19 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-notifications-node@0.2.19 + +### Patch Changes + +- 7e7ed57: A new extension point was added that can be used to modify how the users receiving notifications + are resolved. The interface passed to the extension point should only return complete user entity references + based on the notification target references and the excluded entity references. Note that the inputs are lists + of entity references that can be any entity kind, not just user entities. + + Using this extension point will override the default behavior of resolving users with the + `DefaultNotificationRecipientResolver`. + +- Updated dependencies + - @backstage/catalog-client@1.12.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-signals-node@0.1.24 + +## @backstage/plugin-org@0.6.44 + +### Patch Changes + +- 22b69f2: Fixing issue with extra slash in the routing +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + +## @backstage/plugin-org-react@0.1.42 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + +## @backstage/plugin-permission-backend@0.7.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + +## @backstage/plugin-permission-node@0.10.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-proxy-backend@0.6.6 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-proxy-node@0.1.8 + +## @backstage/plugin-proxy-node@0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-scaffolder@1.34.1 + +### Patch Changes + +- 0d415ae: Render a TechDocs link on the Scaffolder Template List page when templates include either `backstage.io/techdocs-ref` or `backstage.io/techdocs-entity` annotations, using the shared `buildTechDocsURL` helper. Also adds tests to verify both annotations and optional `backstage.io/techdocs-entity-path` are respected. +- 7151260: Prevent the MultiEntityPicker from removing options present in form state when new options are selected +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/integration@1.18.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-scaffolder-react@1.19.1 + - @backstage/integration-react@1.2.10 + - @backstage/plugin-scaffolder-common@1.7.1 + +## @backstage/plugin-scaffolder-backend@2.2.1 + +### Patch Changes + +- a57185f: Added support for executing actions from the `ActionsRegistry` in the `scaffolder-backend` +- c3405db: Fixed a regression that prevented uploads greater than 100KB. Uploads up to 10MB are supported again. +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-scaffolder-backend-module-github@0.9.0 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.9.5 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-bitbucket-cloud-common@0.3.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12 + - @backstage/plugin-permission-node@0.10.4 + - @backstage/plugin-scaffolder-backend-module-azure@0.2.13 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.14 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.13 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.13 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.2.13 + - @backstage/plugin-scaffolder-backend-module-gitea@0.2.13 + - @backstage/plugin-scaffolder-common@1.7.1 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-azure@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.14 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.13 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.13 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-bitbucket-cloud-common@0.3.2 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.3.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.9.5 + +### Patch Changes + +- a84ddea: The log message now indicates that the pipeline trigger token was deleted and not pipeline itself. +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-node@0.2.19 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-rails@0.5.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.4.14 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + - @backstage/plugin-scaffolder-node-test-utils@0.3.3 + +## @backstage/plugin-scaffolder-common@1.7.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-scaffolder-node@0.11.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-common@1.7.1 + +## @backstage/plugin-scaffolder-node-test-utils@0.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@1.9.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + +## @backstage/plugin-scaffolder-react@1.19.1 + +### Patch Changes + +- 58fc108: Fix scaffolder task log stream not having a minimum height +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-common@1.7.1 + +## @backstage/plugin-search@1.4.30 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-search-react@1.9.4 + +## @backstage/plugin-search-backend@2.0.6 + +### Patch Changes + +- 0efcc97: Updated generated schemas +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + - @backstage/plugin-search-backend-node@1.3.15 + +## @backstage/plugin-search-backend-module-catalog@0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + +## @backstage/plugin-search-backend-module-elasticsearch@1.7.6 + +### Patch Changes + +- cde70ca: Added support for batchKeyField in the Elasticsearch indexer to allow consistent document IDs during bulk uploads. +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + +## @backstage/plugin-search-backend-module-explore@0.3.7 + +### Patch Changes + +- 9a93520: Deprecate and mark explore collator as moved +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + +## @backstage/plugin-search-backend-module-pg@0.5.48 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.3.13 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + +## @backstage/plugin-search-backend-module-techdocs@0.4.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + - @backstage/plugin-techdocs-node@1.13.7 + +## @backstage/plugin-search-backend-node@1.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-search-react@1.9.4 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + +## @backstage/plugin-signals@0.0.23 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + +## @backstage/plugin-signals-backend@0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-signals-node@0.1.24 + +## @backstage/plugin-signals-node@0.1.24 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/integration@1.18.0 + - @backstage/core-components@0.18.0 + - @backstage/integration-react@1.2.10 + +## @backstage/plugin-techdocs-node@1.13.7 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + +## @backstage/plugin-techdocs-react@1.3.3 + +### Patch Changes + +- a0b604c: Update to documentation regarding TechDocs redirects. +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + +## @backstage/plugin-user-settings@0.8.26 + +### Patch Changes + +- 320a9ac: Add the OpenShift authenticator provider to the default `user-settings` providers page. +- b713b54: Tool-tip text correction for the Theme selection in settings page +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/core-app-api@1.19.0 + +## @backstage/plugin-user-settings-backend@0.3.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-signals-node@0.1.24 + +## example-app@0.2.113 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.34.2 + - @backstage/ui@0.7.1 + - @backstage/plugin-techdocs@1.15.0 + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-notifications@0.5.9 + - @backstage/app-defaults@1.7.0 + - @backstage/plugin-catalog-graph@0.5.0 + - @backstage/plugin-org@0.6.44 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/plugin-auth-react@0.1.19 + - @backstage/plugin-home@0.8.12 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-scaffolder@1.34.1 + - @backstage/plugin-user-settings@0.8.26 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-components@0.18.0 + - @backstage/frontend-app-api@0.13.0 + - @backstage/plugin-scaffolder-react@1.19.1 + - @backstage/core-app-api@1.19.0 + - @backstage/plugin-api-docs@0.12.11 + - @backstage/plugin-catalog-import@0.13.5 + - @backstage/plugin-catalog-unprocessed-entities@0.2.21 + - @backstage/plugin-devtools@0.1.31 + - @backstage/plugin-kubernetes@0.12.11 + - @backstage/plugin-search@1.4.30 + - @backstage/plugin-search-react@1.9.4 + - @backstage/plugin-signals@0.0.23 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28 + - @backstage/integration-react@1.2.10 + - @backstage/plugin-kubernetes-cluster@0.0.29 + +## example-app-next@0.0.27 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.34.2 + - @backstage/ui@0.7.1 + - @backstage/plugin-techdocs@1.15.0 + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-notifications@0.5.9 + - @backstage/app-defaults@1.7.0 + - @backstage/plugin-catalog-graph@0.5.0 + - @backstage/plugin-org@0.6.44 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-auth-react@0.1.19 + - @backstage/plugin-auth@0.1.0 + - @backstage/plugin-home@0.8.12 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-scaffolder@1.34.1 + - @backstage/plugin-app@0.3.0 + - @backstage/plugin-user-settings@0.8.26 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-components@0.18.0 + - @backstage/frontend-app-api@0.13.0 + - @backstage/frontend-defaults@0.3.1 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-scaffolder-react@1.19.1 + - @backstage/core-app-api@1.19.0 + - @backstage/plugin-api-docs@0.12.11 + - @backstage/plugin-app-visualizer@0.1.23 + - @backstage/plugin-catalog-import@0.13.5 + - @backstage/plugin-catalog-unprocessed-entities@0.2.21 + - @backstage/plugin-kubernetes@0.12.11 + - @backstage/plugin-search@1.4.30 + - @backstage/plugin-search-react@1.9.4 + - @backstage/plugin-signals@0.0.23 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28 + - @backstage/integration-react@1.2.10 + - @backstage/plugin-kubernetes-cluster@0.0.29 + +## app-next-example-plugin@0.0.27 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-components@0.18.0 + +## example-backend@0.0.42 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-scaffolder-backend-module-github@0.9.0 + - @backstage/plugin-scaffolder-backend@2.2.1 + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-mcp-actions-backend@0.1.3 + - @backstage/plugin-auth-backend@0.25.4 + - @backstage/plugin-techdocs-backend@2.1.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-app-backend@0.5.6 + - @backstage/plugin-kubernetes-backend@0.20.2 + - @backstage/plugin-events-backend@0.5.6 + - @backstage/plugin-search-backend@2.0.6 + - @backstage/plugin-search-backend-module-explore@0.3.7 + - @backstage/plugin-notifications-backend@0.5.10 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-auth-backend-module-github-provider@0.3.7 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.12 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.6 + - @backstage/plugin-catalog-backend-module-openapi@0.2.14 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.4 + - @backstage/plugin-devtools-backend@0.5.9 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.4 + - @backstage/plugin-permission-backend@0.7.4 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.12 + - @backstage/plugin-permission-node@0.10.4 + - @backstage/plugin-proxy-backend@0.6.6 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.14 + - @backstage/plugin-search-backend-module-catalog@0.3.8 + - @backstage/plugin-search-backend-module-techdocs@0.4.6 + - @backstage/plugin-search-backend-node@1.3.15 + - @backstage/plugin-signals-backend@0.3.8 + +## e2e-test@0.2.32 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.7.4 + +## @internal/frontend@0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/types@1.2.2 + +## @internal/scaffolder@0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-scaffolder-react@1.19.1 + +## techdocs-cli-embedded-app@0.2.112 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.34.2 + - @backstage/plugin-techdocs@1.15.0 + - @backstage/app-defaults@1.7.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-components@0.18.0 + - @backstage/core-app-api@1.19.0 + - @backstage/integration-react@1.2.10 + +## @internal/plugin-todo-list@1.0.43 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + +## @internal/plugin-todo-list-backend@1.0.43 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 diff --git a/package.json b/package.json index 4b74eda8a9..9cd3fc011b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.43.0-next.2", + "version": "1.43.0", "backstage": { "cli": { "new": { diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index f22c96ceca..02f126bcf5 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/app-defaults +## 1.7.0 + +### Minor Changes + +- 9956704: Add and configure the OpenShift authentication provider to the default APIs. + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-app-api@1.19.0 + ## 1.6.6-next.0 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 9f972eccbf..37973fbf0f 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/app-defaults", - "version": "1.6.6-next.0", + "version": "1.7.0", "description": "Provides the default wiring of a Backstage App", "backstage": { "role": "web-library" diff --git a/packages/app-next-example-plugin/CHANGELOG.md b/packages/app-next-example-plugin/CHANGELOG.md index 5748f8f75b..7949e120fa 100644 --- a/packages/app-next-example-plugin/CHANGELOG.md +++ b/packages/app-next-example-plugin/CHANGELOG.md @@ -1,5 +1,13 @@ # app-next-example-plugin +## 0.0.27 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-components@0.18.0 + ## 0.0.27-next.0 ### Patch Changes diff --git a/packages/app-next-example-plugin/package.json b/packages/app-next-example-plugin/package.json index 7878658b5d..3b76202029 100644 --- a/packages/app-next-example-plugin/package.json +++ b/packages/app-next-example-plugin/package.json @@ -1,6 +1,6 @@ { "name": "app-next-example-plugin", - "version": "0.0.27-next.0", + "version": "0.0.27", "description": "Backstage internal example plugin", "backstage": { "role": "frontend-plugin", diff --git a/packages/app-next/CHANGELOG.md b/packages/app-next/CHANGELOG.md index a4e2386525..7e66d0d041 100644 --- a/packages/app-next/CHANGELOG.md +++ b/packages/app-next/CHANGELOG.md @@ -1,5 +1,46 @@ # example-app-next +## 0.0.27 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.34.2 + - @backstage/ui@0.7.1 + - @backstage/plugin-techdocs@1.15.0 + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-notifications@0.5.9 + - @backstage/app-defaults@1.7.0 + - @backstage/plugin-catalog-graph@0.5.0 + - @backstage/plugin-org@0.6.44 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-auth-react@0.1.19 + - @backstage/plugin-auth@0.1.0 + - @backstage/plugin-home@0.8.12 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-scaffolder@1.34.1 + - @backstage/plugin-app@0.3.0 + - @backstage/plugin-user-settings@0.8.26 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-components@0.18.0 + - @backstage/frontend-app-api@0.13.0 + - @backstage/frontend-defaults@0.3.1 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-scaffolder-react@1.19.1 + - @backstage/core-app-api@1.19.0 + - @backstage/plugin-api-docs@0.12.11 + - @backstage/plugin-app-visualizer@0.1.23 + - @backstage/plugin-catalog-import@0.13.5 + - @backstage/plugin-catalog-unprocessed-entities@0.2.21 + - @backstage/plugin-kubernetes@0.12.11 + - @backstage/plugin-search@1.4.30 + - @backstage/plugin-search-react@1.9.4 + - @backstage/plugin-signals@0.0.23 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28 + - @backstage/integration-react@1.2.10 + - @backstage/plugin-kubernetes-cluster@0.0.29 + ## 0.0.27-next.2 ### Patch Changes diff --git a/packages/app-next/package.json b/packages/app-next/package.json index 49ce264f06..461b5673fc 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -1,6 +1,6 @@ { "name": "example-app-next", - "version": "0.0.27-next.2", + "version": "0.0.27", "backstage": { "role": "frontend" }, diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index f3b911accf..533f4ab84c 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,41 @@ # example-app +## 0.2.113 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.34.2 + - @backstage/ui@0.7.1 + - @backstage/plugin-techdocs@1.15.0 + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-notifications@0.5.9 + - @backstage/app-defaults@1.7.0 + - @backstage/plugin-catalog-graph@0.5.0 + - @backstage/plugin-org@0.6.44 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/plugin-auth-react@0.1.19 + - @backstage/plugin-home@0.8.12 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-scaffolder@1.34.1 + - @backstage/plugin-user-settings@0.8.26 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-components@0.18.0 + - @backstage/frontend-app-api@0.13.0 + - @backstage/plugin-scaffolder-react@1.19.1 + - @backstage/core-app-api@1.19.0 + - @backstage/plugin-api-docs@0.12.11 + - @backstage/plugin-catalog-import@0.13.5 + - @backstage/plugin-catalog-unprocessed-entities@0.2.21 + - @backstage/plugin-devtools@0.1.31 + - @backstage/plugin-kubernetes@0.12.11 + - @backstage/plugin-search@1.4.30 + - @backstage/plugin-search-react@1.9.4 + - @backstage/plugin-signals@0.0.23 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.28 + - @backstage/integration-react@1.2.10 + - @backstage/plugin-kubernetes-cluster@0.0.29 + ## 0.2.113-next.2 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 5e38aa6256..cb8fdbd9df 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.113-next.2", + "version": "0.2.113", "backstage": { "role": "frontend" }, diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md index a0d274a400..d965783fac 100644 --- a/packages/backend-app-api/CHANGELOG.md +++ b/packages/backend-app-api/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/backend-app-api +## 1.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + ## 1.2.7-next.0 ### Patch Changes diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index f20cd2e901..9407f28707 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-app-api", - "version": "1.2.7-next.0", + "version": "1.2.7", "description": "Core API used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index b4c7393455..1adfd0f80e 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/backend-defaults +## 0.12.1 + +### Patch Changes + +- 33bd4d0: Deduplicate discovered features discovered with discoveryFeatureLoader +- 4eda590: Fixed cache namespace and key prefix separator configuration to properly use configured values instead of hardcoded plugin ID. The cache manager now correctly combines the configured namespace with plugin IDs using the configured separator for Redis and Valkey. Memcache and memory store continue to use plugin ID as namespace. +- f244e61: Add `backend.logger` config options to configure the `RootLoggerService`. + + Read more about the new configuration options in the + [Root Logger Service](https://backstage.io/docs/backend-system/core-services/root-logger/) + documentation. + +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-app-api@1.2.7 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + ## 0.12.1-next.1 ### Patch Changes diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 1b1f0101c7..013e32db59 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-defaults", - "version": "0.12.1-next.1", + "version": "0.12.1", "description": "Backend defaults used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/backend-dynamic-feature-service/CHANGELOG.md b/packages/backend-dynamic-feature-service/CHANGELOG.md index d2e46404ef..e3f857785d 100644 --- a/packages/backend-dynamic-feature-service/CHANGELOG.md +++ b/packages/backend-dynamic-feature-service/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/backend-dynamic-feature-service +## 0.7.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/config-loader@1.10.3 + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-backend@0.5.6 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-app-node@0.1.37 + - @backstage/plugin-permission-node@0.10.4 + - @backstage/plugin-scaffolder-node@0.11.1 + - @backstage/plugin-search-backend-node@1.3.15 + ## 0.7.4-next.1 ### Patch Changes diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index 6a7cc2315f..04577cbf43 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-dynamic-feature-service", - "version": "0.7.4-next.1", + "version": "0.7.4", "description": "Backstage dynamic feature service", "backstage": { "role": "node-library" diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md index b86bc1b616..87bea0405e 100644 --- a/packages/backend-openapi-utils/CHANGELOG.md +++ b/packages/backend-openapi-utils/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/backend-openapi-utils +## 0.6.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.6.1-next.0 ### Patch Changes diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json index 02d9dd1407..31d7aaf486 100644 --- a/packages/backend-openapi-utils/package.json +++ b/packages/backend-openapi-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-openapi-utils", - "version": "0.6.1-next.0", + "version": "0.6.1", "description": "OpenAPI typescript support.", "backstage": { "role": "node-library" diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md index ffc1a6eb6a..914d45f68b 100644 --- a/packages/backend-plugin-api/CHANGELOG.md +++ b/packages/backend-plugin-api/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/backend-plugin-api +## 1.4.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/plugin-permission-node@0.10.4 + ## 1.4.3-next.0 ### Patch Changes diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index 46142aceda..baebdcd506 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-plugin-api", - "version": "1.4.3-next.0", + "version": "1.4.3", "description": "Core API used by Backstage backend plugins", "backstage": { "role": "node-library" diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index ad3915e3fb..cc82ef5593 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/backend-test-utils +## 1.9.0 + +### Minor Changes + +- 4e2c237: The `mockServices.rootConfig()` instance now has an `update` method that can be used to test configuration subscriptions and updates. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-app-api@1.2.7 + - @backstage/backend-plugin-api@1.4.3 + ## 1.9.0-next.1 ### Patch Changes diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 6a6d24cf57..868c98a8f2 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-test-utils", - "version": "1.9.0-next.1", + "version": "1.9.0", "description": "Test helpers library for Backstage backends", "backstage": { "role": "node-library" diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 15b61dfb4e..398465966e 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,44 @@ # example-backend +## 0.0.42 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-scaffolder-backend-module-github@0.9.0 + - @backstage/plugin-scaffolder-backend@2.2.1 + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-mcp-actions-backend@0.1.3 + - @backstage/plugin-auth-backend@0.25.4 + - @backstage/plugin-techdocs-backend@2.1.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-app-backend@0.5.6 + - @backstage/plugin-kubernetes-backend@0.20.2 + - @backstage/plugin-events-backend@0.5.6 + - @backstage/plugin-search-backend@2.0.6 + - @backstage/plugin-search-backend-module-explore@0.3.7 + - @backstage/plugin-notifications-backend@0.5.10 + - @backstage/plugin-auth-backend-module-openshift-provider@0.1.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-auth-backend-module-github-provider@0.3.7 + - @backstage/plugin-auth-backend-module-guest-provider@0.2.12 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.5.6 + - @backstage/plugin-catalog-backend-module-openapi@0.2.14 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12 + - @backstage/plugin-catalog-backend-module-unprocessed@0.6.4 + - @backstage/plugin-devtools-backend@0.5.9 + - @backstage/plugin-events-backend-module-google-pubsub@0.1.4 + - @backstage/plugin-permission-backend@0.7.4 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.2.12 + - @backstage/plugin-permission-node@0.10.4 + - @backstage/plugin-proxy-backend@0.6.6 + - @backstage/plugin-scaffolder-backend-module-notifications@0.1.14 + - @backstage/plugin-search-backend-module-catalog@0.3.8 + - @backstage/plugin-search-backend-module-techdocs@0.4.6 + - @backstage/plugin-search-backend-node@1.3.15 + - @backstage/plugin-signals-backend@0.3.8 + ## 0.0.42-next.1 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 64c2f1dd47..8bca4f3b22 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.0.42-next.1", + "version": "0.0.42", "backstage": { "role": "backend" }, diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 254f37bd69..a7d254702d 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/catalog-client +## 1.12.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- 0efcc97: Updated generated schemas + ## 1.12.0-next.0 ### Minor Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 3b9d1c603a..d3c13d9e6f 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-client", - "version": "1.12.0-next.0", + "version": "1.12.0", "description": "An isomorphic client for the catalog backend", "backstage": { "role": "common-library" diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index bdaf3a05f6..7947c15cbb 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/cli +## 0.34.2 + +### Patch Changes + +- e6f45dc: Updated the WebPack configuration to use `contenthash`. This fixes an issue were builds would sometimes generate output files with the same name but different content across builds, leading to breakages when loading the frontend app. +- fffd434: Disallow import fallback of critical shared dependencies in module federation. +- 080f252: Fixed the `new-frontend-plugin` template that was incorrectly passing `id` instead of `pluginId` to `createFrontendPlugin` and unnecessarily importing `React`. +- e0db9b8: Modify the `backstage.json` also for custom patterns if it extends the default pattern. + + Examples: + + - `@backstage/*` (default pattern) + - `@{backstage,backstage-community}/*` + - `@{extra1,backstage,extra2}/*` + +- 275bda8: Fixed an issue that could cause conflicts of detected modules in workspaces with multiple apps. +- e1adce4: Updated the backend plugin template to use a new pattern for the `TodoListService` that reduces boilerplate. +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + ## 0.34.2-next.2 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 8956b50713..f50a5601eb 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli", - "version": "0.34.2-next.2", + "version": "0.34.2", "description": "CLI for developing Backstage plugins and apps", "backstage": { "role": "cli" diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index a7b38c24b9..872136d5a2 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/config-loader +## 1.10.3 + +### Patch Changes + +- a73f495: Allow using `BACKSTAGE_ENV` for loading environment specific config files +- Updated dependencies + - @backstage/types@1.2.2 + ## 1.10.3-next.0 ### Patch Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index b0e7af3b5e..f8d103e97b 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/config-loader", - "version": "1.10.3-next.0", + "version": "1.10.3", "description": "Config loading functionality used by Backstage backend, and CLI", "backstage": { "role": "node-library" diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index c418a80e64..05bdf33224 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/core-app-api +## 1.19.0 + +### Minor Changes + +- 3fca906: Add `OpenShiftAuth` helper to create default OAuth flow for OpenShift. + +### Patch Changes + +- 5ae6d9d: feat: support no en languages +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/types@1.2.2 + ## 1.18.0 ### Minor Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 640921b120..8a52a71867 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-app-api", - "version": "1.18.0", + "version": "1.19.0", "description": "Core app API used by Backstage apps", "backstage": { "role": "web-library" diff --git a/packages/core-compat-api/CHANGELOG.md b/packages/core-compat-api/CHANGELOG.md index ea40b301bb..eb4c7d051f 100644 --- a/packages/core-compat-api/CHANGELOG.md +++ b/packages/core-compat-api/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/core-compat-api +## 0.5.2 + +### Patch Changes + +- dc01d6f: Fix for `compatWrapper` creating many wrapping `Providers` when they should not +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + ## 0.5.2-next.2 ### Patch Changes diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index db51762180..acab8947c8 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-compat-api", - "version": "0.5.2-next.2", + "version": "0.5.2", "backstage": { "role": "web-library" }, diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 10e04ae0c6..63e6e45065 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/core-components +## 0.18.0 + +### Minor Changes + +- b9a87f4: Add optional `distance` property to `DependencyEdge` to reflect the distance to a root. + +### Patch Changes + +- 1ad3d94: Dependency graph can now be opened in full screen mode +- e409bec: Fixes for rendering initials in the avatar component. +- ae7d426: update about card links style for pretty display with other language +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + ## 0.17.6-next.1 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 5cd6b9f270..d369052e99 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-components", - "version": "0.17.6-next.1", + "version": "0.18.0", "description": "Core components used by Backstage plugins and apps", "backstage": { "role": "web-library" diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index c109a9248e..61291f8e05 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/core-plugin-api +## 1.11.0 + +### Minor Changes + +- 5114627: Make `openshiftAuthApiRef` available in `@backstage/core-plugin-api`. + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + ## 1.10.9 ### Patch Changes diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index ec789b618f..6e62c66c3a 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-plugin-api", - "version": "1.10.9", + "version": "1.11.0", "description": "Core API used by Backstage plugins", "backstage": { "role": "web-library" diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 0e622c2490..5d317d4afa 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/create-app +## 0.7.4 + +### Patch Changes + +- b2d9fc1: Creates a plugin that redirects from the Home page to the Catalog index page to avoid seeing a not found page error when starting the app. +- 020d484: Bumped create-app version. +- 02dbe8e: Add missing dependency to `@backstage/cli`, `@backstage/core-plugin-api` and `@backstage/integration-react` + ## 0.7.4-next.2 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 59f8498c54..263d65e5c8 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/create-app", - "version": "0.7.4-next.2", + "version": "0.7.4", "description": "A CLI that helps you create your own Backstage app", "backstage": { "role": "cli" diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 45aaf0a814..47ec659481 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/dev-utils +## 1.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/app-defaults@1.7.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-app-api@1.19.0 + - @backstage/integration-react@1.2.10 + ## 1.1.14-next.2 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index fd5881af44..40882a898b 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/dev-utils", - "version": "1.1.14-next.2", + "version": "1.1.14", "description": "Utilities for developing Backstage plugins.", "backstage": { "role": "web-library" diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md index 1a0dd4e3ea..471c2fc53e 100644 --- a/packages/e2e-test/CHANGELOG.md +++ b/packages/e2e-test/CHANGELOG.md @@ -1,5 +1,12 @@ # e2e-test +## 0.2.32 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.7.4 + ## 0.2.32-next.0 ### Patch Changes diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 82a0ee1a2a..6b255f43b1 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -1,6 +1,6 @@ { "name": "e2e-test", - "version": "0.2.32-next.0", + "version": "0.2.32", "description": "E2E test for verifying Backstage packages", "backstage": { "role": "cli" diff --git a/packages/frontend-app-api/CHANGELOG.md b/packages/frontend-app-api/CHANGELOG.md index 50cd8614a2..1a23d4b535 100644 --- a/packages/frontend-app-api/CHANGELOG.md +++ b/packages/frontend-app-api/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/frontend-app-api +## 0.13.0 + +### Minor Changes + +- 6516c3d: The `createSpecializedApp` no longer throws when encountering many common errors when starting up the app. It will instead return them through the `errors` property so that they can be handled more gracefully in the app. + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/types@1.2.2 + - @backstage/frontend-defaults@0.3.1 + - @backstage/core-app-api@1.19.0 + ## 0.12.1-next.0 ### Patch Changes diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index ce62403915..3f2dab8571 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-app-api", - "version": "0.12.1-next.0", + "version": "0.13.0", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-defaults/CHANGELOG.md b/packages/frontend-defaults/CHANGELOG.md index 86eae4efdf..65713fac5f 100644 --- a/packages/frontend-defaults/CHANGELOG.md +++ b/packages/frontend-defaults/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/frontend-defaults +## 0.3.1 + +### Patch Changes + +- 6516c3d: The default app now leverages the new error reporting functionality from `@backstage/frontend-app-api`. If there are critical errors during startup, an error screen that shows a summary of all errors will now be shown, rather than leaving the screen blank. Other errors will be logged as warnings in the console. +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-app@0.3.0 + - @backstage/core-components@0.18.0 + - @backstage/frontend-app-api@0.13.0 + ## 0.3.1-next.0 ### Patch Changes diff --git a/packages/frontend-defaults/package.json b/packages/frontend-defaults/package.json index 880cd618ef..6a8b100c0d 100644 --- a/packages/frontend-defaults/package.json +++ b/packages/frontend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-defaults", - "version": "0.3.1-next.0", + "version": "0.3.1", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-dynamic-feature-loader/CHANGELOG.md b/packages/frontend-dynamic-feature-loader/CHANGELOG.md index dda15e8fd1..0e7886365d 100644 --- a/packages/frontend-dynamic-feature-loader/CHANGELOG.md +++ b/packages/frontend-dynamic-feature-loader/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/frontend-dynamic-feature-loader +## 0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + ## 0.1.5-next.0 ### Patch Changes diff --git a/packages/frontend-dynamic-feature-loader/package.json b/packages/frontend-dynamic-feature-loader/package.json index b965bdf7a1..7ed00c92de 100644 --- a/packages/frontend-dynamic-feature-loader/package.json +++ b/packages/frontend-dynamic-feature-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-dynamic-feature-loader", - "version": "0.1.5-next.0", + "version": "0.1.5", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-internal/CHANGELOG.md b/packages/frontend-internal/CHANGELOG.md index 0537c8ffd1..7de465f8c0 100644 --- a/packages/frontend-internal/CHANGELOG.md +++ b/packages/frontend-internal/CHANGELOG.md @@ -1,5 +1,13 @@ # @internal/frontend +## 0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/types@1.2.2 + ## 0.0.13-next.0 ### Patch Changes diff --git a/packages/frontend-internal/package.json b/packages/frontend-internal/package.json index ea68ff1405..2399eaa453 100644 --- a/packages/frontend-internal/package.json +++ b/packages/frontend-internal/package.json @@ -1,6 +1,6 @@ { "name": "@internal/frontend", - "version": "0.0.13-next.0", + "version": "0.0.13", "backstage": { "role": "web-library", "inline": true diff --git a/packages/frontend-plugin-api/CHANGELOG.md b/packages/frontend-plugin-api/CHANGELOG.md index 1b39c5ad44..647bd8f6f1 100644 --- a/packages/frontend-plugin-api/CHANGELOG.md +++ b/packages/frontend-plugin-api/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/frontend-plugin-api +## 0.12.0 + +### Minor Changes + +- 894d514: Make `openshiftApiRef` available to the new frontend system. + +### Patch Changes + +- 2fb8b04: Improved the types of `createFrontendPlugin` and `createFrontendModule` so that errors due to incompatible options are indicated more clearly. +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + ## 0.11.1-next.0 ### Patch Changes diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index 2eb218e5ab..1e261787fb 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-plugin-api", - "version": "0.11.1-next.0", + "version": "0.12.0", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-test-utils/CHANGELOG.md b/packages/frontend-test-utils/CHANGELOG.md index 585322aa8e..4e96c27dd3 100644 --- a/packages/frontend-test-utils/CHANGELOG.md +++ b/packages/frontend-test-utils/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/frontend-test-utils +## 0.3.6 + +### Patch Changes + +- 6516c3d: Internal update to use and throw app errors. +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-app@0.3.0 + - @backstage/frontend-app-api@0.13.0 + - @backstage/types@1.2.2 + ## 0.3.6-next.0 ### Patch Changes diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index b45dbb76e2..ba71540d7a 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-test-utils", - "version": "0.3.6-next.0", + "version": "0.3.6", "backstage": { "role": "web-library" }, diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index 543f8796dd..b152cf511b 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/integration-react +## 1.2.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/integration@1.18.0 + ## 1.2.10-next.0 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 1a7d6dc01e..a3d8000f94 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration-react", - "version": "1.2.10-next.0", + "version": "1.2.10", "description": "Frontend package for managing integrations towards external systems", "backstage": { "role": "web-library" diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index 11f87da988..946e392c0b 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/integration +## 1.18.0 + +### Minor Changes + +- 03bdc68: Added support for limiting GithubAppCredentialsMux to specific apps + +### Patch Changes + +- 56897d7: Fixes issue with Github credentials provider which fails to match organization name if using allowedInstallationOwners + ## 1.18.0-next.0 ### Minor Changes diff --git a/packages/integration/package.json b/packages/integration/package.json index 014bd3564e..68289e2079 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration", - "version": "1.18.0-next.0", + "version": "1.18.0", "description": "Helpers for managing integrations towards external systems", "backstage": { "role": "common-library" diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index a5a9bb0505..d93d88571a 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/repo-tools +## 0.15.2 + +### Patch Changes + +- 133ac7a: Fixed knip-reports command failing when workspace path contains spaces and process termination issues by replacing `execFile` with `spawn` and removing `shell` option. +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/backend-plugin-api@1.4.3 + ## 0.15.2-next.1 ### Patch Changes diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index d93785f25e..0b0954da70 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/repo-tools", - "version": "0.15.2-next.1", + "version": "0.15.2", "description": "CLI for Backstage repo tooling ", "backstage": { "role": "cli" diff --git a/packages/scaffolder-internal/CHANGELOG.md b/packages/scaffolder-internal/CHANGELOG.md index dea0dbb318..de8c725e03 100644 --- a/packages/scaffolder-internal/CHANGELOG.md +++ b/packages/scaffolder-internal/CHANGELOG.md @@ -1,5 +1,13 @@ # @internal/scaffolder +## 0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-scaffolder-react@1.19.1 + ## 0.0.13-next.1 ### Patch Changes diff --git a/packages/scaffolder-internal/package.json b/packages/scaffolder-internal/package.json index 6bfae424d7..abbd487103 100644 --- a/packages/scaffolder-internal/package.json +++ b/packages/scaffolder-internal/package.json @@ -1,6 +1,6 @@ { "name": "@internal/scaffolder", - "version": "0.0.13-next.1", + "version": "0.0.13", "backstage": { "role": "web-library", "inline": true diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 7bc24fb346..76a8f81a63 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,20 @@ # techdocs-cli-embedded-app +## 0.2.112 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.34.2 + - @backstage/plugin-techdocs@1.15.0 + - @backstage/app-defaults@1.7.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-components@0.18.0 + - @backstage/core-app-api@1.19.0 + - @backstage/integration-react@1.2.10 + ## 0.2.112-next.1 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 132cf5a987..09e974ed88 100644 --- a/packages/techdocs-cli-embedded-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -1,6 +1,6 @@ { "name": "techdocs-cli-embedded-app", - "version": "0.2.112-next.1", + "version": "0.2.112", "backstage": { "role": "frontend" }, diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index c888ff6b4f..1fb32bec51 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,14 @@ # @techdocs/cli +## 1.9.8 + +### Patch Changes + +- db63208: Fixed an issue where `@techdocs/cli serve` command did not pick up the latest changes to TechDocs. +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-techdocs-node@1.13.7 + ## 1.9.8-next.0 ### Patch Changes diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 8ae30b9889..7ce62cd803 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,6 +1,6 @@ { "name": "@techdocs/cli", - "version": "1.9.8-next.0", + "version": "1.9.8", "description": "Utility CLI for managing TechDocs sites in Backstage.", "backstage": { "role": "cli" diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index c6adabdae1..abfaacfd45 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/types +## 1.2.2 + +### Patch Changes + +- a95cebd: Internal refactoring for better type support + ## 1.2.1 ### Patch Changes diff --git a/packages/types/package.json b/packages/types/package.json index 55c3a36f4a..f7dcb5d11f 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/types", - "version": "1.2.1", + "version": "1.2.2", "description": "Common TypeScript types used within Backstage", "backstage": { "role": "common-library" diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index d251516dcd..0fcd9d2ff6 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/ui +## 0.7.1 + +### Patch Changes + +- 7307930: Add missing class for flex: baseline +- 89da341: Fix Select component to properly attach aria-label and aria-labelledby props to the rendered element for improved accessibility. +- 0ffa4c7: Removed the need to mock `window.matchMedia` in tests, falling back to default breakpoint values instead. + ## 0.7.1-next.0 ### Patch Changes diff --git a/packages/ui/package.json b/packages/ui/package.json index 7b14af29e1..c0a0e7c143 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/ui", - "version": "0.7.1-next.0", + "version": "0.7.1", "backstage": { "role": "web-library" }, diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index c57a56754c..4a40848ced 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-api-docs +## 0.12.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + ## 0.12.11-next.2 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 17cf78a69a..febec4ff08 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.12.11-next.2", + "version": "0.12.11", "description": "A Backstage plugin that helps represent API entities in the frontend", "backstage": { "role": "frontend-plugin", diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index ffe2bd1b71..02039c933e 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-app-backend +## 0.5.6 + +### Patch Changes + +- afd368e: Internal update to not expose the old `createRouter`. +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-app-node@0.1.37 + ## 0.5.6-next.1 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 4640a6459b..609cf1c543 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.5.6-next.1", + "version": "0.5.6", "description": "A Backstage backend plugin that serves the Backstage frontend app", "backstage": { "role": "backend-plugin", diff --git a/plugins/app-node/CHANGELOG.md b/plugins/app-node/CHANGELOG.md index 14b3a215ee..ea9532aca5 100644 --- a/plugins/app-node/CHANGELOG.md +++ b/plugins/app-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-app-node +## 0.1.37 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.10.3 + - @backstage/backend-plugin-api@1.4.3 + ## 0.1.37-next.1 ### Patch Changes diff --git a/plugins/app-node/package.json b/plugins/app-node/package.json index f28b28f461..f11a84bc55 100644 --- a/plugins/app-node/package.json +++ b/plugins/app-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-node", - "version": "0.1.37-next.1", + "version": "0.1.37", "description": "Node.js library for the app plugin", "backstage": { "role": "node-library", diff --git a/plugins/app-visualizer/CHANGELOG.md b/plugins/app-visualizer/CHANGELOG.md index 1270d1dab5..bc5ac063cc 100644 --- a/plugins/app-visualizer/CHANGELOG.md +++ b/plugins/app-visualizer/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-app-visualizer +## 0.1.23 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + ## 0.1.23-next.0 ### Patch Changes diff --git a/plugins/app-visualizer/package.json b/plugins/app-visualizer/package.json index f5260dec0c..4affb81b50 100644 --- a/plugins/app-visualizer/package.json +++ b/plugins/app-visualizer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-visualizer", - "version": "0.1.23-next.0", + "version": "0.1.23", "description": "Visualizes the Backstage app structure", "backstage": { "role": "frontend-plugin", diff --git a/plugins/app/CHANGELOG.md b/plugins/app/CHANGELOG.md index 56ea8057aa..c1026add86 100644 --- a/plugins/app/CHANGELOG.md +++ b/plugins/app/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-app +## 0.3.0 + +### Minor Changes + +- 99790db: Add implementation of OpenShift authentication provider. + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/integration-react@1.2.10 + ## 0.2.1-next.0 ### Patch Changes diff --git a/plugins/app/package.json b/plugins/app/package.json index 229837c340..d17d903b0a 100644 --- a/plugins/app/package.json +++ b/plugins/app/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app", - "version": "0.2.1-next.0", + "version": "0.3.0", "backstage": { "role": "frontend-plugin", "pluginId": "app", diff --git a/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md b/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md index 98f6213d34..16d1637cd6 100644 --- a/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-atlassian-provider +## 0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-atlassian-provider/package.json b/plugins/auth-backend-module-atlassian-provider/package.json index 9600af59ea..4fd74b74ca 100644 --- a/plugins/auth-backend-module-atlassian-provider/package.json +++ b/plugins/auth-backend-module-atlassian-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-atlassian-provider", - "version": "0.4.7-next.0", + "version": "0.4.7", "description": "The atlassian-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-auth0-provider/CHANGELOG.md b/plugins/auth-backend-module-auth0-provider/CHANGELOG.md index ce929a85e7..725b88735c 100644 --- a/plugins/auth-backend-module-auth0-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-auth0-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-auth0-provider +## 0.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-auth0-provider/package.json b/plugins/auth-backend-module-auth0-provider/package.json index 741c6ed8c6..8e06baa4db 100644 --- a/plugins/auth-backend-module-auth0-provider/package.json +++ b/plugins/auth-backend-module-auth0-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-auth0-provider", - "version": "0.2.7-next.0", + "version": "0.2.7", "description": "The auth0-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md b/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md index 9b6c019506..f2026fd2b9 100644 --- a/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-aws-alb-provider +## 0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.25.4 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-aws-alb-provider/package.json b/plugins/auth-backend-module-aws-alb-provider/package.json index 801cd0e62f..d85b993f28 100644 --- a/plugins/auth-backend-module-aws-alb-provider/package.json +++ b/plugins/auth-backend-module-aws-alb-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-aws-alb-provider", - "version": "0.4.7-next.0", + "version": "0.4.7", "description": "The aws-alb provider module for the Backstage auth backend.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md b/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md index cf6931c11f..355d9ea55f 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-azure-easyauth-provider +## 0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.12-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-azure-easyauth-provider/package.json b/plugins/auth-backend-module-azure-easyauth-provider/package.json index 98e665fbbf..534d721107 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/package.json +++ b/plugins/auth-backend-module-azure-easyauth-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-azure-easyauth-provider", - "version": "0.2.12-next.0", + "version": "0.2.12", "description": "The azure-easyauth-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md b/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md index c998f1cb8d..79a379c94c 100644 --- a/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-bitbucket-provider +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-bitbucket-provider/package.json b/plugins/auth-backend-module-bitbucket-provider/package.json index fe66aee5d5..e7826bcf08 100644 --- a/plugins/auth-backend-module-bitbucket-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-bitbucket-provider", - "version": "0.3.7-next.0", + "version": "0.3.7", "description": "The bitbucket-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-bitbucket-server-provider/CHANGELOG.md b/plugins/auth-backend-module-bitbucket-server-provider/CHANGELOG.md index fb160d4dcd..dad1d86780 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-bitbucket-server-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-bitbucket-server-provider +## 0.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-bitbucket-server-provider/package.json b/plugins/auth-backend-module-bitbucket-server-provider/package.json index c1ed77c3cd..7f830811e7 100644 --- a/plugins/auth-backend-module-bitbucket-server-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-server-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-bitbucket-server-provider", - "version": "0.2.7-next.0", + "version": "0.2.7", "description": "The bitbucket-server-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md b/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md index 6cdb312961..3bd2055825 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-cloudflare-access-provider +## 0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-cloudflare-access-provider/package.json b/plugins/auth-backend-module-cloudflare-access-provider/package.json index ddcbde2425..537526dbdc 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/package.json +++ b/plugins/auth-backend-module-cloudflare-access-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-cloudflare-access-provider", - "version": "0.4.7-next.0", + "version": "0.4.7", "description": "The cloudflare-access-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md index 3e45cb6195..d587f6a999 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-gcp-iap-provider +## 0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-gcp-iap-provider/package.json b/plugins/auth-backend-module-gcp-iap-provider/package.json index 05d3db7892..9bf40aaf4d 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/package.json +++ b/plugins/auth-backend-module-gcp-iap-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-gcp-iap-provider", - "version": "0.4.7-next.0", + "version": "0.4.7", "description": "A GCP IAP auth provider module for the Backstage auth backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-github-provider/CHANGELOG.md b/plugins/auth-backend-module-github-provider/CHANGELOG.md index 149f6a79d0..0ea39f5b6d 100644 --- a/plugins/auth-backend-module-github-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-github-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-github-provider +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-github-provider/package.json b/plugins/auth-backend-module-github-provider/package.json index 8cd8a41111..d5854b1ff2 100644 --- a/plugins/auth-backend-module-github-provider/package.json +++ b/plugins/auth-backend-module-github-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-github-provider", - "version": "0.3.7-next.0", + "version": "0.3.7", "description": "The github-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md index 3eebbc89fb..afd0f2258f 100644 --- a/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-gitlab-provider +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-gitlab-provider/package.json b/plugins/auth-backend-module-gitlab-provider/package.json index fc55675419..b7cb8c5da3 100644 --- a/plugins/auth-backend-module-gitlab-provider/package.json +++ b/plugins/auth-backend-module-gitlab-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-gitlab-provider", - "version": "0.3.7-next.0", + "version": "0.3.7", "description": "The gitlab-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-google-provider/CHANGELOG.md b/plugins/auth-backend-module-google-provider/CHANGELOG.md index 1c464a3f2f..7c5651d120 100644 --- a/plugins/auth-backend-module-google-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-google-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-google-provider +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json index d9ee2a862a..f947b84b39 100644 --- a/plugins/auth-backend-module-google-provider/package.json +++ b/plugins/auth-backend-module-google-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-google-provider", - "version": "0.3.7-next.0", + "version": "0.3.7", "description": "A Google auth provider module for the Backstage auth backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-guest-provider/CHANGELOG.md b/plugins/auth-backend-module-guest-provider/CHANGELOG.md index b92fdf5c88..d57b0e967e 100644 --- a/plugins/auth-backend-module-guest-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-guest-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-guest-provider +## 0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.12-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-guest-provider/package.json b/plugins/auth-backend-module-guest-provider/package.json index 900cc6001c..7e6a657797 100644 --- a/plugins/auth-backend-module-guest-provider/package.json +++ b/plugins/auth-backend-module-guest-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-guest-provider", - "version": "0.2.12-next.0", + "version": "0.2.12", "description": "The guest-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md b/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md index 49e5d06c63..3a21bd1bf7 100644 --- a/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-microsoft-provider +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-microsoft-provider/package.json b/plugins/auth-backend-module-microsoft-provider/package.json index 7fe3e19bd4..a5f661b839 100644 --- a/plugins/auth-backend-module-microsoft-provider/package.json +++ b/plugins/auth-backend-module-microsoft-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-microsoft-provider", - "version": "0.3.7-next.0", + "version": "0.3.7", "description": "The microsoft-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md b/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md index 0091dfff3e..0caf910ee2 100644 --- a/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-oauth2-provider +## 0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-oauth2-provider/package.json b/plugins/auth-backend-module-oauth2-provider/package.json index c5a3b58b7b..beeba6af8b 100644 --- a/plugins/auth-backend-module-oauth2-provider/package.json +++ b/plugins/auth-backend-module-oauth2-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oauth2-provider", - "version": "0.4.7-next.0", + "version": "0.4.7", "description": "The oauth2-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md b/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md index 77c2890c73..b64d3a2481 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-oauth2-proxy-provider +## 0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.12-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/package.json b/plugins/auth-backend-module-oauth2-proxy-provider/package.json index 370021bfd4..6edd907d51 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/package.json +++ b/plugins/auth-backend-module-oauth2-proxy-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oauth2-proxy-provider", - "version": "0.2.12-next.0", + "version": "0.2.12", "description": "The oauth2-proxy-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oidc-provider/CHANGELOG.md b/plugins/auth-backend-module-oidc-provider/CHANGELOG.md index 370a089c97..d21b18e15c 100644 --- a/plugins/auth-backend-module-oidc-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oidc-provider/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-auth-backend-module-oidc-provider +## 0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.25.4 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-oidc-provider/package.json b/plugins/auth-backend-module-oidc-provider/package.json index 9339b5d8ce..d40ded83d7 100644 --- a/plugins/auth-backend-module-oidc-provider/package.json +++ b/plugins/auth-backend-module-oidc-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oidc-provider", - "version": "0.4.7-next.0", + "version": "0.4.7", "description": "The oidc-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-okta-provider/CHANGELOG.md b/plugins/auth-backend-module-okta-provider/CHANGELOG.md index 69f695b3c4..4db3b0639b 100644 --- a/plugins/auth-backend-module-okta-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-okta-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-okta-provider +## 0.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-okta-provider/package.json b/plugins/auth-backend-module-okta-provider/package.json index 026daaa886..a1f8977142 100644 --- a/plugins/auth-backend-module-okta-provider/package.json +++ b/plugins/auth-backend-module-okta-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-okta-provider", - "version": "0.2.7-next.0", + "version": "0.2.7", "description": "The okta-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md b/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md index a93e56280d..95b2e88aec 100644 --- a/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-onelogin-provider +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-onelogin-provider/package.json b/plugins/auth-backend-module-onelogin-provider/package.json index 465c9eb5b8..a7b087aaa6 100644 --- a/plugins/auth-backend-module-onelogin-provider/package.json +++ b/plugins/auth-backend-module-onelogin-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-onelogin-provider", - "version": "0.3.7-next.0", + "version": "0.3.7", "description": "The onelogin-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-openshift-provider/CHANGELOG.md b/plugins/auth-backend-module-openshift-provider/CHANGELOG.md new file mode 100644 index 0000000000..06f116c2ef --- /dev/null +++ b/plugins/auth-backend-module-openshift-provider/CHANGELOG.md @@ -0,0 +1,14 @@ +# @backstage/plugin-auth-backend-module-openshift-provider + +## 0.1.0 + +### Minor Changes + +- 5a84253: Add new `auth-backend-module-openshift-provider`. This authentication provider enables Backstage to sign in with OpenShift. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 diff --git a/plugins/auth-backend-module-openshift-provider/package.json b/plugins/auth-backend-module-openshift-provider/package.json index 9fad0b542e..648f09bf24 100644 --- a/plugins/auth-backend-module-openshift-provider/package.json +++ b/plugins/auth-backend-module-openshift-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-openshift-provider", - "version": "0.0.0", + "version": "0.1.0", "description": "The OpenShift backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md b/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md index a5c687b6aa..a23d6cb733 100644 --- a/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-pinniped-provider +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-pinniped-provider/package.json b/plugins/auth-backend-module-pinniped-provider/package.json index 2c5b1fe7b4..ad80ff2904 100644 --- a/plugins/auth-backend-module-pinniped-provider/package.json +++ b/plugins/auth-backend-module-pinniped-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-pinniped-provider", - "version": "0.3.7-next.0", + "version": "0.3.7", "description": "The pinniped-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md b/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md index f197712c5b..41ac1cafdd 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-vmware-cloud-provider +## 0.5.7 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.5.7-next.0 ### Patch Changes diff --git a/plugins/auth-backend-module-vmware-cloud-provider/package.json b/plugins/auth-backend-module-vmware-cloud-provider/package.json index 23f5b7eeb5..0222da22a5 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/package.json +++ b/plugins/auth-backend-module-vmware-cloud-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-vmware-cloud-provider", - "version": "0.5.7-next.0", + "version": "0.5.7", "description": "The vmware-cloud-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 7aa33c315a..be4b8bcaad 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-auth-backend +## 0.25.4 + +### Patch Changes + +- 1d47bf3: Implementing Dynamic Client Registration with the OIDC server. You can enable this by setting `auth.experimentalDynamicClientRegistration.enabled` in `app-config.yaml`. This is highly experimental, but feedback welcome. +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.25.4-next.1 ### Patch Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index e5f048ccae..74c03204b6 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.25.4-next.1", + "version": "0.25.4", "description": "A Backstage backend plugin that handles authentication", "backstage": { "role": "backend-plugin", diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index 3c2ceea7c0..d784004cd3 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-auth-node +## 0.6.7 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- 3aff9e1: Changes OAuth cookies from domain-scoped to host-only by avoid setting the domain attribute in the default cookie configurer. +- Updated dependencies + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.6.7-next.1 ### Patch Changes diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index 64c12ae977..5c61000f64 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.6.7-next.1", + "version": "0.6.7", "backstage": { "role": "node-library", "pluginId": "auth", diff --git a/plugins/auth-react/CHANGELOG.md b/plugins/auth-react/CHANGELOG.md index 9b314d3e4f..88440f314b 100644 --- a/plugins/auth-react/CHANGELOG.md +++ b/plugins/auth-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-react +## 0.1.19 + +### Patch Changes + +- 54ddfef: Updating plugin metadata +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + ## 0.1.19-next.1 ### Patch Changes diff --git a/plugins/auth-react/package.json b/plugins/auth-react/package.json index 2bfee0dd44..77b01bfdd1 100644 --- a/plugins/auth-react/package.json +++ b/plugins/auth-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-react", - "version": "0.1.19-next.1", + "version": "0.1.19", "description": "Web library for the auth plugin", "backstage": { "role": "web-library", diff --git a/plugins/auth/CHANGELOG.md b/plugins/auth/CHANGELOG.md index d28fef1c9e..7092c0bf3c 100644 --- a/plugins/auth/CHANGELOG.md +++ b/plugins/auth/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-auth +## 0.1.0 + +### Minor Changes + +- 54ddfef: Initial publish of the `auth` frontend package + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-components@0.18.0 + ## 0.1.0-next.0 ### Minor Changes diff --git a/plugins/auth/package.json b/plugins/auth/package.json index 6c8184db35..dffb0eb8ff 100644 --- a/plugins/auth/package.json +++ b/plugins/auth/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth", - "version": "0.1.0-next.0", + "version": "0.1.0", "backstage": { "role": "frontend-plugin", "pluginId": "auth", diff --git a/plugins/bitbucket-cloud-common/CHANGELOG.md b/plugins/bitbucket-cloud-common/CHANGELOG.md index 51cb463738..31345b723c 100644 --- a/plugins/bitbucket-cloud-common/CHANGELOG.md +++ b/plugins/bitbucket-cloud-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-bitbucket-cloud-common +## 0.3.2 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + ## 0.3.2-next.0 ### Patch Changes diff --git a/plugins/bitbucket-cloud-common/package.json b/plugins/bitbucket-cloud-common/package.json index cece3f6aad..1c72ef31b9 100644 --- a/plugins/bitbucket-cloud-common/package.json +++ b/plugins/bitbucket-cloud-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bitbucket-cloud-common", - "version": "0.3.2-next.0", + "version": "0.3.2", "description": "Common functionalities for bitbucket-cloud plugins", "backstage": { "role": "common-library", diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index 7ceff47a80..0836bee10d 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.4.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.15-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 4dfe5b990d..3f21de8eb1 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", - "version": "0.4.15-next.1", + "version": "0.4.15", "description": "A Backstage catalog backend module that helps integrate towards AWS", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md index 0962c2aa28..842d0498cf 100644 --- a/plugins/catalog-backend-module-azure/CHANGELOG.md +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-azure +## 0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.9-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index dad34fec2a..17c66699a5 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-azure", - "version": "0.3.9-next.1", + "version": "0.3.9", "description": "A Backstage catalog backend module that helps integrate towards Azure", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md index d40c4e03e4..5021408d44 100644 --- a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-backstage-openapi +## 0.5.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + ## 0.5.6-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-backstage-openapi/package.json b/plugins/catalog-backend-module-backstage-openapi/package.json index f004e1bb16..a17bf50b47 100644 --- a/plugins/catalog-backend-module-backstage-openapi/package.json +++ b/plugins/catalog-backend-module-backstage-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-backstage-openapi", - "version": "0.5.6-next.1", + "version": "0.5.6", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md index c462210097..45d3c5ed5b 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-backend-module-bitbucket-cloud +## 0.5.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-bitbucket-cloud-common@0.3.2 + ## 0.5.3-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index 97eea0897f..68dfbaa619 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-cloud", - "version": "0.5.3-next.1", + "version": "0.5.3", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket Cloud", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md index 66965f3009..1df0cda05b 100644 --- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-backend-module-bitbucket-server +## 0.5.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + ## 0.5.3-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 98d28131a9..ce3ec49ae8 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-server", - "version": "0.5.3-next.1", + "version": "0.5.3", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-gcp/CHANGELOG.md b/plugins/catalog-backend-module-gcp/CHANGELOG.md index fa30682d37..4d06b61dbe 100644 --- a/plugins/catalog-backend-module-gcp/CHANGELOG.md +++ b/plugins/catalog-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-gcp +## 0.3.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.12-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gcp/package.json b/plugins/catalog-backend-module-gcp/package.json index 5459221795..8fb1acd48f 100644 --- a/plugins/catalog-backend-module-gcp/package.json +++ b/plugins/catalog-backend-module-gcp/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gcp", - "version": "0.3.12-next.1", + "version": "0.3.12", "description": "A Backstage catalog backend module that helps integrate towards GCP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gerrit/CHANGELOG.md b/plugins/catalog-backend-module-gerrit/CHANGELOG.md index ea0a8a0bbb..097e4f344f 100644 --- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-gerrit +## 0.3.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.6-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index 9dac6c9526..1c4d7e8aa7 100644 --- a/plugins/catalog-backend-module-gerrit/package.json +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gerrit", - "version": "0.3.6-next.1", + "version": "0.3.6", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-gitea/CHANGELOG.md b/plugins/catalog-backend-module-gitea/CHANGELOG.md index f8b03e496c..3274f6c9f1 100644 --- a/plugins/catalog-backend-module-gitea/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitea/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-gitea +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitea/package.json b/plugins/catalog-backend-module-gitea/package.json index 521179be95..c9dacc7b64 100644 --- a/plugins/catalog-backend-module-gitea/package.json +++ b/plugins/catalog-backend-module-gitea/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitea", - "version": "0.1.4-next.1", + "version": "0.1.4", "license": "Apache-2.0", "description": "The gitea backend module for the catalog plugin.", "main": "src/index.ts", diff --git a/plugins/catalog-backend-module-github-org/CHANGELOG.md b/plugins/catalog-backend-module-github-org/CHANGELOG.md index 9007c5ab97..6198a1942e 100644 --- a/plugins/catalog-backend-module-github-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-github-org/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-backend-module-github-org +## 0.3.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/plugin-catalog-backend-module-github@0.11.0 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.14-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-github-org/package.json b/plugins/catalog-backend-module-github-org/package.json index ff2b78abc1..f9c1f516fb 100644 --- a/plugins/catalog-backend-module-github-org/package.json +++ b/plugins/catalog-backend-module-github-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github-org", - "version": "0.3.14-next.1", + "version": "0.3.14", "description": "The github-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md index 444d602783..718a0035e8 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-backend-module-github +## 0.11.0 + +### Minor Changes + +- 03bdc68: Added support for discovery by app + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + ## 0.11.0-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index 42c8446a75..5187103233 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github", - "version": "0.11.0-next.1", + "version": "0.11.0", "description": "A Backstage catalog backend module that helps integrate towards GitHub", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md index 88d11fb806..022eb1445a 100644 --- a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-backend-module-gitlab-org +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-gitlab@0.7.3 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.13-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab-org/package.json b/plugins/catalog-backend-module-gitlab-org/package.json index 98d45ebe61..09f6ee7811 100644 --- a/plugins/catalog-backend-module-gitlab-org/package.json +++ b/plugins/catalog-backend-module-gitlab-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab-org", - "version": "0.2.13-next.1", + "version": "0.2.13", "description": "The gitlab-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md index adbc95ee65..2610055302 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.7.3 + +### Patch Changes + +- ea80e76: When possible, requests a more limited set of results from the Gitlab projects API, which can reduce the amount of network traffic required to sync with Gitlab. +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + ## 0.7.3-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 14d62a2ad8..c18be1028b 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab", - "version": "0.7.3-next.1", + "version": "0.7.3", "description": "A Backstage catalog backend module that helps integrate towards GitLab", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md index 1d93406c49..aa22c5aad7 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md +++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-backend-module-incremental-ingestion +## 0.7.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.7.4-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json index 7a83dd50af..ffc675fcff 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/package.json +++ b/plugins/catalog-backend-module-incremental-ingestion/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-incremental-ingestion", - "version": "0.7.4-next.1", + "version": "0.7.4", "description": "An entity provider for streaming large asset sources into the catalog", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index 343a87c514..2a1dbf6d62 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.11.9 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.11.9-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 02e2216d09..91ac03c93a 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", - "version": "0.11.9-next.1", + "version": "0.11.9", "description": "A Backstage catalog backend module that helps integrate towards LDAP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-logs/CHANGELOG.md b/plugins/catalog-backend-module-logs/CHANGELOG.md index 9c42d29587..fbc0b1dfa7 100644 --- a/plugins/catalog-backend-module-logs/CHANGELOG.md +++ b/plugins/catalog-backend-module-logs/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-logs +## 0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@3.1.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + ## 0.1.14-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-logs/package.json b/plugins/catalog-backend-module-logs/package.json index be3ce91abd..3b27e9cd79 100644 --- a/plugins/catalog-backend-module-logs/package.json +++ b/plugins/catalog-backend-module-logs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-logs", - "version": "0.1.14-next.0", + "version": "0.1.14", "description": "A module that subscribes to catalog related events and logs them.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index 0ecb111c7a..8ab315e9dd 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.8.0 + +### Minor Changes + +- 577f0ed: **BREAKING**: Encode query filters for requests made to msgraph. If you currently have manually encoded characters in a filter, this is a breaking change and must be updated to avoid requests being double encoded. + + ```diff + user: + - filter: department in('MARKETING', 'RESEARCH %26 DEVELOPMENT') + + filter: department in('MARKETING', 'RESEARCH & DEVELOPMENT') + ``` + +### Patch Changes + +- 7597781: Ensure that msgraph parent group stays same in case the group has multiple parents +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/backend-plugin-api@1.4.3 + ## 0.8.0-next.2 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 12fcfa0585..f2f8a5d442 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", - "version": "0.8.0-next.2", + "version": "0.8.0", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md index 4a1c9eca0a..6cef384c74 100644 --- a/plugins/catalog-backend-module-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-backend-module-openapi +## 0.2.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.14-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index fec08415cd..155c24115d 100644 --- a/plugins/catalog-backend-module-openapi/package.json +++ b/plugins/catalog-backend-module-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-openapi", - "version": "0.2.14-next.1", + "version": "0.2.14", "description": "A Backstage catalog backend module that helps with OpenAPI specifications", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md index dee1e885fc..14fa817eba 100644 --- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md +++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-backend-module-puppetdb +## 0.2.14 + +### Patch Changes + +- afd368e: **BREAKING ALPHA**: The module has been moved from the `/alpha` export to the root of the package. +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.14-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index 51c43d475f..ced5df4773 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-puppetdb", - "version": "0.2.14-next.1", + "version": "0.2.14", "description": "A Backstage catalog backend module that helps integrate towards PuppetDB", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md index 9c17f17eaa..a654ff5c3d 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md +++ b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-scaffolder-entity-model +## 0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-common@1.7.1 + ## 0.2.12-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/package.json b/plugins/catalog-backend-module-scaffolder-entity-model/package.json index 7019619a6c..beed6b3028 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/package.json +++ b/plugins/catalog-backend-module-scaffolder-entity-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-scaffolder-entity-model", - "version": "0.2.12-next.1", + "version": "0.2.12", "description": "Adds support for the scaffolder specific entity model (e.g. the Template kind) to the catalog backend plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md index ede26e78d1..1c4b8f8363 100644 --- a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md +++ b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-unprocessed +## 0.6.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.6.4-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-unprocessed/package.json b/plugins/catalog-backend-module-unprocessed/package.json index e949240c64..32bcfe904f 100644 --- a/plugins/catalog-backend-module-unprocessed/package.json +++ b/plugins/catalog-backend-module-unprocessed/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-unprocessed", - "version": "0.6.4-next.1", + "version": "0.6.4", "description": "Backstage Catalog module to view unprocessed entities", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 52ebffab31..54be12f4cb 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-catalog-backend +## 3.1.0 + +### Minor Changes + +- 9b40a55: Add support for specifying an entity `spec.type` in `catalog.rules` and `catalog.locations.rules` within the catalog configuration. + + For example, this enables allowing all `Template` entities with the type `website`: + + ```diff + catalog: + rules: + - allow: + - Component + - API + - Resource + - System + - Domain + - Location + + - allow: + + - kind: Template + + spec.type: website + locations: + - type: url + pattern: https://github.com/org/*\/blob/master/*.yaml + ``` + +### Patch Changes + +- 37b4eaf: The 'get-catalog-entity' action now throws a ConflictError instead of generic Error if multiple entities are found, so MCP call doesn't fail with 500. +- 2bbd24f: Order catalog processors by priority. + + This change enables the ordering of catalog processors by their priority, + allowing for more control over the catalog processing sequence. + The default priority is set to 20, and processors can be assigned a custom + priority to influence their execution order. Lower number indicates higher priority. + The priority can be set by implementing the `getPriority` method in the processor class + or by adding a `catalog.processors..priority` configuration + in the `app-config.yaml` file. The configuration takes precedence over the method. + +- e934a27: Updating `catalog:get-catalog-entity` action to be `readOnly` and non destructive +- 0efcc97: Updated generated schemas +- 2204f5b: Prevent deadlock in catalog deferred stitching +- 58874c4: Add support to disable catalog providers and processors via configuration +- a4c82ad: Only run provider orphan cleanup if the engine is started in the first place +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + ## 3.0.2-next.1 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 7a2df294fd..a880cc224f 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "3.0.2-next.1", + "version": "3.1.0", "description": "The Backstage backend plugin that provides the Backstage catalog", "backstage": { "role": "backend-plugin", diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index 5fa578c1ad..8d19916dab 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-graph +## 0.5.0 + +### Minor Changes + +- ae6b606: Support custom relations by using an API to define known relations and which to show by default + Fixes "simplified" bug (#30121) which caused graphs not to be simplified + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + ## 0.4.23-next.2 ### Patch Changes diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 777359e693..86bd930095 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.4.23-next.2", + "version": "0.5.0", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-graph", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index ecd0d8bc46..9c8778e168 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-import +## 0.13.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/integration@1.18.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + - @backstage/integration-react@1.2.10 + ## 0.13.5-next.2 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 39ed5bcef0..97b7283ab9 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.13.5-next.2", + "version": "0.13.5", "description": "A Backstage plugin the helps you import entities into your catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/catalog-node/CHANGELOG.md b/plugins/catalog-node/CHANGELOG.md index 14ac349b6d..ba27f2a2e2 100644 --- a/plugins/catalog-node/CHANGELOG.md +++ b/plugins/catalog-node/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-catalog-node +## 1.19.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- 2bbd24f: Order catalog processors by priority. + + This change enables the ordering of catalog processors by their priority, + allowing for more control over the catalog processing sequence. + The default priority is set to 20, and processors can be assigned a custom + priority to influence their execution order. Lower number indicates higher priority. + The priority can be set by implementing the `getPriority` method in the processor class + or by adding a `catalog.processors..priority` configuration + in the `app-config.yaml` file. The configuration takes precedence over the method. + +- Updated dependencies + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + ## 1.19.0-next.1 ### Minor Changes diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index 3111765897..545656ae80 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-node", - "version": "1.19.0-next.1", + "version": "1.19.0", "description": "The plugin-catalog-node module for @backstage/plugin-catalog-backend", "backstage": { "role": "node-library", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 7ab8dcd967..f5e4d26638 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-catalog-react +## 1.21.0 + +### Minor Changes + +- 0e9ec44: Introduced new `streamEntities` async generator method for the catalog. + + Catalog API and Catalog Service now includes a `streamEntities` method that allows for streaming entities from the catalog. + This method is designed to handle large datasets efficiently by processing entities in a stream rather than loading them + all into memory at once. This is useful when you need to fetch a large number of entities but do not want to use pagination + or fetch all entities at once. + + Example usage: + + ```ts + const pageStream = catalogClient.streamEntities({ pageSize: 100 }, { token }); + for await (const page of pageStream) { + // Handle page of entities + for (const entity of page) { + console.log(entity); + } + } + ``` + +### Patch Changes + +- 0174799: Fix a potential race condition in EntityListProvider when selecting filters +- 4316c11: Catalog table columns support i18n +- 79ff318: Removed the deprecation warning when not passing an explicit type to `EntityCardBlueprint`. Omitting the type is now intended, allowing the layout to pick the default type instead, typically `content`. +- ad0f58d: Support `default*` for older packages as this package is in range for breaking `/alpha` changes +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/frontend-test-utils@0.3.6 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/integration-react@1.2.10 + ## 1.21.0-next.2 ### Minor Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 0b072f51ce..dabfb55d11 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-react", - "version": "1.21.0-next.2", + "version": "1.21.0", "description": "A frontend library that helps other Backstage plugins interact with the catalog", "backstage": { "role": "web-library", diff --git a/plugins/catalog-unprocessed-entities/CHANGELOG.md b/plugins/catalog-unprocessed-entities/CHANGELOG.md index 1542eafba3..31667429ca 100644 --- a/plugins/catalog-unprocessed-entities/CHANGELOG.md +++ b/plugins/catalog-unprocessed-entities/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-unprocessed-entities +## 0.2.21 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + ## 0.2.21-next.1 ### Patch Changes diff --git a/plugins/catalog-unprocessed-entities/package.json b/plugins/catalog-unprocessed-entities/package.json index 004eb96208..9fdd8b5e29 100644 --- a/plugins/catalog-unprocessed-entities/package.json +++ b/plugins/catalog-unprocessed-entities/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-unprocessed-entities", - "version": "0.2.21-next.1", + "version": "0.2.21", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-unprocessed-entities", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index cddfdf18bc..8ae3b4c8da 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-catalog +## 1.31.3 + +### Patch Changes + +- 4316c11: Catalog table columns support i18n +- ce1239e: Auto-focus the first menu item in `EntityContextMenu`, and do not render a divider if an empty array is passed to `UNSTABLE_extraContextMenuItems`. +- 85c5e04: Fix incorrect `defaultTarget` on `createComponentRouteRef`. +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-search-react@1.9.4 + - @backstage/integration-react@1.2.10 + - @backstage/plugin-scaffolder-common@1.7.1 + ## 1.31.3-next.2 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index fd0c1c4375..2a7a956c37 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "1.31.3-next.2", + "version": "1.31.3", "description": "The Backstage plugin for browsing the Backstage catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index 4ecd8c4953..74c7a68e58 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-config-schema +## 0.1.72 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + ## 0.1.72-next.0 ### Patch Changes diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index a1c61537bb..78addfd9c2 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-config-schema", - "version": "0.1.72-next.0", + "version": "0.1.72", "description": "A Backstage plugin that lets you browse the configuration schema of your app", "backstage": { "role": "frontend-plugin", diff --git a/plugins/devtools-backend/CHANGELOG.md b/plugins/devtools-backend/CHANGELOG.md index dbd27d37d9..eeaeda6605 100644 --- a/plugins/devtools-backend/CHANGELOG.md +++ b/plugins/devtools-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-devtools-backend +## 0.5.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/config-loader@1.10.3 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + ## 0.5.9-next.1 ### Patch Changes diff --git a/plugins/devtools-backend/package.json b/plugins/devtools-backend/package.json index 5bd90bdd41..c3892d274f 100644 --- a/plugins/devtools-backend/package.json +++ b/plugins/devtools-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-backend", - "version": "0.5.9-next.1", + "version": "0.5.9", "backstage": { "role": "backend-plugin", "pluginId": "devtools", diff --git a/plugins/devtools/CHANGELOG.md b/plugins/devtools/CHANGELOG.md index 61c2b41fcd..de537019e3 100644 --- a/plugins/devtools/CHANGELOG.md +++ b/plugins/devtools/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-devtools +## 0.1.31 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + ## 0.1.31-next.1 ### Patch Changes diff --git a/plugins/devtools/package.json b/plugins/devtools/package.json index f05ec985d2..9629d30370 100644 --- a/plugins/devtools/package.json +++ b/plugins/devtools/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools", - "version": "0.1.31-next.1", + "version": "0.1.31", "backstage": { "role": "frontend-plugin", "pluginId": "devtools", diff --git a/plugins/events-backend-module-aws-sqs/CHANGELOG.md b/plugins/events-backend-module-aws-sqs/CHANGELOG.md index f390c44909..baca97880c 100644 --- a/plugins/events-backend-module-aws-sqs/CHANGELOG.md +++ b/plugins/events-backend-module-aws-sqs/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-backend-module-aws-sqs +## 0.4.15 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.15-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json index 98fdcec09b..359cd768c8 100644 --- a/plugins/events-backend-module-aws-sqs/package.json +++ b/plugins/events-backend-module-aws-sqs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-aws-sqs", - "version": "0.4.15-next.0", + "version": "0.4.15", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-azure/CHANGELOG.md b/plugins/events-backend-module-azure/CHANGELOG.md index 00d075377e..91791d0966 100644 --- a/plugins/events-backend-module-azure/CHANGELOG.md +++ b/plugins/events-backend-module-azure/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-azure +## 0.2.24 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.24-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-azure/package.json b/plugins/events-backend-module-azure/package.json index 4bc4947de4..3747c2b097 100644 --- a/plugins/events-backend-module-azure/package.json +++ b/plugins/events-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-azure", - "version": "0.2.24-next.0", + "version": "0.2.24", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md index 6b5ea454a3..5014807859 100644 --- a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-bitbucket-cloud +## 0.2.24 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.24-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-bitbucket-cloud/package.json b/plugins/events-backend-module-bitbucket-cloud/package.json index f44a1d5d28..e523b668ca 100644 --- a/plugins/events-backend-module-bitbucket-cloud/package.json +++ b/plugins/events-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-bitbucket-cloud", - "version": "0.2.24-next.0", + "version": "0.2.24", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-bitbucket-server/CHANGELOG.md b/plugins/events-backend-module-bitbucket-server/CHANGELOG.md index ce5a289fa0..8c54c534b3 100644 --- a/plugins/events-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/events-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-bitbucket-server +## 0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + ## 0.1.5-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-bitbucket-server/package.json b/plugins/events-backend-module-bitbucket-server/package.json index 6deaf2e42d..dd4429548c 100644 --- a/plugins/events-backend-module-bitbucket-server/package.json +++ b/plugins/events-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-bitbucket-server", - "version": "0.1.5-next.0", + "version": "0.1.5", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-gerrit/CHANGELOG.md b/plugins/events-backend-module-gerrit/CHANGELOG.md index d0f71a8b2c..511a31488d 100644 --- a/plugins/events-backend-module-gerrit/CHANGELOG.md +++ b/plugins/events-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-gerrit +## 0.2.24 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + ## 0.2.24-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-gerrit/package.json b/plugins/events-backend-module-gerrit/package.json index c736069b00..81978168a7 100644 --- a/plugins/events-backend-module-gerrit/package.json +++ b/plugins/events-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gerrit", - "version": "0.2.24-next.0", + "version": "0.2.24", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-github/CHANGELOG.md b/plugins/events-backend-module-github/CHANGELOG.md index 00a0b957fb..28495671fb 100644 --- a/plugins/events-backend-module-github/CHANGELOG.md +++ b/plugins/events-backend-module-github/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-events-backend-module-github +## 0.4.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.4-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-github/package.json b/plugins/events-backend-module-github/package.json index 2d961b83c3..aebbc6eddf 100644 --- a/plugins/events-backend-module-github/package.json +++ b/plugins/events-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-github", - "version": "0.4.4-next.0", + "version": "0.4.4", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-gitlab/CHANGELOG.md b/plugins/events-backend-module-gitlab/CHANGELOG.md index 174f91dc87..0df6ece6ab 100644 --- a/plugins/events-backend-module-gitlab/CHANGELOG.md +++ b/plugins/events-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-gitlab +## 0.3.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.5-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-gitlab/package.json b/plugins/events-backend-module-gitlab/package.json index c86ea6e8b4..560f4fae76 100644 --- a/plugins/events-backend-module-gitlab/package.json +++ b/plugins/events-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gitlab", - "version": "0.3.5-next.0", + "version": "0.3.5", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-google-pubsub/CHANGELOG.md b/plugins/events-backend-module-google-pubsub/CHANGELOG.md index d789713bd1..6002004dc4 100644 --- a/plugins/events-backend-module-google-pubsub/CHANGELOG.md +++ b/plugins/events-backend-module-google-pubsub/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-backend-module-google-pubsub +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.1.4-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-google-pubsub/package.json b/plugins/events-backend-module-google-pubsub/package.json index fda11585ec..88f6fe714e 100644 --- a/plugins/events-backend-module-google-pubsub/package.json +++ b/plugins/events-backend-module-google-pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-google-pubsub", - "version": "0.1.4-next.0", + "version": "0.1.4", "description": "The google-pubsub backend module for the events plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/events-backend-module-kafka/CHANGELOG.md b/plugins/events-backend-module-kafka/CHANGELOG.md index e005093ef8..f12109da75 100644 --- a/plugins/events-backend-module-kafka/CHANGELOG.md +++ b/plugins/events-backend-module-kafka/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-backend-module-kafka +## 0.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.1.3-next.0 ### Patch Changes diff --git a/plugins/events-backend-module-kafka/package.json b/plugins/events-backend-module-kafka/package.json index 0540029b4e..03a1c6c1dc 100644 --- a/plugins/events-backend-module-kafka/package.json +++ b/plugins/events-backend-module-kafka/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-kafka", - "version": "0.1.3-next.0", + "version": "0.1.3", "description": "The kafka backend module for the events plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/events-backend-test-utils/CHANGELOG.md b/plugins/events-backend-test-utils/CHANGELOG.md index 849e5308b9..5a02e4ed51 100644 --- a/plugins/events-backend-test-utils/CHANGELOG.md +++ b/plugins/events-backend-test-utils/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-events-backend-test-utils +## 0.1.48 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + ## 0.1.48-next.0 ### Patch Changes diff --git a/plugins/events-backend-test-utils/package.json b/plugins/events-backend-test-utils/package.json index db95b057a8..a27e4efbe6 100644 --- a/plugins/events-backend-test-utils/package.json +++ b/plugins/events-backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-test-utils", - "version": "0.1.48-next.0", + "version": "0.1.48", "description": "The plugin-events-backend-test-utils for @backstage/plugin-events-node", "backstage": { "role": "node-library", diff --git a/plugins/events-backend/CHANGELOG.md b/plugins/events-backend/CHANGELOG.md index 2220969cd7..15b1b91730 100644 --- a/plugins/events-backend/CHANGELOG.md +++ b/plugins/events-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-events-backend +## 0.5.6 + +### Patch Changes + +- 0efcc97: Updated generated schemas +- Updated dependencies + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + ## 0.5.6-next.0 ### Patch Changes diff --git a/plugins/events-backend/package.json b/plugins/events-backend/package.json index c16c508989..a5f530f33c 100644 --- a/plugins/events-backend/package.json +++ b/plugins/events-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend", - "version": "0.5.6-next.0", + "version": "0.5.6", "backstage": { "role": "backend-plugin", "pluginId": "events", diff --git a/plugins/events-node/CHANGELOG.md b/plugins/events-node/CHANGELOG.md index 2ae679aa8b..1b706fdf22 100644 --- a/plugins/events-node/CHANGELOG.md +++ b/plugins/events-node/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-node +## 0.4.15 + +### Patch Changes + +- 0efcc97: Updated generated schemas +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.4.15-next.0 ### Patch Changes diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index 536cf246b9..efd879a47a 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-node", - "version": "0.4.15-next.0", + "version": "0.4.15", "description": "The plugin-events-node module for @backstage/plugin-events-backend", "backstage": { "role": "node-library", diff --git a/plugins/example-todo-list-backend/CHANGELOG.md b/plugins/example-todo-list-backend/CHANGELOG.md index 08227bc14a..bd1db36bb8 100644 --- a/plugins/example-todo-list-backend/CHANGELOG.md +++ b/plugins/example-todo-list-backend/CHANGELOG.md @@ -1,5 +1,12 @@ # @internal/plugin-todo-list-backend +## 1.0.43 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + ## 1.0.43-next.0 ### Patch Changes diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json index 8e31279e2a..6775feb352 100644 --- a/plugins/example-todo-list-backend/package.json +++ b/plugins/example-todo-list-backend/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list-backend", - "version": "1.0.43-next.0", + "version": "1.0.43", "backstage": { "role": "backend-plugin", "pluginId": "todo-list", diff --git a/plugins/example-todo-list/CHANGELOG.md b/plugins/example-todo-list/CHANGELOG.md index 205e703446..621859a9ab 100644 --- a/plugins/example-todo-list/CHANGELOG.md +++ b/plugins/example-todo-list/CHANGELOG.md @@ -1,5 +1,13 @@ # @internal/plugin-todo-list +## 1.0.43 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + ## 1.0.43-next.0 ### Patch Changes diff --git a/plugins/example-todo-list/package.json b/plugins/example-todo-list/package.json index 3104980821..f0273e482b 100644 --- a/plugins/example-todo-list/package.json +++ b/plugins/example-todo-list/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list", - "version": "1.0.43-next.0", + "version": "1.0.43", "backstage": { "role": "frontend-plugin", "pluginId": "todo-list", diff --git a/plugins/gateway-backend/CHANGELOG.md b/plugins/gateway-backend/CHANGELOG.md index 7e1fe97014..eabcf7fe66 100644 --- a/plugins/gateway-backend/CHANGELOG.md +++ b/plugins/gateway-backend/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-gateway-backend +## 1.0.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + ## 1.0.5-next.0 ### Patch Changes diff --git a/plugins/gateway-backend/package.json b/plugins/gateway-backend/package.json index 1413086b5b..985f5f9380 100644 --- a/plugins/gateway-backend/package.json +++ b/plugins/gateway-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gateway-backend", - "version": "1.0.5-next.0", + "version": "1.0.5", "backstage": { "role": "backend-plugin", "pluginId": "gateway", diff --git a/plugins/home-react/CHANGELOG.md b/plugins/home-react/CHANGELOG.md index bcd79b94d6..b4fff66b63 100644 --- a/plugins/home-react/CHANGELOG.md +++ b/plugins/home-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-home-react +## 0.1.30 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + ## 0.1.30-next.0 ### Patch Changes diff --git a/plugins/home-react/package.json b/plugins/home-react/package.json index f1605514e0..bce8419b08 100644 --- a/plugins/home-react/package.json +++ b/plugins/home-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home-react", - "version": "0.1.30-next.0", + "version": "0.1.30", "description": "A Backstage plugin that contains react components helps you build a home page", "backstage": { "role": "web-library", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index 1d2709648f..5731706ece 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-home +## 0.8.12 + +### Patch Changes + +- 929c55a: Fixed race condition in CustomHomepageGrid by waiting for storage to load before rendering custom layout to prevent + rendering of the default content. +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + - @backstage/core-app-api@1.19.0 + - @backstage/plugin-home-react@0.1.30 + ## 0.8.12-next.2 ### Patch Changes diff --git a/plugins/home/package.json b/plugins/home/package.json index bc8d71d23b..a348b8eff9 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home", - "version": "0.8.12-next.2", + "version": "0.8.12", "description": "A Backstage plugin that helps you build a home page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 0986e613f8..1ede69fa30 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-kubernetes-backend +## 0.20.2 + +### Patch Changes + +- dd7b6d2: Fix a bug where `getDefault` in the `kubernetesFetcherExtensionPoint` had the wrong `this` value +- 80cf8c9: Fix issue with default objects not being loaded properly +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-kubernetes-node@0.3.4 + - @backstage/plugin-permission-node@0.10.4 + ## 0.20.2-next.2 ### Patch Changes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 15722c373d..6d029eae95 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.20.2-next.2", + "version": "0.20.2", "description": "A Backstage backend plugin that integrates towards Kubernetes", "backstage": { "role": "backend-plugin", diff --git a/plugins/kubernetes-cluster/CHANGELOG.md b/plugins/kubernetes-cluster/CHANGELOG.md index af3a23232d..7f7cf88809 100644 --- a/plugins/kubernetes-cluster/CHANGELOG.md +++ b/plugins/kubernetes-cluster/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-kubernetes-cluster +## 0.0.29 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/plugin-kubernetes-react@0.5.11 + ## 0.0.29-next.2 ### Patch Changes diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json index 0f9ab60401..e789c3edaf 100644 --- a/plugins/kubernetes-cluster/package.json +++ b/plugins/kubernetes-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-cluster", - "version": "0.0.29-next.2", + "version": "0.0.29", "description": "A Backstage plugin that shows details of Kubernetes clusters", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes-node/CHANGELOG.md b/plugins/kubernetes-node/CHANGELOG.md index b0eac9c476..d7366fa90c 100644 --- a/plugins/kubernetes-node/CHANGELOG.md +++ b/plugins/kubernetes-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-kubernetes-node +## 0.3.4 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.4-next.0 ### Patch Changes diff --git a/plugins/kubernetes-node/package.json b/plugins/kubernetes-node/package.json index 23aae77049..2c4c043ec5 100644 --- a/plugins/kubernetes-node/package.json +++ b/plugins/kubernetes-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-node", - "version": "0.3.4-next.0", + "version": "0.3.4", "description": "Node.js library for the kubernetes plugin", "backstage": { "role": "node-library", diff --git a/plugins/kubernetes-react/CHANGELOG.md b/plugins/kubernetes-react/CHANGELOG.md index 973509d5f2..c542907fe6 100644 --- a/plugins/kubernetes-react/CHANGELOG.md +++ b/plugins/kubernetes-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-kubernetes-react +## 0.5.11 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + ## 0.5.11-next.0 ### Patch Changes diff --git a/plugins/kubernetes-react/package.json b/plugins/kubernetes-react/package.json index baa541cc87..82e3d59d8b 100644 --- a/plugins/kubernetes-react/package.json +++ b/plugins/kubernetes-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-react", - "version": "0.5.11-next.0", + "version": "0.5.11", "description": "Web library for the kubernetes-react plugin", "backstage": { "role": "web-library", diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 5a1643287e..627226fee0 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-kubernetes +## 0.12.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-kubernetes-react@0.5.11 + ## 0.12.11-next.2 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index d0b553a4eb..e9da7b47ec 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.12.11-next.2", + "version": "0.12.11", "description": "A Backstage plugin that integrates towards Kubernetes", "backstage": { "role": "frontend-plugin", diff --git a/plugins/mcp-actions-backend/CHANGELOG.md b/plugins/mcp-actions-backend/CHANGELOG.md index e7578dff07..8f079b5dd6 100644 --- a/plugins/mcp-actions-backend/CHANGELOG.md +++ b/plugins/mcp-actions-backend/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-mcp-actions-backend +## 0.1.3 + +### Patch Changes + +- 1d47bf3: Proxy `/.well-known/oauth-authorization-server` to `/.well-known/openid-configuration` on `auth-backend` when `auth.experimentalDynamicClientRegistration.enabled` is enabled. +- 7f2a4a0: Updating docs +- d08b0c9: The MCP backend will now convert known Backstage errors into textual responses with `isError: true`. + The error message can be useful for an LLM to understand and maybe give back to the user. + Previously all errors where thrown out to `@modelcontextprotocol/sdk` which causes a generic 500. +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.1.3-next.1 ### Patch Changes diff --git a/plugins/mcp-actions-backend/package.json b/plugins/mcp-actions-backend/package.json index 4ed2250925..4b8bbd3b72 100644 --- a/plugins/mcp-actions-backend/package.json +++ b/plugins/mcp-actions-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-mcp-actions-backend", - "version": "0.1.3-next.1", + "version": "0.1.3", "backstage": { "role": "backend-plugin", "pluginId": "mcp-actions", diff --git a/plugins/notifications-backend-module-email/CHANGELOG.md b/plugins/notifications-backend-module-email/CHANGELOG.md index 8619edd6f4..1d58fa9727 100644 --- a/plugins/notifications-backend-module-email/CHANGELOG.md +++ b/plugins/notifications-backend-module-email/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-notifications-backend-module-email +## 0.3.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-node@0.2.19 + - @backstage/backend-plugin-api@1.4.3 + ## 0.3.13-next.1 ### Patch Changes diff --git a/plugins/notifications-backend-module-email/package.json b/plugins/notifications-backend-module-email/package.json index 6844a77d3c..ac8682fe2d 100644 --- a/plugins/notifications-backend-module-email/package.json +++ b/plugins/notifications-backend-module-email/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-email", - "version": "0.3.13-next.1", + "version": "0.3.13", "description": "The email backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/notifications-backend-module-slack/CHANGELOG.md b/plugins/notifications-backend-module-slack/CHANGELOG.md index a9d8074e5f..cc69729a3a 100644 --- a/plugins/notifications-backend-module-slack/CHANGELOG.md +++ b/plugins/notifications-backend-module-slack/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-notifications-backend-module-slack +## 0.1.5 + +### Patch Changes + +- a95cebd: Internal refactoring for better type support +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-node@0.2.19 + - @backstage/backend-plugin-api@1.4.3 + ## 0.1.5-next.1 ### Patch Changes diff --git a/plugins/notifications-backend-module-slack/package.json b/plugins/notifications-backend-module-slack/package.json index 6b32e1b31d..4f7e59770e 100644 --- a/plugins/notifications-backend-module-slack/package.json +++ b/plugins/notifications-backend-module-slack/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-slack", - "version": "0.1.5-next.1", + "version": "0.1.5", "description": "The slack backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/notifications-backend/CHANGELOG.md b/plugins/notifications-backend/CHANGELOG.md index 3391980f44..9291e3e550 100644 --- a/plugins/notifications-backend/CHANGELOG.md +++ b/plugins/notifications-backend/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-notifications-backend +## 0.5.10 + +### Patch Changes + +- a95cebd: Internal refactoring for better type support +- 7e7ed57: A new extension point was added that can be used to modify how the users receiving notifications + are resolved. The interface passed to the extension point should only return complete user entity references + based on the notification target references and the excluded entity references. Note that the inputs are lists + of entity references that can be any entity kind, not just user entities. + + Using this extension point will override the default behavior of resolving users with the + `DefaultNotificationRecipientResolver`. + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/plugin-notifications-node@0.2.19 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-signals-node@0.1.24 + ## 0.5.10-next.1 ### Patch Changes diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index a7933b0fc8..1a2c746409 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend", - "version": "0.5.10-next.1", + "version": "0.5.10", "backstage": { "role": "backend-plugin", "pluginId": "notifications", diff --git a/plugins/notifications-node/CHANGELOG.md b/plugins/notifications-node/CHANGELOG.md index 77e35d0996..1db30a23ee 100644 --- a/plugins/notifications-node/CHANGELOG.md +++ b/plugins/notifications-node/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-notifications-node +## 0.2.19 + +### Patch Changes + +- 7e7ed57: A new extension point was added that can be used to modify how the users receiving notifications + are resolved. The interface passed to the extension point should only return complete user entity references + based on the notification target references and the excluded entity references. Note that the inputs are lists + of entity references that can be any entity kind, not just user entities. + + Using this extension point will override the default behavior of resolving users with the + `DefaultNotificationRecipientResolver`. + +- Updated dependencies + - @backstage/catalog-client@1.12.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-signals-node@0.1.24 + ## 0.2.19-next.1 ### Patch Changes diff --git a/plugins/notifications-node/package.json b/plugins/notifications-node/package.json index 234d9d342f..2046692085 100644 --- a/plugins/notifications-node/package.json +++ b/plugins/notifications-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-node", - "version": "0.2.19-next.1", + "version": "0.2.19", "description": "Node.js library for the notifications plugin", "backstage": { "role": "node-library", diff --git a/plugins/notifications/CHANGELOG.md b/plugins/notifications/CHANGELOG.md index 97b280ed47..9ce2086421 100644 --- a/plugins/notifications/CHANGELOG.md +++ b/plugins/notifications/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-notifications +## 0.5.9 + +### Patch Changes + +- 4815b12: Fixed missing app context when rendering the notifications view +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + ## 0.5.9-next.1 ### Patch Changes diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json index 0aa3cde29b..e6d66a5ec7 100644 --- a/plugins/notifications/package.json +++ b/plugins/notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications", - "version": "0.5.9-next.1", + "version": "0.5.9", "backstage": { "role": "frontend-plugin", "pluginId": "notifications", diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md index 6cf5609da0..aee4c9702e 100644 --- a/plugins/org-react/CHANGELOG.md +++ b/plugins/org-react/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-org-react +## 0.1.42 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + ## 0.1.42-next.2 ### Patch Changes diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json index 06c4135396..c177584968 100644 --- a/plugins/org-react/package.json +++ b/plugins/org-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org-react", - "version": "0.1.42-next.2", + "version": "0.1.42", "backstage": { "role": "web-library", "pluginId": "org", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 9df2d654e3..6a62f12664 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-org +## 0.6.44 + +### Patch Changes + +- 22b69f2: Fixing issue with extra slash in the routing +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + ## 0.6.44-next.2 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index 96a4ab9e62..9d1c60072c 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org", - "version": "0.6.44-next.2", + "version": "0.6.44", "description": "A Backstage plugin that helps you create entity pages for your organization", "backstage": { "role": "frontend-plugin", diff --git a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md index 210db3363a..601c110e9d 100644 --- a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md +++ b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-permission-backend-module-allow-all-policy +## 0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + ## 0.2.12-next.0 ### Patch Changes diff --git a/plugins/permission-backend-module-policy-allow-all/package.json b/plugins/permission-backend-module-policy-allow-all/package.json index e741d11560..b20d4f85a6 100644 --- a/plugins/permission-backend-module-policy-allow-all/package.json +++ b/plugins/permission-backend-module-policy-allow-all/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend-module-allow-all-policy", - "version": "0.2.12-next.0", + "version": "0.2.12", "description": "Allow all policy backend module for the permission plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index cc20d3020e..5bcc2ccff7 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-permission-backend +## 0.7.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + ## 0.7.4-next.0 ### Patch Changes diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index 702ed29bd6..434694dc06 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend", - "version": "0.7.4-next.0", + "version": "0.7.4", "backstage": { "role": "backend-plugin", "pluginId": "permission", diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index e0e7e6a026..bcd8dc8be3 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-permission-node +## 0.10.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/backend-plugin-api@1.4.3 + ## 0.10.4-next.0 ### Patch Changes diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 2b325f9cf4..38f9d07cd8 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-node", - "version": "0.10.4-next.0", + "version": "0.10.4", "description": "Common permission and authorization utilities for backend plugins", "backstage": { "role": "node-library", diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index 15f5f02520..fd2683ed4d 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-proxy-backend +## 0.6.6 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-proxy-node@0.1.8 + ## 0.6.6-next.0 ### Patch Changes diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 5413888fca..76501017c5 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-proxy-backend", - "version": "0.6.6-next.0", + "version": "0.6.6", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", "backstage": { "role": "backend-plugin", diff --git a/plugins/proxy-node/CHANGELOG.md b/plugins/proxy-node/CHANGELOG.md index b9d5e5987b..01cc972c66 100644 --- a/plugins/proxy-node/CHANGELOG.md +++ b/plugins/proxy-node/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-proxy-node +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + ## 0.1.8-next.0 ### Patch Changes diff --git a/plugins/proxy-node/package.json b/plugins/proxy-node/package.json index 4ad0c63bff..79453d4c6c 100644 --- a/plugins/proxy-node/package.json +++ b/plugins/proxy-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-proxy-node", - "version": "0.1.8-next.0", + "version": "0.1.8", "description": "The plugin-proxy-node module for @backstage/plugin-proxy-backend", "backstage": { "role": "node-library", diff --git a/plugins/scaffolder-backend-module-azure/CHANGELOG.md b/plugins/scaffolder-backend-module-azure/CHANGELOG.md index 5f00e22a34..fc6d08bf48 100644 --- a/plugins/scaffolder-backend-module-azure/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-azure/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-azure +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-azure/package.json b/plugins/scaffolder-backend-module-azure/package.json index 8dff9c680e..c1db900f79 100644 --- a/plugins/scaffolder-backend-module-azure/package.json +++ b/plugins/scaffolder-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-azure", - "version": "0.2.13-next.0", + "version": "0.2.13", "description": "The azure module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md index 10a2cde00b..cc61bd54fd 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-cloud +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-bitbucket-cloud-common@0.3.2 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json index ae94ed0caa..a68a30da84 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud", - "version": "0.2.13-next.0", + "version": "0.2.13", "description": "The Bitbucket Cloud module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md index 1fe58506b1..ff9e8a84d1 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-server +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket-server/package.json b/plugins/scaffolder-backend-module-bitbucket-server/package.json index 818033a8fd..452653afb7 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket-server", - "version": "0.2.13-next.0", + "version": "0.2.13", "description": "The Bitbucket Server module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md index b9d50d9591..25ff743f3c 100644 --- a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket +## 0.3.14 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.13 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.13 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.3.14-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket/package.json b/plugins/scaffolder-backend-module-bitbucket/package.json index 4b54d49087..b88bfcc259 100644 --- a/plugins/scaffolder-backend-module-bitbucket/package.json +++ b/plugins/scaffolder-backend-module-bitbucket/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket", - "version": "0.3.14-next.0", + "version": "0.3.14", "description": "The bitbucket module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md index 09560b5526..354a075745 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-confluence-to-markdown +## 0.3.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.3.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json index 06a9627649..a708176e47 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown", - "version": "0.3.13-next.0", + "version": "0.3.13", "description": "The confluence-to-markdown module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 8766a54513..24b6ca0dd4 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.3.15-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 32d3ec7be7..6300973618 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", - "version": "0.3.15-next.0", + "version": "0.3.15", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gcp/CHANGELOG.md b/plugins/scaffolder-backend-module-gcp/CHANGELOG.md index 0da8a53d22..e7c99d1075 100644 --- a/plugins/scaffolder-backend-module-gcp/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-gcp +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gcp/package.json b/plugins/scaffolder-backend-module-gcp/package.json index 20ba1f6148..bc3a2cbabb 100644 --- a/plugins/scaffolder-backend-module-gcp/package.json +++ b/plugins/scaffolder-backend-module-gcp/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gcp", - "version": "0.2.13-next.0", + "version": "0.2.13", "description": "The GCP Bucket module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md b/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md index 462bdce34f..c6b2802b91 100644 --- a/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-gerrit +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gerrit/package.json b/plugins/scaffolder-backend-module-gerrit/package.json index 6939ff2cc0..3a4013a54e 100644 --- a/plugins/scaffolder-backend-module-gerrit/package.json +++ b/plugins/scaffolder-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gerrit", - "version": "0.2.13-next.0", + "version": "0.2.13", "description": "The gerrit module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md index 3a166448f7..4c76e53bf5 100644 --- a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-gitea +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index 5ee0625450..acc972343f 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitea", - "version": "0.2.13-next.0", + "version": "0.2.13", "description": "The gitea module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-github/CHANGELOG.md b/plugins/scaffolder-backend-module-github/CHANGELOG.md index 2c396607d5..7f7ba5a597 100644 --- a/plugins/scaffolder-backend-module-github/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-github/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-scaffolder-backend-module-github +## 0.9.0 + +### Minor Changes + +- f0f06b4: Adding a new scaffolder action `github:issues:create` following the reference of `github:issues:label` with `dryRun` testing possibility + + It can be used like this + + ``` + steps: + - id: create-simple-issue + name: Create Simple Issue + action: github:issues:create + input: + repoUrl: ${{ parameters.repoUrl }} + title: "[${{ parameters.projectName }}] Simple Bug Report" + body: | + ## Bug Description + This is a simple bug report created by the scaffolder template. + + ### Steps to Reproduce + 1. Run the application + 2. Navigate to the main page + 3. Click on the problematic button + + ### Expected Behavior + The button should work correctly. + + ### Actual Behavior + The button does not respond to clicks. + output: + links: + - title: Simple Issue + url: ${{ steps['create-simple-issue'].output.issueUrl }} + ``` + +### Patch Changes + +- aee107b: Add `auto_init` option to `github:repo:create` action to create repository with an initial commit containing a README.md file + + This initial commit is created by GitHub itself and the commit is signed, so the repository will not be empty after creation. + + ```diff + - action: github:repo:create + id: init-new-repo + input: + repoUrl: 'github.com?repo=repo&owner=owner' + description: This is the description + visibility: private + + autoInit: true + + ``` + +- 6393b78: Add block creations field in github branch protection scaffolder actions +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.8.3-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index 2d28caf5c6..50fc1646e0 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-github", - "version": "0.8.3-next.1", + "version": "0.9.0", "description": "The github module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md index c6a42b8e6a..df082eb7eb 100644 --- a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-backend-module-gitlab +## 0.9.5 + +### Patch Changes + +- a84ddea: The log message now indicates that the pipeline trigger token was deleted and not pipeline itself. +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.9.5-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index 495563a7c1..f8194a434c 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitlab", - "version": "0.9.5-next.0", + "version": "0.9.5", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend-module-notifications/CHANGELOG.md b/plugins/scaffolder-backend-module-notifications/CHANGELOG.md index 29fe4c33d4..bf28a5d88f 100644 --- a/plugins/scaffolder-backend-module-notifications/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-notifications/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-notifications +## 0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-notifications-node@0.2.19 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.1.14-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-notifications/package.json b/plugins/scaffolder-backend-module-notifications/package.json index d3f0b19e7e..22aaa22396 100644 --- a/plugins/scaffolder-backend-module-notifications/package.json +++ b/plugins/scaffolder-backend-module-notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-notifications", - "version": "0.1.14-next.0", + "version": "0.1.14", "description": "The notifications backend module for the scaffolder plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index afd81c17cd..5970f3f8a2 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.5.13 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.5.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index fb74e346bd..c6f57ae2a4 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", - "version": "0.5.13-next.0", + "version": "0.5.13", "description": "A module for the scaffolder backend that lets you template projects using Rails", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md index ecc41bb537..097b7f38f3 100644 --- a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-scaffolder-backend-module-sentry +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index 492e4cc9e2..55930738b6 100644 --- a/plugins/scaffolder-backend-module-sentry/package.json +++ b/plugins/scaffolder-backend-module-sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-sentry", - "version": "0.2.13-next.0", + "version": "0.2.13", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index 06e1e82c98..9ac2c16a7e 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.4.14 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + - @backstage/plugin-scaffolder-node-test-utils@0.3.3 + ## 0.4.14-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index 0af061c967..e5b60c405d 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-yeoman", - "version": "0.4.14-next.1", + "version": "0.4.14", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 305afa351c..94b1ed5e93 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-scaffolder-backend +## 2.2.1 + +### Patch Changes + +- a57185f: Added support for executing actions from the `ActionsRegistry` in the `scaffolder-backend` +- c3405db: Fixed a regression that prevented uploads greater than 100KB. Uploads up to 10MB are supported again. +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-scaffolder-backend-module-github@0.9.0 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/integration@1.18.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.9.5 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-bitbucket-cloud-common@0.3.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.2.12 + - @backstage/plugin-permission-node@0.10.4 + - @backstage/plugin-scaffolder-backend-module-azure@0.2.13 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.3.14 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.2.13 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.13 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.2.13 + - @backstage/plugin-scaffolder-backend-module-gitea@0.2.13 + - @backstage/plugin-scaffolder-common@1.7.1 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 2.2.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 2e77b90177..53aab2b891 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "2.2.1-next.1", + "version": "2.2.1", "description": "The Backstage backend plugin that helps you create new things", "backstage": { "role": "backend-plugin", diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md index bb407c70b8..2c8133b33c 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-scaffolder-common +## 1.7.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + ## 1.7.1-next.0 ### Patch Changes diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index 0f4af8faf9..63ddd99e9e 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-common", - "version": "1.7.1-next.0", + "version": "1.7.1", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", "backstage": { "role": "common-library", diff --git a/plugins/scaffolder-node-test-utils/CHANGELOG.md b/plugins/scaffolder-node-test-utils/CHANGELOG.md index 6497a73281..d14226a463 100644 --- a/plugins/scaffolder-node-test-utils/CHANGELOG.md +++ b/plugins/scaffolder-node-test-utils/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-node-test-utils +## 0.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@1.9.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-node@0.11.1 + ## 0.3.3-next.1 ### Patch Changes diff --git a/plugins/scaffolder-node-test-utils/package.json b/plugins/scaffolder-node-test-utils/package.json index eb223f4580..8baac6e67f 100644 --- a/plugins/scaffolder-node-test-utils/package.json +++ b/plugins/scaffolder-node-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-node-test-utils", - "version": "0.3.3-next.1", + "version": "0.3.3", "backstage": { "role": "node-library", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-node/CHANGELOG.md b/plugins/scaffolder-node/CHANGELOG.md index 6af39da4a2..c322aa5700 100644 --- a/plugins/scaffolder-node/CHANGELOG.md +++ b/plugins/scaffolder-node/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-node +## 0.11.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-scaffolder-common@1.7.1 + ## 0.11.1-next.0 ### Patch Changes diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index a8a3016e28..a3ae56d3d5 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-node", - "version": "0.11.1-next.0", + "version": "0.11.1", "description": "The plugin-scaffolder-node module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "node-library", diff --git a/plugins/scaffolder-react/CHANGELOG.md b/plugins/scaffolder-react/CHANGELOG.md index 00c948a51b..12061a9561 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-scaffolder-react +## 1.19.1 + +### Patch Changes + +- 58fc108: Fix scaffolder task log stream not having a minimum height +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/plugin-scaffolder-common@1.7.1 + ## 1.19.1-next.2 ### Patch Changes diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index 25c8ea0ec6..f33c6e0a70 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-react", - "version": "1.19.1-next.2", + "version": "1.19.1", "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", "backstage": { "role": "web-library", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 420938d6fd..5b331f64d3 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-scaffolder +## 1.34.1 + +### Patch Changes + +- 0d415ae: Render a TechDocs link on the Scaffolder Template List page when templates include either `backstage.io/techdocs-ref` or `backstage.io/techdocs-entity` annotations, using the shared `buildTechDocsURL` helper. Also adds tests to verify both annotations and optional `backstage.io/techdocs-entity-path` are respected. +- 7151260: Prevent the MultiEntityPicker from removing options present in form state when new options are selected +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/integration@1.18.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-scaffolder-react@1.19.1 + - @backstage/integration-react@1.2.10 + - @backstage/plugin-scaffolder-common@1.7.1 + ## 1.34.1-next.2 ### Patch Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 695afd2244..6190cefbac 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "1.34.1-next.2", + "version": "1.34.1", "description": "The Backstage plugin that helps you create new things", "backstage": { "role": "frontend-plugin", diff --git a/plugins/search-backend-module-catalog/CHANGELOG.md b/plugins/search-backend-module-catalog/CHANGELOG.md index 8f60690872..5267a5f25a 100644 --- a/plugins/search-backend-module-catalog/CHANGELOG.md +++ b/plugins/search-backend-module-catalog/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-search-backend-module-catalog +## 0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + ## 0.3.8-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-catalog/package.json b/plugins/search-backend-module-catalog/package.json index 0ebd03caa6..8db4d6fb96 100644 --- a/plugins/search-backend-module-catalog/package.json +++ b/plugins/search-backend-module-catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-catalog", - "version": "0.3.8-next.1", + "version": "0.3.8", "description": "A module for the search backend that exports catalog modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index 69735daa2b..f9aaad3e06 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 1.7.6 + +### Patch Changes + +- cde70ca: Added support for batchKeyField in the Elasticsearch indexer to allow consistent document IDs during bulk uploads. +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + ## 1.7.6-next.0 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 7e56865e3c..7e87d35fc6 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", - "version": "1.7.6-next.0", + "version": "1.7.6", "description": "A module for the search backend that implements search using ElasticSearch", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-explore/CHANGELOG.md b/plugins/search-backend-module-explore/CHANGELOG.md index f8fcf763a5..644c75f6f7 100644 --- a/plugins/search-backend-module-explore/CHANGELOG.md +++ b/plugins/search-backend-module-explore/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-search-backend-module-explore +## 0.3.7 + +### Patch Changes + +- 9a93520: Deprecate and mark explore collator as moved +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + ## 0.3.7-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-explore/package.json b/plugins/search-backend-module-explore/package.json index 925b489f41..08fe70b093 100644 --- a/plugins/search-backend-module-explore/package.json +++ b/plugins/search-backend-module-explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-explore", - "version": "0.3.7-next.1", + "version": "0.3.7", "description": "A module for the search backend that exports explore modules", "backstage": { "moved": "@backstage-community/plugin-search-backend-module-explore", diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index 48a7c12180..14afeae46a 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-backend-module-pg +## 0.5.48 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + ## 0.5.48-next.0 ### Patch Changes diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 40a56baab1..4a385fffeb 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-pg", - "version": "0.5.48-next.0", + "version": "0.5.48", "description": "A module for the search backend that implements search using PostgreSQL", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md b/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md index 68c7683dda..b5b6802e05 100644 --- a/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md +++ b/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-backend-module-stack-overflow-collator +## 0.3.13 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + ## 0.3.13-next.0 ### Patch Changes diff --git a/plugins/search-backend-module-stack-overflow-collator/package.json b/plugins/search-backend-module-stack-overflow-collator/package.json index 6cf96d8c18..3885882caa 100644 --- a/plugins/search-backend-module-stack-overflow-collator/package.json +++ b/plugins/search-backend-module-stack-overflow-collator/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-stack-overflow-collator", - "version": "0.3.13-next.0", + "version": "0.3.13", "description": "A module for the search backend that exports stack overflow modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-techdocs/CHANGELOG.md b/plugins/search-backend-module-techdocs/CHANGELOG.md index ead9c04a7b..1a952bbb9b 100644 --- a/plugins/search-backend-module-techdocs/CHANGELOG.md +++ b/plugins/search-backend-module-techdocs/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-search-backend-module-techdocs +## 0.4.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-node@1.3.15 + - @backstage/plugin-techdocs-node@1.13.7 + ## 0.4.6-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-techdocs/package.json b/plugins/search-backend-module-techdocs/package.json index d1b784a4d8..89bb5f964c 100644 --- a/plugins/search-backend-module-techdocs/package.json +++ b/plugins/search-backend-module-techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-techdocs", - "version": "0.4.6-next.1", + "version": "0.4.6", "description": "A module for the search backend that exports techdocs modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index 39b21dd29c..f61d4f4318 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-search-backend-node +## 1.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@1.4.3 + ## 1.3.15-next.0 ### Patch Changes diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index bf8375ffe1..e862163a08 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-node", - "version": "1.3.15-next.0", + "version": "1.3.15", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", "backstage": { "role": "node-library", diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index 93b283cc4a..323d97127b 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-search-backend +## 2.0.6 + +### Patch Changes + +- 0efcc97: Updated generated schemas +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/types@1.2.2 + - @backstage/backend-openapi-utils@0.6.1 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-permission-node@0.10.4 + - @backstage/plugin-search-backend-node@1.3.15 + ## 2.0.6-next.0 ### Patch Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index d353a2c079..1802646e5d 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend", - "version": "2.0.6-next.0", + "version": "2.0.6", "description": "The Backstage backend plugin that provides your backstage app with search", "backstage": { "role": "backend-plugin", diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md index 1f68efe604..d8133c742e 100644 --- a/plugins/search-react/CHANGELOG.md +++ b/plugins/search-react/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-search-react +## 1.9.4 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + ## 1.9.4-next.0 ### Patch Changes diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 9345d508c1..2fbfa9267f 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-react", - "version": "1.9.4-next.0", + "version": "1.9.4", "backstage": { "role": "web-library", "pluginId": "search", diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 8e9ca88787..5bd43fd7c4 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-search +## 1.4.30 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-search-react@1.9.4 + ## 1.4.30-next.2 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index b58993b65c..6ecbb32437 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "1.4.30-next.2", + "version": "1.4.30", "description": "The Backstage plugin that provides your backstage app with search", "backstage": { "role": "frontend-plugin", diff --git a/plugins/signals-backend/CHANGELOG.md b/plugins/signals-backend/CHANGELOG.md index ba744b3f13..86eb851f7a 100644 --- a/plugins/signals-backend/CHANGELOG.md +++ b/plugins/signals-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-signals-backend +## 0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-signals-node@0.1.24 + ## 0.3.8-next.0 ### Patch Changes diff --git a/plugins/signals-backend/package.json b/plugins/signals-backend/package.json index 2055b9f2d6..b5de8611df 100644 --- a/plugins/signals-backend/package.json +++ b/plugins/signals-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals-backend", - "version": "0.3.8-next.0", + "version": "0.3.8", "backstage": { "role": "backend-plugin", "pluginId": "signals", diff --git a/plugins/signals-node/CHANGELOG.md b/plugins/signals-node/CHANGELOG.md index 898182f99c..0bdbb32c87 100644 --- a/plugins/signals-node/CHANGELOG.md +++ b/plugins/signals-node/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-signals-node +## 0.1.24 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-node@0.6.7 + - @backstage/plugin-events-node@0.4.15 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + ## 0.1.24-next.0 ### Patch Changes diff --git a/plugins/signals-node/package.json b/plugins/signals-node/package.json index 6aabea9423..907d51c588 100644 --- a/plugins/signals-node/package.json +++ b/plugins/signals-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals-node", - "version": "0.1.24-next.0", + "version": "0.1.24", "description": "Node.js library for the signals plugin", "backstage": { "role": "node-library", diff --git a/plugins/signals/CHANGELOG.md b/plugins/signals/CHANGELOG.md index 9eccc1148f..cb5c18da48 100644 --- a/plugins/signals/CHANGELOG.md +++ b/plugins/signals/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-signals +## 0.0.23 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + ## 0.0.23-next.1 ### Patch Changes diff --git a/plugins/signals/package.json b/plugins/signals/package.json index 23b512c133..c3a3f9ad66 100644 --- a/plugins/signals/package.json +++ b/plugins/signals/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals", - "version": "0.0.23-next.1", + "version": "0.0.23", "backstage": { "role": "frontend-plugin", "pluginId": "signals", diff --git a/plugins/techdocs-addons-test-utils/CHANGELOG.md b/plugins/techdocs-addons-test-utils/CHANGELOG.md index 714fd56c8f..11f82bfe63 100644 --- a/plugins/techdocs-addons-test-utils/CHANGELOG.md +++ b/plugins/techdocs-addons-test-utils/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-techdocs-addons-test-utils +## 1.1.0 + +### Minor Changes + +- 72543e9: Adding catalogApiRef to test-utils to support catalog API usage by TechDocs reader page. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.15.0 + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/core-plugin-api@1.11.0 + - @backstage/plugin-catalog@1.31.3 + - @backstage/core-app-api@1.19.0 + - @backstage/plugin-search-react@1.9.4 + - @backstage/integration-react@1.2.10 + ## 1.0.53-next.2 ### Patch Changes diff --git a/plugins/techdocs-addons-test-utils/package.json b/plugins/techdocs-addons-test-utils/package.json index 94fd865e02..2de0951a9c 100644 --- a/plugins/techdocs-addons-test-utils/package.json +++ b/plugins/techdocs-addons-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-addons-test-utils", - "version": "1.0.53-next.2", + "version": "1.1.0", "backstage": { "role": "web-library", "pluginId": "techdocs-addons", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 98b42c0b84..1234dd5b58 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-techdocs-backend +## 2.1.0 + +### Minor Changes + +- 063cdc5: Techdocs: support HumanDuration for cache TTL and timeout configuration +- a0b604c: Adding new entity that specifies an external entity in the techdocs-entity annotation and updates to documentation regarding TechDocs redirects. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-catalog-node@1.19.0 + - @backstage/catalog-client@1.12.0 + - @backstage/integration@1.18.0 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-search-backend-module-techdocs@0.4.6 + - @backstage/plugin-techdocs-node@1.13.7 + ## 2.1.0-next.1 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index fd1ed4cadf..c9d0218404 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "2.1.0-next.1", + "version": "2.1.0", "description": "The Backstage backend plugin that renders technical documentation for your components", "backstage": { "role": "backend-plugin", diff --git a/plugins/techdocs-module-addons-contrib/CHANGELOG.md b/plugins/techdocs-module-addons-contrib/CHANGELOG.md index 91ae7badb3..d3842b48d6 100644 --- a/plugins/techdocs-module-addons-contrib/CHANGELOG.md +++ b/plugins/techdocs-module-addons-contrib/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-techdocs-module-addons-contrib +## 1.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/integration@1.18.0 + - @backstage/core-components@0.18.0 + - @backstage/integration-react@1.2.10 + ## 1.1.28-next.0 ### Patch Changes diff --git a/plugins/techdocs-module-addons-contrib/package.json b/plugins/techdocs-module-addons-contrib/package.json index 308625938f..12522a0949 100644 --- a/plugins/techdocs-module-addons-contrib/package.json +++ b/plugins/techdocs-module-addons-contrib/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-module-addons-contrib", - "version": "1.1.28-next.0", + "version": "1.1.28", "description": "Plugin module for contributed TechDocs Addons", "backstage": { "role": "frontend-plugin-module", diff --git a/plugins/techdocs-node/CHANGELOG.md b/plugins/techdocs-node/CHANGELOG.md index efcbc4e394..6cc19abea5 100644 --- a/plugins/techdocs-node/CHANGELOG.md +++ b/plugins/techdocs-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-techdocs-node +## 1.13.7 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.18.0 + - @backstage/backend-plugin-api@1.4.3 + ## 1.13.7-next.0 ### Patch Changes diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index c3081d2426..d7ecd3b661 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-node", - "version": "1.13.7-next.0", + "version": "1.13.7", "description": "Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", "backstage": { "role": "node-library", diff --git a/plugins/techdocs-react/CHANGELOG.md b/plugins/techdocs-react/CHANGELOG.md index f594c37f87..d03925b40d 100644 --- a/plugins/techdocs-react/CHANGELOG.md +++ b/plugins/techdocs-react/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-techdocs-react +## 1.3.3 + +### Patch Changes + +- a0b604c: Update to documentation regarding TechDocs redirects. +- Updated dependencies + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + ## 1.3.3-next.0 ### Patch Changes diff --git a/plugins/techdocs-react/package.json b/plugins/techdocs-react/package.json index 0d9082b4d1..13e9dfc66c 100644 --- a/plugins/techdocs-react/package.json +++ b/plugins/techdocs-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-react", - "version": "1.3.3-next.0", + "version": "1.3.3", "description": "Shared frontend utilities for TechDocs and Addons", "backstage": { "role": "web-library", diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 02593351ee..7bdefe0293 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-techdocs +## 1.15.0 + +### Minor Changes + +- a0b604c: Adding redirect handling for TechDocs URLs that reference entities that now reference an external entity for TechDocs. Including tests and documentation. + +### Patch Changes + +- 313cec7: Updated dependency `dompurify` to `^3.2.4`. +- 8d18d23: TechDocs page titles have been improved, especially for deeply nested pages. +- 1dfee19: Reverts a change in CSS layout that shifted the content of the Techdocs too far to the left. +- 4ce5831: Support Techdocs redirect with dompurify 3.2.6+ +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/plugin-techdocs-react@1.3.3 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/plugin-auth-react@0.1.19 + - @backstage/core-plugin-api@1.11.0 + - @backstage/catalog-client@1.12.0 + - @backstage/integration@1.18.0 + - @backstage/core-components@0.18.0 + - @backstage/core-compat-api@0.5.2 + - @backstage/plugin-search-react@1.9.4 + - @backstage/integration-react@1.2.10 + ## 1.14.2-next.2 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 7777133862..7f03dd32f5 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "1.14.2-next.2", + "version": "1.15.0", "description": "The Backstage plugin that renders technical documentation for your components", "backstage": { "role": "frontend-plugin", diff --git a/plugins/user-settings-backend/CHANGELOG.md b/plugins/user-settings-backend/CHANGELOG.md index 6b69021f4c..20a635d42e 100644 --- a/plugins/user-settings-backend/CHANGELOG.md +++ b/plugins/user-settings-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-user-settings-backend +## 0.3.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.12.1 + - @backstage/plugin-auth-node@0.6.7 + - @backstage/types@1.2.2 + - @backstage/backend-plugin-api@1.4.3 + - @backstage/plugin-signals-node@0.1.24 + ## 0.3.6-next.0 ### Patch Changes diff --git a/plugins/user-settings-backend/package.json b/plugins/user-settings-backend/package.json index 16ccb92984..f1f8a3ba68 100644 --- a/plugins/user-settings-backend/package.json +++ b/plugins/user-settings-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings-backend", - "version": "0.3.6-next.0", + "version": "0.3.6", "description": "The Backstage backend plugin to manage user settings", "backstage": { "role": "backend-plugin", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index cfb4c0b712..fba325d67f 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-user-settings +## 0.8.26 + +### Patch Changes + +- 320a9ac: Add the OpenShift authenticator provider to the default `user-settings` providers page. +- b713b54: Tool-tip text correction for the Theme selection in settings page +- Updated dependencies + - @backstage/plugin-catalog-react@1.21.0 + - @backstage/frontend-plugin-api@0.12.0 + - @backstage/core-plugin-api@1.11.0 + - @backstage/core-components@0.18.0 + - @backstage/types@1.2.2 + - @backstage/core-compat-api@0.5.2 + - @backstage/core-app-api@1.19.0 + ## 0.8.26-next.2 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 3ce96f9146..1ec4ac7072 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings", - "version": "0.8.26-next.2", + "version": "0.8.26", "description": "A Backstage plugin that provides a settings page", "backstage": { "role": "frontend-plugin", From 5998ce722b00e25ba5a85cf1de9bf8591ff02e31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:59:34 +0000 Subject: [PATCH 225/233] chore(deps): bump next from 15.3.4 to 15.4.7 in /docs-ui Bumps [next](https://github.com/vercel/next.js) from 15.3.4 to 15.4.7. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/compare/v15.3.4...v15.4.7) --- updated-dependencies: - dependency-name: next dependency-version: 15.4.7 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- docs-ui/package.json | 2 +- docs-ui/yarn.lock | 115 +++++++++++++++++-------------------------- 2 files changed, 46 insertions(+), 71 deletions(-) diff --git a/docs-ui/package.json b/docs-ui/package.json index 71db880859..50bcae8d72 100644 --- a/docs-ui/package.json +++ b/docs-ui/package.json @@ -28,7 +28,7 @@ "@uiw/react-codemirror": "^4.23.7", "html-react-parser": "^5.2.5", "motion": "^12.4.1", - "next": "15.3.4", + "next": "15.4.7", "next-mdx-remote-client": "^2.1.2", "prop-types": "^15.8.1", "react": "19.1.1", diff --git a/docs-ui/yarn.lock b/docs-ui/yarn.lock index 0a76d117be..0969bf0ffa 100644 --- a/docs-ui/yarn.lock +++ b/docs-ui/yarn.lock @@ -774,10 +774,10 @@ __metadata: languageName: node linkType: hard -"@next/env@npm:15.3.4": - version: 15.3.4 - resolution: "@next/env@npm:15.3.4" - checksum: 10/40ea0bee2eca72dce6102d30ac50029309bf68a281585270e38f920ae47043f240352ad250058d462169a436dea3d8f78a935406ee61088bd4a614524b4a315b +"@next/env@npm:15.4.7": + version: 15.4.7 + resolution: "@next/env@npm:15.4.7" + checksum: 10/8341fa6f1b5aebb6f99d8abad2f3ee2685281aaa51785f8ceff3c9a7f03186645f372003ee833715c0ce05e4e7d960defa4252018977f19564001b872b24714e languageName: node linkType: hard @@ -807,58 +807,58 @@ __metadata: languageName: node linkType: hard -"@next/swc-darwin-arm64@npm:15.3.4": - version: 15.3.4 - resolution: "@next/swc-darwin-arm64@npm:15.3.4" +"@next/swc-darwin-arm64@npm:15.4.7": + version: 15.4.7 + resolution: "@next/swc-darwin-arm64@npm:15.4.7" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@next/swc-darwin-x64@npm:15.3.4": - version: 15.3.4 - resolution: "@next/swc-darwin-x64@npm:15.3.4" +"@next/swc-darwin-x64@npm:15.4.7": + version: 15.4.7 + resolution: "@next/swc-darwin-x64@npm:15.4.7" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@next/swc-linux-arm64-gnu@npm:15.3.4": - version: 15.3.4 - resolution: "@next/swc-linux-arm64-gnu@npm:15.3.4" +"@next/swc-linux-arm64-gnu@npm:15.4.7": + version: 15.4.7 + resolution: "@next/swc-linux-arm64-gnu@npm:15.4.7" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:15.3.4": - version: 15.3.4 - resolution: "@next/swc-linux-arm64-musl@npm:15.3.4" +"@next/swc-linux-arm64-musl@npm:15.4.7": + version: 15.4.7 + resolution: "@next/swc-linux-arm64-musl@npm:15.4.7" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:15.3.4": - version: 15.3.4 - resolution: "@next/swc-linux-x64-gnu@npm:15.3.4" +"@next/swc-linux-x64-gnu@npm:15.4.7": + version: 15.4.7 + resolution: "@next/swc-linux-x64-gnu@npm:15.4.7" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:15.3.4": - version: 15.3.4 - resolution: "@next/swc-linux-x64-musl@npm:15.3.4" +"@next/swc-linux-x64-musl@npm:15.4.7": + version: 15.4.7 + resolution: "@next/swc-linux-x64-musl@npm:15.4.7" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:15.3.4": - version: 15.3.4 - resolution: "@next/swc-win32-arm64-msvc@npm:15.3.4" +"@next/swc-win32-arm64-msvc@npm:15.4.7": + version: 15.4.7 + resolution: "@next/swc-win32-arm64-msvc@npm:15.4.7" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:15.3.4": - version: 15.3.4 - resolution: "@next/swc-win32-x64-msvc@npm:15.3.4" +"@next/swc-win32-x64-msvc@npm:15.4.7": + version: 15.4.7 + resolution: "@next/swc-win32-x64-msvc@npm:15.4.7" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -1122,13 +1122,6 @@ __metadata: languageName: node linkType: hard -"@swc/counter@npm:0.1.3": - version: 0.1.3 - resolution: "@swc/counter@npm:0.1.3" - checksum: 10/df8f9cfba9904d3d60f511664c70d23bb323b3a0803ec9890f60133954173047ba9bdeabce28cd70ba89ccd3fd6c71c7b0bd58be85f611e1ffbe5d5c18616598 - languageName: node - linkType: hard - "@swc/helpers@npm:0.5.15": version: 0.5.15 resolution: "@swc/helpers@npm:0.5.15" @@ -1916,15 +1909,6 @@ __metadata: languageName: node linkType: hard -"busboy@npm:1.6.0": - version: 1.6.0 - resolution: "busboy@npm:1.6.0" - dependencies: - streamsearch: "npm:^1.1.0" - checksum: 10/bee10fa10ea58e7e3e7489ffe4bda6eacd540a17de9f9cd21cc37e297b2dd9fe52b2715a5841afaec82900750d810d01d7edb4b2d456427f449b92b417579763 - languageName: node - linkType: hard - "cacache@npm:^19.0.1": version: 19.0.1 resolution: "cacache@npm:19.0.1" @@ -2360,7 +2344,7 @@ __metadata: html-react-parser: "npm:^5.2.5" lightningcss: "npm:^1.28.2" motion: "npm:^12.4.1" - next: "npm:15.3.4" + next: "npm:15.4.7" next-mdx-remote-client: "npm:^2.1.2" prop-types: "npm:^15.8.1" react: "npm:19.1.1" @@ -5179,29 +5163,27 @@ __metadata: languageName: node linkType: hard -"next@npm:15.3.4": - version: 15.3.4 - resolution: "next@npm:15.3.4" +"next@npm:15.4.7": + version: 15.4.7 + resolution: "next@npm:15.4.7" dependencies: - "@next/env": "npm:15.3.4" - "@next/swc-darwin-arm64": "npm:15.3.4" - "@next/swc-darwin-x64": "npm:15.3.4" - "@next/swc-linux-arm64-gnu": "npm:15.3.4" - "@next/swc-linux-arm64-musl": "npm:15.3.4" - "@next/swc-linux-x64-gnu": "npm:15.3.4" - "@next/swc-linux-x64-musl": "npm:15.3.4" - "@next/swc-win32-arm64-msvc": "npm:15.3.4" - "@next/swc-win32-x64-msvc": "npm:15.3.4" - "@swc/counter": "npm:0.1.3" + "@next/env": "npm:15.4.7" + "@next/swc-darwin-arm64": "npm:15.4.7" + "@next/swc-darwin-x64": "npm:15.4.7" + "@next/swc-linux-arm64-gnu": "npm:15.4.7" + "@next/swc-linux-arm64-musl": "npm:15.4.7" + "@next/swc-linux-x64-gnu": "npm:15.4.7" + "@next/swc-linux-x64-musl": "npm:15.4.7" + "@next/swc-win32-arm64-msvc": "npm:15.4.7" + "@next/swc-win32-x64-msvc": "npm:15.4.7" "@swc/helpers": "npm:0.5.15" - busboy: "npm:1.6.0" caniuse-lite: "npm:^1.0.30001579" postcss: "npm:8.4.31" - sharp: "npm:^0.34.1" + sharp: "npm:^0.34.3" styled-jsx: "npm:5.1.6" peerDependencies: "@opentelemetry/api": ^1.1.0 - "@playwright/test": ^1.41.2 + "@playwright/test": ^1.51.1 babel-plugin-react-compiler: "*" react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 @@ -5236,7 +5218,7 @@ __metadata: optional: true bin: next: dist/bin/next - checksum: 10/bcc781ca5abba6a29408431f81b57598708871796fe4c70d1187cc92ca4905467a6a3cde32840cb7911099d54a93c1543c130e4dea2b59dbf9753f5850a8efdf + checksum: 10/e611751247d5cfff9337a84979918bcd61919d7b66a411c982569336c3dba7e7a8704eead7e082e579807200fa8bc80602a3e2ce4423a8b5387aab3353400918 languageName: node linkType: hard @@ -6059,7 +6041,7 @@ __metadata: languageName: node linkType: hard -"sharp@npm:^0.34.1": +"sharp@npm:^0.34.3": version: 0.34.3 resolution: "sharp@npm:0.34.3" dependencies: @@ -6347,13 +6329,6 @@ __metadata: languageName: node linkType: hard -"streamsearch@npm:^1.1.0": - version: 1.1.0 - resolution: "streamsearch@npm:1.1.0" - checksum: 10/612c2b2a7dbcc859f74597112f80a42cbe4d448d03da790d5b7b39673c1197dd3789e91cd67210353e58857395d32c1e955a9041c4e6d5bae723436b3ed9ed14 - languageName: node - linkType: hard - "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" From 00707cf3d94d11de3a664163c23fc557c9b07819 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 14:19:49 +0000 Subject: [PATCH 226/233] chore(deps-dev): bump axios from 1.11.0 to 1.12.0 Bumps [axios](https://github.com/axios/axios) from 1.11.0 to 1.12.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.11.0...v1.12.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.12.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- packages/app/package.json | 2 +- yarn.lock | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index cb8fdbd9df..90ffcd0b3b 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -94,7 +94,7 @@ "@types/react": "*", "@types/react-dom": "*", "@types/zen-observable": "^0.8.0", - "axios": "^1.11.0", + "axios": "^1.12.0", "cross-env": "^7.0.0", "msw": "^1.0.0" }, diff --git a/yarn.lock b/yarn.lock index 3a5aa9ac30..a8d806f0d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24130,7 +24130,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.0.0, axios@npm:^1.11.0, axios@npm:^1.6.0, axios@npm:^1.7.4, axios@npm:^1.8.3": +"axios@npm:^1.0.0, axios@npm:^1.6.0, axios@npm:^1.7.4, axios@npm:^1.8.3": version: 1.11.0 resolution: "axios@npm:1.11.0" dependencies: @@ -24141,6 +24141,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.12.0": + version: 1.12.2 + resolution: "axios@npm:1.12.2" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.4" + proxy-from-env: "npm:^1.1.0" + checksum: 10/886a79770594eaad76493fecf90344b567bd956240609b5dcd09bd0afe8d3e6f1ad6d3257a93a483b6192b409d4b673d9515a34619e3e3ed1b2c0ec2a83b20ba + languageName: node + linkType: hard + "axobject-query@npm:^4.1.0": version: 4.1.0 resolution: "axobject-query@npm:4.1.0" @@ -29707,7 +29718,7 @@ __metadata: "@types/react": "npm:*" "@types/react-dom": "npm:*" "@types/zen-observable": "npm:^0.8.0" - axios: "npm:^1.11.0" + axios: "npm:^1.12.0" cross-env: "npm:^7.0.0" history: "npm:^5.0.0" msw: "npm:^1.0.0" From f8091fe083b181e62e37b62db4fc2d9f7b38cd02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 16 Sep 2025 16:44:55 +0200 Subject: [PATCH 227/233] add release notes for 1.43.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- docs/releases/v1.43.0.md | 90 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 docs/releases/v1.43.0.md diff --git a/docs/releases/v1.43.0.md b/docs/releases/v1.43.0.md new file mode 100644 index 0000000000..74169e6a2a --- /dev/null +++ b/docs/releases/v1.43.0.md @@ -0,0 +1,90 @@ +--- +id: v1.43.0 +title: v1.43.0 +description: Backstage Release v1.43.0 +--- + +These are the release notes for the v1.43.0 release of [Backstage](https://backstage.io/). + +A huge thanks to the whole team of maintainers and contributors as well as the amazing Backstage Community for the hard work in getting this release developed and done. + +## Highlights + +### **BREAKING:** MSgraph catalog module encoding queries + +The catalog MSgraph module now properly encodes filter queries before they are sent to the remote system. This fixes some confusion in how to properly write queries–and thus simplifies operations for users. However, it does mean that if you use the `@backstage/plugin-catalog-backend-module-msgraph` module, you will have to update your `filter` parameters to be just plain text without any manual encoding. Example: + +```diff + user: +- filter: department in('MARKETING', 'R %26 D') ++ filter: department in('MARKETING', 'R & D') +``` + +Contributed by [@stephenglass](https://github.com/stephenglass) in [#30567](https://github.com/backstage/backstage/pull/30567) + +### OpenShift auth + +Backstage now supports OpenShift as an auth provider! This was a long standing PR that we are very excited to have merged into the project. Go [check out the docs](https://backstage.io/docs/auth/openshift/provider) for it! + +Contributed by [@ioboi](https://github.com/ioboi) in [#29470](https://github.com/backstage/backstage/pull/29470) + +### Improved app error display for new frontend system + +If the frontend app encounters one or more critical errors during startup they are now displayed on an error page that summarizes the errors and their source. This is in contrast to before this change, where errors would result in a blank page with the first error logged to the console. + +Some more errors have also been moved to be warnings, which will be logged in the console regardless of whether the app starts up or not. + +### Actions Registry integration with the Scaffolder + +Actions which are registered in the Actions Registry are now also available in Scaffolder templates. You will be able to browse the actions and see the schema like the original Scaffolder Templating Actions, as well as being able to use `action: acme:my-action` inside template steps to call these distributed actions. + +### _Experimental_: Dynamic Client Registration support for MCP Actions Backend + +We’ve shipped experimental support for allowing the `auth-backend` to issue tokens on behalf of users for use in MCP Clients such as Claude and Cursor. This is highly experimental, and tokens are only valid for 1 hour currently. We’re eager to get some feedback on this feature, so if you would like to try it out you can check out the [mcp-actions docs](https://github.com/backstage/backstage/blob/master/plugins/mcp-actions-backend/README.md#experimental-dynamic-client-registration) and leave us some feedback in a [GitHub issue](https://github.com/backstage/backstage/issues/new), or on our [Community Discord](https://discord.gg/backstage-687207715902193673)! + +### Introducing `BACKSTAGE_ENV` + +The Backstage configuration system now pays attention to the `BACKSTAGE_ENV` environment variable, and uses it to locate config files to load without you having to manually specify `--config` flags at startup. This makes it even smoother to leverage e.g. `app-config.production.yaml` files in your production environment. + +Check out [the updated documentation](https://backstage.io/docs/conf/) for more information. + +Contributed by [@drodil](https://github.com/drodil) in [#30722](https://github.com/backstage/backstage/pull/30722) + +### Backend logger configuration + +It is now possible to configure log level, meta, and log level overrides through static configuration. This allows you to raise or lower the log level for individual plugins or services. But it also allows you to match on any log field using either exact matches or regular expressions. Read more about the new configuration options in the [Root Logger Service](https://backstage.io/docs/backend-system/core-services/root-logger/) documentation. + +Contributed by [@tcardonne](https://github.com/tcardonne) in [#30727](https://github.com/backstage/backstage/pull/30727) + +### Toggling and priorities for catalog features + +You are now able to enable/disable individual processors and providers in your catalog, directly through your app config. You can also affect the order in which processors run, by assigning them priorities. + +Contributed by [@drodil](https://github.com/drodil) in [#31034](https://github.com/backstage/backstage/pull/31034) and [#30969](https://github.com/backstage/backstage/pull/30969) + +### Moved the Explore search module + +Just a quick note, that the search collator module for the `explore` plugin was moved to the community repository, where it fits snugly next to the plugin itself. If you are using the yarn plugin, these references should be updated automatically for you. But if you do not, just update your dependencies on `@backstage/plugin-search-backend-module-explore`, to `@backstage-community/plugin-search-backend-module-explore`. + +## Security Fixes + +This release does not contain any security fixes. However, it is worth calling out that OAuth cookies issued by the auth backend have changed from domain-scoped to host-only. + +Contributed by [@JessicaJHee](https://github.com/JessicaJHee) in [#30922](https://github.com/backstage/backstage/pull/30922) + +## Upgrade path + +We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated). + +## Links and References + +Below you can find a list of links and references to help you learn about and start using this new release. + +- [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) +- [GitHub repository](https://github.com/backstage/backstage) +- Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support +- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.1.0-changelog.md) +- Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins) + +Sign up for our [newsletter](https://info.backstage.spotify.com/newsletter_subscribe) if you want to be informed about what is happening in the world of Backstage. From 4dc9aeba6fc1ca1c6a83c27af9c07b03cfc8679d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 16 Sep 2025 16:48:53 +0200 Subject: [PATCH 228/233] dedupe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- yarn.lock | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index a8d806f0d5..c5918258b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24130,18 +24130,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.0.0, axios@npm:^1.6.0, axios@npm:^1.7.4, axios@npm:^1.8.3": - version: 1.11.0 - resolution: "axios@npm:1.11.0" - dependencies: - follow-redirects: "npm:^1.15.6" - form-data: "npm:^4.0.4" - proxy-from-env: "npm:^1.1.0" - checksum: 10/232df4af7a4e4e07baa84621b9cc4b0c518a757b4eacc7f635c0eb3642cb98dff347326739f24b891b3b4481b7b838c79a3a0c4819c9fbc1fc40232431b9c5dc - languageName: node - linkType: hard - -"axios@npm:^1.12.0": +"axios@npm:^1.0.0, axios@npm:^1.12.0, axios@npm:^1.6.0, axios@npm:^1.7.4, axios@npm:^1.8.3": version: 1.12.2 resolution: "axios@npm:1.12.2" dependencies: From 75ed96cd6e92c6185660cbfaabb6998cd67153e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 16 Sep 2025 16:50:08 +0200 Subject: [PATCH 229/233] dedupe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- yarn.lock | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/yarn.lock b/yarn.lock index b34a88fa24..38ac0975af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30275,18 +30275,6 @@ __metadata: languageName: node linkType: hard -"fdir@npm:^6.4.4": - version: 6.4.6 - resolution: "fdir@npm:6.4.6" - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - checksum: 10/c186ba387e7b75ccf874a098d9bc5fe0af0e9c52fc56f8eac8e80aa4edb65532684bf2bf769894ff90f53bf221d6136692052d31f07a9952807acae6cbe7ee50 - languageName: node - linkType: hard - "fdir@npm:^6.5.0": version: 6.5.0 resolution: "fdir@npm:6.5.0" @@ -46860,7 +46848,7 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.15": +"tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.9": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: @@ -46870,16 +46858,6 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.9": - version: 0.2.14 - resolution: "tinyglobby@npm:0.2.14" - dependencies: - fdir: "npm:^6.4.4" - picomatch: "npm:^4.0.2" - checksum: 10/3d306d319718b7cc9d79fb3f29d8655237aa6a1f280860a217f93417039d0614891aee6fc47c5db315f4fcc6ac8d55eb8e23e2de73b2c51a431b42456d9e5764 - languageName: node - linkType: hard - "tinylogic@npm:^2.0.0": version: 2.0.0 resolution: "tinylogic@npm:2.0.0" From 5921a043c03506d9e050ef2e3ff8b129573332c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 16 Sep 2025 16:57:11 +0200 Subject: [PATCH 230/233] enter pre MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/pre.json | 209 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000..12e4285e7c --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,209 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "example-app": "0.2.113", + "@backstage/app-defaults": "1.7.0", + "example-app-next": "0.0.27", + "app-next-example-plugin": "0.0.27", + "example-backend": "0.0.42", + "@backstage/backend-app-api": "1.2.7", + "@backstage/backend-defaults": "0.12.1", + "@backstage/backend-dev-utils": "0.1.5", + "@backstage/backend-dynamic-feature-service": "0.7.4", + "@backstage/backend-openapi-utils": "0.6.1", + "@backstage/backend-plugin-api": "1.4.3", + "@backstage/backend-test-utils": "1.9.0", + "@backstage/catalog-client": "1.12.0", + "@backstage/catalog-model": "1.7.5", + "@backstage/cli": "0.34.2", + "@backstage/cli-common": "0.1.15", + "@backstage/cli-node": "0.2.14", + "@backstage/codemods": "0.1.52", + "@backstage/config": "1.3.3", + "@backstage/config-loader": "1.10.3", + "@backstage/core-app-api": "1.19.0", + "@backstage/core-compat-api": "0.5.2", + "@backstage/core-components": "0.18.0", + "@backstage/core-plugin-api": "1.11.0", + "@backstage/create-app": "0.7.4", + "@backstage/dev-utils": "1.1.14", + "e2e-test": "0.2.32", + "@backstage/e2e-test-utils": "0.1.1", + "@backstage/errors": "1.2.7", + "@backstage/eslint-plugin": "0.1.11", + "@backstage/frontend-app-api": "0.13.0", + "@backstage/frontend-defaults": "0.3.1", + "@backstage/frontend-dynamic-feature-loader": "0.1.5", + "@internal/frontend": "0.0.13", + "@backstage/frontend-plugin-api": "0.12.0", + "@backstage/frontend-test-utils": "0.3.6", + "@backstage/integration": "1.18.0", + "@backstage/integration-aws-node": "0.1.17", + "@backstage/integration-react": "1.2.10", + "@internal/opaque": "0.0.1", + "@backstage/release-manifests": "0.0.13", + "@backstage/repo-tools": "0.15.2", + "@internal/scaffolder": "0.0.13", + "@techdocs/cli": "1.9.8", + "techdocs-cli-embedded-app": "0.2.112", + "@backstage/test-utils": "1.7.11", + "@backstage/theme": "0.6.8", + "@backstage/types": "1.2.2", + "@backstage/ui": "0.7.1", + "@backstage/version-bridge": "1.0.11", + "yarn-plugin-backstage": "0.0.7", + "@backstage/plugin-api-docs": "0.12.11", + "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.10", + "@backstage/plugin-app": "0.3.0", + "@backstage/plugin-app-backend": "0.5.6", + "@backstage/plugin-app-node": "0.1.37", + "@backstage/plugin-app-visualizer": "0.1.23", + "@backstage/plugin-auth": "0.1.0", + "@backstage/plugin-auth-backend": "0.25.4", + "@backstage/plugin-auth-backend-module-atlassian-provider": "0.4.7", + "@backstage/plugin-auth-backend-module-auth0-provider": "0.2.7", + "@backstage/plugin-auth-backend-module-aws-alb-provider": "0.4.7", + "@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.2.12", + "@backstage/plugin-auth-backend-module-bitbucket-provider": "0.3.7", + "@backstage/plugin-auth-backend-module-bitbucket-server-provider": "0.2.7", + "@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.4.7", + "@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.4.7", + "@backstage/plugin-auth-backend-module-github-provider": "0.3.7", + "@backstage/plugin-auth-backend-module-gitlab-provider": "0.3.7", + "@backstage/plugin-auth-backend-module-google-provider": "0.3.7", + "@backstage/plugin-auth-backend-module-guest-provider": "0.2.12", + "@backstage/plugin-auth-backend-module-microsoft-provider": "0.3.7", + "@backstage/plugin-auth-backend-module-oauth2-provider": "0.4.7", + "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.2.12", + "@backstage/plugin-auth-backend-module-oidc-provider": "0.4.7", + "@backstage/plugin-auth-backend-module-okta-provider": "0.2.7", + "@backstage/plugin-auth-backend-module-onelogin-provider": "0.3.7", + "@backstage/plugin-auth-backend-module-openshift-provider": "0.1.0", + "@backstage/plugin-auth-backend-module-pinniped-provider": "0.3.7", + "@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.5.7", + "@backstage/plugin-auth-node": "0.6.7", + "@backstage/plugin-auth-react": "0.1.19", + "@backstage/plugin-bitbucket-cloud-common": "0.3.2", + "@backstage/plugin-catalog": "1.31.3", + "@backstage/plugin-catalog-backend": "3.1.0", + "@backstage/plugin-catalog-backend-module-aws": "0.4.15", + "@backstage/plugin-catalog-backend-module-azure": "0.3.9", + "@backstage/plugin-catalog-backend-module-backstage-openapi": "0.5.6", + "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.5.3", + "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.5.3", + "@backstage/plugin-catalog-backend-module-gcp": "0.3.12", + "@backstage/plugin-catalog-backend-module-gerrit": "0.3.6", + "@backstage/plugin-catalog-backend-module-gitea": "0.1.4", + "@backstage/plugin-catalog-backend-module-github": "0.11.0", + "@backstage/plugin-catalog-backend-module-github-org": "0.3.14", + "@backstage/plugin-catalog-backend-module-gitlab": "0.7.3", + "@backstage/plugin-catalog-backend-module-gitlab-org": "0.2.13", + "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.7.4", + "@backstage/plugin-catalog-backend-module-ldap": "0.11.9", + "@backstage/plugin-catalog-backend-module-logs": "0.1.14", + "@backstage/plugin-catalog-backend-module-msgraph": "0.8.0", + "@backstage/plugin-catalog-backend-module-openapi": "0.2.14", + "@backstage/plugin-catalog-backend-module-puppetdb": "0.2.14", + "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.2.12", + "@backstage/plugin-catalog-backend-module-unprocessed": "0.6.4", + "@backstage/plugin-catalog-common": "1.1.5", + "@backstage/plugin-catalog-graph": "0.5.0", + "@backstage/plugin-catalog-import": "0.13.5", + "@backstage/plugin-catalog-node": "1.19.0", + "@backstage/plugin-catalog-react": "1.21.0", + "@backstage/plugin-catalog-unprocessed-entities": "0.2.21", + "@backstage/plugin-catalog-unprocessed-entities-common": "0.0.9", + "@backstage/plugin-config-schema": "0.1.72", + "@backstage/plugin-devtools": "0.1.31", + "@backstage/plugin-devtools-backend": "0.5.9", + "@backstage/plugin-devtools-common": "0.1.17", + "@backstage/plugin-events-backend": "0.5.6", + "@backstage/plugin-events-backend-module-aws-sqs": "0.4.15", + "@backstage/plugin-events-backend-module-azure": "0.2.24", + "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.24", + "@backstage/plugin-events-backend-module-bitbucket-server": "0.1.5", + "@backstage/plugin-events-backend-module-gerrit": "0.2.24", + "@backstage/plugin-events-backend-module-github": "0.4.4", + "@backstage/plugin-events-backend-module-gitlab": "0.3.5", + "@backstage/plugin-events-backend-module-google-pubsub": "0.1.4", + "@backstage/plugin-events-backend-module-kafka": "0.1.3", + "@backstage/plugin-events-backend-test-utils": "0.1.48", + "@backstage/plugin-events-node": "0.4.15", + "@internal/plugin-todo-list": "1.0.43", + "@internal/plugin-todo-list-backend": "1.0.43", + "@internal/plugin-todo-list-common": "1.0.26", + "@backstage/plugin-gateway-backend": "1.0.5", + "@backstage/plugin-home": "0.8.12", + "@backstage/plugin-home-react": "0.1.30", + "@backstage/plugin-kubernetes": "0.12.11", + "@backstage/plugin-kubernetes-backend": "0.20.2", + "@backstage/plugin-kubernetes-cluster": "0.0.29", + "@backstage/plugin-kubernetes-common": "0.9.6", + "@backstage/plugin-kubernetes-node": "0.3.4", + "@backstage/plugin-kubernetes-react": "0.5.11", + "@backstage/plugin-mcp-actions-backend": "0.1.3", + "@backstage/plugin-notifications": "0.5.9", + "@backstage/plugin-notifications-backend": "0.5.10", + "@backstage/plugin-notifications-backend-module-email": "0.3.13", + "@backstage/plugin-notifications-backend-module-slack": "0.1.5", + "@backstage/plugin-notifications-common": "0.1.0", + "@backstage/plugin-notifications-node": "0.2.19", + "@backstage/plugin-org": "0.6.44", + "@backstage/plugin-org-react": "0.1.42", + "@backstage/plugin-permission-backend": "0.7.4", + "@backstage/plugin-permission-backend-module-allow-all-policy": "0.2.12", + "@backstage/plugin-permission-common": "0.9.1", + "@backstage/plugin-permission-node": "0.10.4", + "@backstage/plugin-permission-react": "0.4.36", + "@backstage/plugin-proxy-backend": "0.6.6", + "@backstage/plugin-proxy-node": "0.1.8", + "@backstage/plugin-scaffolder": "1.34.1", + "@backstage/plugin-scaffolder-backend": "2.2.1", + "@backstage/plugin-scaffolder-backend-module-azure": "0.2.13", + "@backstage/plugin-scaffolder-backend-module-bitbucket": "0.3.14", + "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.2.13", + "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.2.13", + "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.3.13", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.3.15", + "@backstage/plugin-scaffolder-backend-module-gcp": "0.2.13", + "@backstage/plugin-scaffolder-backend-module-gerrit": "0.2.13", + "@backstage/plugin-scaffolder-backend-module-gitea": "0.2.13", + "@backstage/plugin-scaffolder-backend-module-github": "0.9.0", + "@backstage/plugin-scaffolder-backend-module-gitlab": "0.9.5", + "@backstage/plugin-scaffolder-backend-module-notifications": "0.1.14", + "@backstage/plugin-scaffolder-backend-module-rails": "0.5.13", + "@backstage/plugin-scaffolder-backend-module-sentry": "0.2.13", + "@backstage/plugin-scaffolder-backend-module-yeoman": "0.4.14", + "@backstage/plugin-scaffolder-common": "1.7.1", + "@backstage/plugin-scaffolder-node": "0.11.1", + "@backstage/plugin-scaffolder-node-test-utils": "0.3.3", + "@backstage/plugin-scaffolder-react": "1.19.1", + "@backstage/plugin-search": "1.4.30", + "@backstage/plugin-search-backend": "2.0.6", + "@backstage/plugin-search-backend-module-catalog": "0.3.8", + "@backstage/plugin-search-backend-module-elasticsearch": "1.7.6", + "@backstage/plugin-search-backend-module-explore": "0.3.7", + "@backstage/plugin-search-backend-module-pg": "0.5.48", + "@backstage/plugin-search-backend-module-stack-overflow-collator": "0.3.13", + "@backstage/plugin-search-backend-module-techdocs": "0.4.6", + "@backstage/plugin-search-backend-node": "1.3.15", + "@backstage/plugin-search-common": "1.2.19", + "@backstage/plugin-search-react": "1.9.4", + "@backstage/plugin-signals": "0.0.23", + "@backstage/plugin-signals-backend": "0.3.8", + "@backstage/plugin-signals-node": "0.1.24", + "@backstage/plugin-signals-react": "0.0.15", + "@backstage/plugin-techdocs": "1.15.0", + "@backstage/plugin-techdocs-addons-test-utils": "1.1.0", + "@backstage/plugin-techdocs-backend": "2.1.0", + "@backstage/plugin-techdocs-common": "0.1.1", + "@backstage/plugin-techdocs-module-addons-contrib": "1.1.28", + "@backstage/plugin-techdocs-node": "1.13.7", + "@backstage/plugin-techdocs-react": "1.3.3", + "@backstage/plugin-user-settings": "0.8.26", + "@backstage/plugin-user-settings-backend": "0.3.6", + "@backstage/plugin-user-settings-common": "0.0.1" + }, + "changesets": [] +} From 976fa7015cecbf0f545afd86d3ef11df023810aa Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 16 Sep 2025 16:12:40 +0100 Subject: [PATCH 231/233] Cleanup Signed-off-by: Charles de Dreuille --- ...erify_storybook-noop.yml => verify_chromatic-noop.yml} | 6 +++--- .../{verify_storybook.yml => verify_chromatic.yml} | 8 ++++---- .gitignore | 3 +-- package.json | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) rename .github/workflows/{verify_storybook-noop.yml => verify_chromatic-noop.yml} (90%) rename .github/workflows/{verify_storybook.yml => verify_chromatic.yml} (93%) diff --git a/.github/workflows/verify_storybook-noop.yml b/.github/workflows/verify_chromatic-noop.yml similarity index 90% rename from .github/workflows/verify_storybook-noop.yml rename to .github/workflows/verify_chromatic-noop.yml index 98ce6b26a1..600e8f9bdb 100644 --- a/.github/workflows/verify_storybook-noop.yml +++ b/.github/workflows/verify_chromatic-noop.yml @@ -1,11 +1,11 @@ # NO-OP placeholder that always passes for other paths # This is here so that we're able to set the status check as required -name: Storybook Void +name: Chromatic Void on: pull_request: paths-ignore: - - '.github/workflows/verify_storybook.yml' + - '.github/workflows/verify_chromatic.yml' - 'storybook/**' - 'packages/config/src/**' - 'packages/theme/src/**' @@ -25,7 +25,7 @@ jobs: noop: runs-on: ubuntu-latest - name: Storybook + name: Chromatic steps: - name: Harden Runner uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_chromatic.yml similarity index 93% rename from .github/workflows/verify_storybook.yml rename to .github/workflows/verify_chromatic.yml index 7c88a8f82e..86dbe6477c 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_chromatic.yml @@ -1,9 +1,9 @@ -name: Storybook +name: Chromatic on: # NOTE: If you change these you must update verify_storybook-noop.yml as well pull_request: paths: - - '.github/workflows/verify_storybook.yml' + - '.github/workflows/verify_chromatic.yml' - '.storybook/**' - 'packages/ui/src/**' - 'packages/config/src/**' @@ -26,7 +26,7 @@ jobs: os: [ubuntu-latest] node-version: [20.x] - name: Storybook + name: Chromatic steps: - name: Harden Runner uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 @@ -59,5 +59,5 @@ jobs: # projectToken intentionally shared to allow collaborators to run Chromatic on forks # https://www.chromatic.com/docs/custom-ci-provider#run-chromatic-on-external-forks-of-open-source-projects projectToken: chpt_dab72dc0f97d55b - storybookBuildDir: dist-storybook-chromatic + storybookBuildDir: dist-storybook onlyChanged: true diff --git a/.gitignore b/.gitignore index a4a3ec321b..aaf05b51cd 100644 --- a/.gitignore +++ b/.gitignore @@ -186,5 +186,4 @@ docs.json tsconfig.typedoc.tmp.json # Storybook -dist-storybook/ -dist-storybook-chromatic/ \ No newline at end of file +dist-storybook/ \ No newline at end of file diff --git a/package.json b/package.json index a048db1c60..0f1d16ce41 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ }, "scripts": { "build-storybook": "storybook build --output-dir dist-storybook", - "build-storybook:chromatic": "STORYBOOK_STORY_SET=chromatic storybook build --stats-json --output-dir dist-storybook-chromatic", + "build-storybook:chromatic": "STORYBOOK_STORY_SET=chromatic storybook build --stats-json --output-dir dist-storybook", "build:all": "backstage-cli repo build --all", "build:api-docs": "LANG=en_EN yarn build:api-reports --docs --exclude 'plugins/@(api-docs|api-docs-module-protoc-gen-doc|app-visualizer|catalog-graph|catalog-import|catalog-unprocessed-entities|config-schema|example-todo-list|example-todo-list-backend)'", "build:api-reports": "yarn build:api-reports:only --tsc", From 84a640cb83f87af769e63867a184ec6d5e7dc53f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 17 Sep 2025 09:35:08 +0200 Subject: [PATCH 232/233] fix release typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- docs/releases/v1.43.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releases/v1.43.0.md b/docs/releases/v1.43.0.md index 74169e6a2a..449023d167 100644 --- a/docs/releases/v1.43.0.md +++ b/docs/releases/v1.43.0.md @@ -84,7 +84,7 @@ Below you can find a list of links and references to help you learn about and st - [GitHub repository](https://github.com/backstage/backstage) - Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) - [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support -- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.1.0-changelog.md) +- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.43.0-changelog.md) - Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins) Sign up for our [newsletter](https://info.backstage.spotify.com/newsletter_subscribe) if you want to be informed about what is happening in the world of Backstage. From 6ebc1ead92c2c5140630ecd20da644e6dfbfa3eb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 17 Sep 2025 09:53:22 +0200 Subject: [PATCH 233/233] cli: fix mf config to only omit imports for remote Signed-off-by: Patrik Oldsberg --- .changeset/hungry-crews-fetch.md | 5 +++++ packages/cli/src/modules/build/lib/bundler/config.ts | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .changeset/hungry-crews-fetch.md diff --git a/.changeset/hungry-crews-fetch.md b/.changeset/hungry-crews-fetch.md new file mode 100644 index 0000000000..a9728e9cb0 --- /dev/null +++ b/.changeset/hungry-crews-fetch.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fixed module federation config by only setting `import: false` on shared libraries for remote. diff --git a/packages/cli/src/modules/build/lib/bundler/config.ts b/packages/cli/src/modules/build/lib/bundler/config.ts index 9031ce09d2..da444dff15 100644 --- a/packages/cli/src/modules/build/lib/bundler/config.ts +++ b/packages/cli/src/modules/build/lib/bundler/config.ts @@ -264,26 +264,26 @@ export async function createConfig( singleton: true, requiredVersion: '*', eager: !isRemote, - import: false, + ...(isRemote && { import: false }), }, 'react-dom': { singleton: true, requiredVersion: '*', eager: !isRemote, - import: false, + ...(isRemote && { import: false }), }, // React Router 'react-router': { singleton: true, requiredVersion: '*', eager: !isRemote, - import: false, + ...(isRemote && { import: false }), }, 'react-router-dom': { singleton: true, requiredVersion: '*', eager: !isRemote, - import: false, + ...(isRemote && { import: false }), }, // MUI v4 // not setting import: false for MUI packages as this